/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { ImplementationError, InternalError, UnexpectedDataError } from "@matter/general";
import { FabricIndex, FieldElement } from "@matter/model";
import {
  ValidationDatatypeMismatchError,
  ValidationError,
  ValidationMandatoryFieldMissingError,
  ValidationOutOfBoundsError
} from "../common/ValidationError.js";
import { TlvAny } from "./TlvAny.js";
import { TlvType } from "./TlvCodec.js";
import { TlvSchema } from "./TlvSchema.js";
class ObjectSchema extends TlvSchema {
  constructor(fieldDefinitions, type = TlvType.Structure, allowProtocolSpecificTags = false) {
    super();
    this.fieldDefinitions = fieldDefinitions;
    this.type = type;
    this.allowProtocolSpecificTags = allowProtocolSpecificTags;
    let isFabricScoped = false;
    for (const name in this.fieldDefinitions) {
      const field = this.fieldDefinitions[name];
      if (field.repeated && type !== TlvType.List) {
        throw new Error("Repeated fields are only allowed in TLV List.");
      }
      this.fieldById[field.id] = { name, field };
      if (field.id === FabricIndex.id) {
        isFabricScoped = true;
      }
    }
    this.isFabricScoped = isFabricScoped;
  }
  isFabricScoped;
  fieldById = new Array();
  /** @deprecated Part of old ClusterType() compat layer. */
  get element() {
    const children = [];
    for (const name in this.fieldDefinitions) {
      const field = this.fieldDefinitions[name];
      const inner = field.schema.element;
      children.push(
        FieldElement({
          name,
          id: field.id,
          ...inner,
          ...field.optional && { conformance: "O" }
        })
      );
    }
    return { type: "struct", children };
  }
  #encodeEntryToTlv(writer, name, value, options) {
    const { id, schema, optional: isOptional, repeated: isRepeated } = this.fieldDefinitions[name];
    const { forWriteInteraction = false, allowMissingFieldsForNonFabricFilteredRead = false } = options ?? {};
    if (forWriteInteraction && allowMissingFieldsForNonFabricFilteredRead) {
      throw new InternalError(
        "Encode options cannot indicate a write interaction and a fabric filtered read interaction at the same time."
      );
    }
    const fieldValue = value[name];
    if (fieldValue === void 0) {
      if (!isOptional && !allowMissingFieldsForNonFabricFilteredRead) {
        if (forWriteInteraction && id === FabricIndex.id) {
          return;
        }
        throw new ValidationMandatoryFieldMissingError(`Missing mandatory field ${name}`, name);
      }
      return;
    }
    if (isRepeated) {
      if (!Array.isArray(fieldValue)) {
        throw new ValidationDatatypeMismatchError(`Repeated field ${name} should be an array.`, name);
      }
      for (const element of fieldValue) {
        schema.encodeTlvInternal(writer, element, { id }, options);
      }
    } else {
      schema.encodeTlvInternal(writer, fieldValue, { id }, options);
    }
  }
  /**
   * Encode the object as Structure, by the order of field definitions.
   */
  #encodeStructure(writer, value, options) {
    for (const name in this.fieldDefinitions) {
      this.#encodeEntryToTlv(writer, name, value, options);
    }
  }
  /**
   * Encode the object as List, by the order of the fields in the object.
   */
  #encodeList(writer, value, options) {
    const encodedFields = /* @__PURE__ */ new Set();
    for (const name of Object.keys(value)) {
      this.#encodeEntryToTlv(writer, name, value, options);
      encodedFields.add(name);
    }
    for (const name in this.fieldDefinitions) {
      if (encodedFields.has(name)) continue;
      this.#encodeEntryToTlv(writer, name, value, options);
    }
  }
  encodeTlvInternal(writer, value, tag, options) {
    writer.writeTag({ type: this.type }, tag);
    if (this.type === TlvType.Structure) {
      this.#encodeStructure(writer, value, options);
    } else {
      this.#encodeList(writer, value, options);
    }
    writer.writeTag({ type: TlvType.EndOfContainer });
  }
  decodeTlvInternalValue(reader, typeLength) {
    if (typeLength.type !== this.type)
      throw new UnexpectedDataError(`Unexpected type ${typeLength.type} (expected ${this.type}).`);
    const result = {};
    while (true) {
      const { tag: { profile, id } = {}, typeLength: elementTypeLength } = reader.readTagType();
      if (elementTypeLength.type === TlvType.EndOfContainer) break;
      if (profile !== void 0 && !this.allowProtocolSpecificTags)
        throw new UnexpectedDataError("Structure element tags should be context-specific.");
      if (id === void 0) throw new UnexpectedDataError("Structure element tags should have an id.");
      const fieldName = this.fieldById[id];
      if (fieldName === void 0) {
        TlvAny.decodeTlvInternalValue(reader, elementTypeLength);
        continue;
      }
      const { field, name } = fieldName;
      const decoded = field.schema.decodeTlvInternalValue(reader, elementTypeLength);
      if (field.repeated) {
        if (result[name] === void 0) {
          result[name] = [decoded];
        } else {
          result[name].push(decoded);
        }
      } else {
        result[name] = decoded;
      }
    }
    for (const name in this.fieldDefinitions) {
      const { optional, fallback, repeated } = this.fieldDefinitions[name];
      if (optional) continue;
      const value = result[name];
      if (value !== void 0) continue;
      if (fallback !== void 0) {
        if (repeated) {
          result[name] = [fallback];
        } else {
          result[name] = fallback;
        }
      }
    }
    return result;
  }
  validate(value) {
    for (const name in this.fieldDefinitions) {
      const { optional, schema, repeated: isRepeated } = this.fieldDefinitions[name];
      const data = value[name];
      if (data === void 0) {
        if (optional) {
          continue;
        }
        throw new ValidationMandatoryFieldMissingError(`Missing mandatory field ${name}`, name);
      }
      if (isRepeated) {
        const { minLength = 2, maxLength = 65535 } = this.fieldDefinitions[name];
        if (!Array.isArray(data)) {
          throw new ValidationDatatypeMismatchError(`Repeated field ${name} should be an array.`, name);
        }
        if (data.length > maxLength)
          throw new ValidationOutOfBoundsError(
            `Repeated field list for ${name} is too long: ${data.length}, max ${maxLength}.`,
            name
          );
        if (data.length < minLength)
          throw new ValidationOutOfBoundsError(
            `Repeated field list for ${name} is too short: ${data.length}, min ${minLength}.`,
            name
          );
        for (const element of data) {
          try {
            schema.validate(element);
          } catch (e) {
            ValidationError.accept(e);
            e.fieldName = `${name}${e.fieldName !== void 0 ? `.${e.fieldName}` : ""}`;
            throw e;
          }
        }
      } else {
        try {
          schema.validate(data);
        } catch (e) {
          ValidationError.accept(e);
          e.fieldName = `${name}${e.fieldName !== void 0 ? `.${e.fieldName}` : ""}`;
          throw e;
        }
      }
    }
  }
  injectField(value, fieldId, fieldValue, injectChecker) {
    for (const k in this.fieldDefinitions) {
      const field = this.fieldDefinitions[k];
      if (field.id === fieldId) {
        if (injectChecker(value[k])) {
          field.schema.validate(fieldValue);
          value[k] = fieldValue;
        }
      } else {
        value[k] = field.schema.injectField(value[k], fieldId, fieldValue, injectChecker);
      }
    }
    return value;
  }
  removeField(value, fieldId, removeChecker) {
    for (const k in this.fieldDefinitions) {
      const field = this.fieldDefinitions[k];
      if (field.id === fieldId) {
        if (value[k] !== void 0 && removeChecker(value[k])) {
          delete value[k];
        }
      } else {
        value[k] = field.schema.removeField(value[k], fieldId, removeChecker);
      }
    }
    return value;
  }
}
const TlvObject = (fields) => new ObjectSchema(fields, TlvType.Structure);
class ObjectSchemaWithMaxSize extends ObjectSchema {
  constructor(fieldDefinitions, maxSize, type = TlvType.Structure, allowProtocolSpecificTags = false) {
    super(fieldDefinitions, type, allowProtocolSpecificTags);
    this.maxSize = maxSize;
  }
  encode(value) {
    const encoded = super.encode(value);
    if (encoded.byteLength > this.maxSize) {
      throw new ImplementationError(
        `Encoded TLV object with ${encoded.byteLength} bytes exceeds maximum size of ${this.maxSize} bytes.`
      );
    }
    return encoded;
  }
}
const TlvObjectWithMaxSize = (fields, maxSize) => new ObjectSchemaWithMaxSize(fields, maxSize, TlvType.Structure);
const TlvTaggedList = (fields, allowProtocolSpecificTags = false) => new ObjectSchema(fields, TlvType.List, allowProtocolSpecificTags);
const TlvField = (id, schema, fallback) => ({ id, schema, fallback, optional: false });
const TlvOptionalField = (id, schema) => ({ id, schema, optional: true });
const TlvRepeatedField = (id, schema, lengthOptions) => {
  const { minLength, maxLength, length } = lengthOptions ?? {};
  return {
    id,
    schema,
    optional: false,
    repeated: true,
    minLength: length ?? minLength,
    maxLength: length ?? maxLength
  };
};
const TlvOptionalRepeatedField = (id, schema, lengthOptions) => {
  const { maxLength } = lengthOptions ?? {};
  return {
    id,
    schema,
    optional: true,
    repeated: true,
    minLength: 0,
    maxLength
  };
};
export {
  ObjectSchema,
  ObjectSchemaWithMaxSize,
  TlvField,
  TlvObject,
  TlvObjectWithMaxSize,
  TlvOptionalField,
  TlvOptionalRepeatedField,
  TlvRepeatedField,
  TlvTaggedList
};
//# sourceMappingURL=TlvObject.js.map
