/**
 * @license
 * Copyright 2022-2024 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { camelize, GeneratedClass, ImplementationError, isObject } from "#general";
import { Access, ElementTag, Metatype } from "#model";
import { PhantomReferenceError, SchemaImplementationError } from "../../../errors.js";
import { Val } from "../../Val.js";
import { Instrumentation } from "../Instrumentation.js";
import { Internal } from "../Internal.js";
import { ManagedReference } from "../ManagedReference.js";
import { PrimitiveManager } from "./PrimitiveManager.js";
const SESSION = Symbol("options");
const AUTHORIZE_READ = Symbol("authorize-read");
function StructManager(owner, schema) {
  const instanceDescriptors = {};
  const propertyAccessControls = {};
  let hasFabricIndex = false;
  for (const member of schema.activeMembers) {
    const name2 = camelize(member.name);
    const { access, descriptor } = configureProperty(owner, member);
    instanceDescriptors[name2] = descriptor;
    propertyAccessControls[name2] = access;
    if (member.name === "FabricIndex") {
      hasFabricIndex = true;
    }
  }
  let name = schema.name;
  if (schema.tag === ElementTag.Cluster && !name.endsWith("$State")) {
    name = `${name}$State`;
  }
  const Wrapper = GeneratedClass({
    name,
    initialize(ref, session) {
      if (!isObject(ref.value)) {
        throw new SchemaImplementationError(
          ref.location,
          `Cannot manage ${typeof ref.value} because it is not a struct`
        );
      }
      if (hasFabricIndex) {
        const owningFabric = ref.value.fabricIndex;
        ref.location = { ...ref.location, owningFabric };
      }
      Object.defineProperties(this, {
        [Internal.reference]: {
          value: ref
        },
        [SESSION]: {
          value: session
        }
      });
      Object.defineProperties(this, instanceDescriptors);
    },
    instanceDescriptors: {
      // TODO - interferes with Chai deep equal.  Best fix would probably be a custom deep equal assertion but
      // leaving out for now
      // [Symbol.toStringTag]: {
      //     value: name,
      // },
      // TODO - makes Mocha diffs pretty useless.  Best fix is probably customized diff but leaving out for now
      // toString: {
      //     value() {
      //         return serialize(this);
      //     }
      // },
      // AUTHORIZE_READ is effectively a protected method, see StructManager.assertDirectReadAuthorized below
      [AUTHORIZE_READ]: {
        value(index) {
          const access = propertyAccessControls[index];
          if (access === void 0) {
            throw new ImplementationError(`Direct read of unknown property ${index}`);
          }
          access.authorizeRead(this[SESSION], this[Internal.reference].location);
        }
      }
    }
  });
  Instrumentation.instrumentStruct(Wrapper);
  return (reference, session) => {
    reference.owner = new Wrapper(reference, session);
    return reference.owner;
  };
}
((StructManager2) => {
  function assertDirectReadAuthorized(struct, index) {
    if (!struct?.[AUTHORIZE_READ]) {
      throw new ImplementationError("Cannot authorize read of unmanaged value");
    }
    return struct[AUTHORIZE_READ](index);
  }
  StructManager2.assertDirectReadAuthorized = assertDirectReadAuthorized;
})(StructManager || (StructManager = {}));
function configureProperty(manager, schema) {
  const name = camelize(schema.name);
  const { access, manage, validate } = manager.get(schema);
  const fabricScopedList = schema.effectiveAccess.fabric === Access.Fabric.Scoped && schema.effectiveMetatype === Metatype.array;
  const descriptor = {
    enumerable: true,
    set(value) {
      access.authorizeWrite(this[SESSION], this[Internal.reference].location);
      const oldValue = this[Internal.reference].value[name];
      const self = this;
      this[Internal.reference].change(() => {
        const struct = this[Internal.reference].value;
        let target;
        if (struct[Val.properties]) {
          const properties = struct[Val.properties](
            this[Internal.reference].rootOwner,
            this[SESSION]
          );
          if (name in properties) {
            target = properties;
          } else {
            target = struct;
          }
        } else {
          target = struct;
        }
        if (value && value[Internal.reference]) {
          value = value[Internal.reference].value;
        }
        if (fabricScopedList && Array.isArray(value) && Array.isArray(target[name])) {
          const proxy = self[name];
          for (let i = 0; i < value.length; i++) {
            proxy[i] = value[i];
          }
          proxy.length = value.length;
        } else {
          target[name] = value;
        }
        if (!this[SESSION].acceptInvalid && validate) {
          try {
            validate(value, this[SESSION], {
              path: this[Internal.reference].location.path,
              siblings: struct
            });
          } catch (e) {
            target[name] = oldValue;
            throw e;
          }
        }
      });
    }
  };
  if (manage === PrimitiveManager) {
    descriptor.get = function() {
      if (access.mayRead(this[SESSION], this[Internal.reference].location)) {
        const struct = this[Internal.reference].value;
        if (struct === void 0) {
          throw new PhantomReferenceError(this[Internal.reference].location);
        }
        if (struct[Val.properties]) {
          const properties = struct[Val.properties](this[Internal.reference].rootOwner, this[SESSION]);
          if (name in properties) {
            return properties[name];
          }
        }
        return struct[name];
      }
    };
  } else {
    let cloneContainer;
    switch (schema.effectiveMetatype) {
      case Metatype.array:
        cloneContainer = (container) => [...container];
        break;
      case Metatype.bitmap:
        cloneContainer = (container) => {
          if (typeof container === "number" || typeof container === "bigint") {
            return {};
          }
          return { ...container };
        };
        break;
      default:
        cloneContainer = (container) => ({ ...container });
    }
    descriptor.get = function() {
      let value;
      const struct = this[Internal.reference].value;
      if (struct[Val.properties]) {
        const properties = struct[Val.properties](
          this[Internal.reference].rootOwner,
          this[SESSION]
        );
        if (name in properties) {
          value = properties[name];
        } else {
          value = struct[name];
        }
      } else {
        value = struct[name];
      }
      if (value === void 0) {
        return;
      }
      if (!access.mayRead(this[SESSION], this[Internal.reference].location)) {
        return void 0;
      }
      if (value === null) {
        return value;
      }
      const managed = this[Internal.reference].subrefs?.[name];
      if (managed) {
        return managed.owner;
      }
      const assertWriteOk = (value2) => {
        access.authorizeWrite(this[SESSION], this[Internal.reference].location);
        if (validate) {
          validate(value2, this[SESSION], {
            path: this[Internal.reference].location.path,
            siblings: this[Internal.reference].value
          });
        }
      };
      const ref = ManagedReference(this[Internal.reference], name, assertWriteOk, cloneContainer);
      ref.owner = manage(ref, this[SESSION]);
      return ref.owner;
    };
  }
  return { descriptor, access };
}
export {
  StructManager
};
//# sourceMappingURL=StructManager.js.map
