/**
 * @license
 * Copyright 2022-2025 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { camelize, decamelize, ImplementationError, InternalError } from "#general";
import { ElementTag, Metatype } from "../common/index.js";
import { ModelTraversal } from "../logic/ModelTraversal.js";
import { Children } from "./Children.js";
const inspect = Symbol.for("nodejs.util.inspect.custom");
class Model {
  type;
  isSeed;
  description;
  details;
  xref;
  errors;
  asOf;
  until;
  #id = void 0;
  #name;
  #frozen;
  /**
   * Indicates that an element defines a datatype.
   */
  isType;
  /**
   * Normally {@link base} performs lookup based on {@link type}.  Setting this field short circuits this resolution.
   *
   * The operational base also enables resolution from the operational base's tree.  This enables resolution on
   * operational models that are not installed in a parent hierarchy.
   *
   * We set the operational base when freezing the model.
   */
  operationalBase;
  /**
   * This is like {@link operationalBase} except for the element's shadow.
   */
  operationalShadow;
  #children;
  #parent;
  /**
   * Did validation find errors?
   */
  get valid() {
    return !this.errors;
  }
  /**
   * The path ("." delimited) in the Matter tree.
   *
   * This is informational and generally tries to adhere to JS API conventions.
   */
  get path() {
    if (this.parent && this.parent.tag !== ElementTag.Matter) {
      if (this.parent.tag === ElementTag.Field) {
        return `${this.parent.path}.${camelize(this.name)}`;
      }
      if (this.parent.tag === ElementTag.Cluster) {
        switch (this.tag) {
          case ElementTag.Attribute:
            return `${this.parent.path}.state.${camelize(this.name, false)}`;
          case ElementTag.Command:
            return `${this.parent.path}.${camelize(this.name, false)}`;
          case ElementTag.Event:
            return `${this.parent.path}.events.${camelize(this.name, false)}`;
        }
      }
      const parent = this.parent;
      if (parent.tag !== ElementTag.Cluster) {
        const parentMetatype = parent?.effectiveMetatype;
        if (parentMetatype === Metatype.object || parentMetatype === Metatype.array) {
          return `${parent.path}.${camelize(this.name, false)}`;
        }
      }
      return `${parent.path}.${this.name}`;
    } else {
      return this.name;
    }
  }
  /**
   * Determine if this model resides in the global namespace.
   */
  get isGlobal() {
    return this.tag === "matter" || this.parent?.tag === "matter";
  }
  /**
   * The structural parent.  This is the model for the element that contains this element's definition.
   */
  get parent() {
    return this.#parent;
  }
  set parent(parent) {
    if (this.parent === parent) {
      return;
    }
    if (this.parent) {
      const index = this.parent.children.indexOf(this);
      if (index !== -1) {
        this.parent.children.splice(index, 1);
      }
    }
    if (parent) {
      parent.children.push(this);
    }
  }
  /**
   * Allows subclasses to pull a working ID from an alternate source.
   */
  get effectiveId() {
    return this.id;
  }
  /**
   * Get a string that uniquely identifies this model.  This is normally the effective ID but some models require a
   * generated identifier.
   */
  get key() {
    const key = this.effectiveId?.toString();
    if (key === void 0) {
      return key;
    }
    const discriminator = this.discriminator;
    if (discriminator === void 0) {
      return key;
    }
    return `${key}\u241C${discriminator}`;
  }
  /**
   * Obtain a discriminator that differentiates different models with the same name
   */
  get discriminator() {
    return;
  }
  /**
   * Children of models are always models.
   */
  get children() {
    if (!this.#children) {
      this.children = [];
    }
    return this.#children;
  }
  /**
   * Set the children of the model.
   */
  set children(children) {
    this.#children = Children(
      children,
      (child) => {
        if (child.#parent === this) {
          return;
        }
        if (child.#parent) {
          const position = child.#parent.children.indexOf(child);
          if (position !== -1) {
            child.#parent.children.splice(position, 1);
          }
          child.#parent = void 0;
        }
        child.#parent = this;
      },
      (child) => {
        if (child.#parent === this) {
          child.#parent = void 0;
          return true;
        }
        return false;
      }
    );
  }
  /**
   * Factory support.  Populated by derivatives upon definition.
   */
  static types = {};
  /**
   * In some circumstances the base type can be inferred.  This inference happens here.
   *
   * Does not recurse so only returns the direct base type.
   */
  get effectiveType() {
    return this.type;
  }
  /**
   * Get a model for my base type as defined by {@link type}, if any.
   */
  get base() {
    if (this.operationalBase !== void 0) {
      return this.operationalBase ?? void 0;
    }
    return new ModelTraversal().findBase(this);
  }
  /**
   * Get shadow model, if any.  A "shadow" is an element in my parent's inheritance hierarchy that I override.
   */
  get shadow() {
    if (this.operationalShadow !== void 0) {
      return this.operationalShadow ?? void 0;
    }
    return new ModelTraversal().findShadow(this);
  }
  /**
   * Get the first global base type.  This may have semantic meaning more specific than the base primitive type.
   */
  get globalBase() {
    return new ModelTraversal().findGlobalBase(this);
  }
  /**
   * A local or parent xref.
   */
  get effectiveXref() {
    return new ModelTraversal().findXref(this);
  }
  /**
   * The set of tags from which this model may derive.
   */
  get allowedBaseTags() {
    return [this.tag];
  }
  /**
   * Update a subset of fields.
   */
  patch(values) {
    for (const [k, v] of Object.entries(values.valueOf())) {
      this[k] = v;
    }
  }
  /**
   * Determine whether this element applies to a specific revision.
   */
  appliesTo(revision) {
    return (this.asOf === void 0 || revision >= this.asOf) && (this.until === void 0 || revision < this.until);
  }
  /**
   * Create a model for an element.
   */
  static create(definition) {
    if (typeof definition !== "object") {
      throw new InternalError(`Model definition must be object, not ${typeof definition}`);
    }
    const t = definition["tag"];
    const constructor = Model.types[t];
    if (!constructor) {
      throw new InternalError(`Unknown element tag "${t}"`);
    }
    return new constructor(definition);
  }
  /**
   * Retrieve all models of a specific element type from local scope.
   *
   * @param type model class or a predicate object
   * @param key filters to models matching a specific type
   */
  all(type, key) {
    return this.children.all(type, key);
  }
  /**
   * Retrieve a specific child by ID or name.
   */
  get(type, key) {
    return this.children.get(type, key);
  }
  /**
   * Retrieve a model of a specific type from the ownership hierarchy.
   */
  owner(constructor) {
    return new ModelTraversal().findOwner(constructor, this);
  }
  /**
   * Record a validation error for this model.
   */
  error(code, message) {
    if (!this.errors) {
      this.errors = [];
    }
    this.errors.push({
      code,
      source: this.path,
      message,
      xref: this.effectiveXref?.toString()
    });
  }
  /**
   * Convert model to JSON.
   */
  toJSON() {
    const fields = this.valueOf();
    if (this.children.length) {
      fields.children = this.children.map((child) => child.toJSON());
    }
    return fields;
  }
  /**
   * Convert to non-class structure.
   */
  valueOf() {
    const result = {};
    for (const key in this) {
      switch (key) {
        case "parent":
        case "errors":
        case "scope":
        case "isType":
        case "operationalBase":
        case "isScope":
          continue;
        default:
          if (this[key] !== void 0 && (this[key] !== null || key === "default")) {
            result[key] = this[key];
          }
      }
    }
    return result;
  }
  /**
   * Apply a function to all tree elements.
   */
  visit(visitor) {
    return new ModelTraversal().visit(this, visitor);
  }
  /**
   * Visit this model and each of its ancestors.
   */
  forEachAncestor(fn) {
    new ModelTraversal().visitInheritance(this, fn);
  }
  /**
   * Find all children that reference a specific type.
   */
  references(type) {
    return new ModelTraversal().findReferences(this, type);
  }
  /**
   * Search the inheritance chain for a child property.
   */
  member(key, allowedTags = [ElementTag.Field, ElementTag.Attribute]) {
    return new ModelTraversal().findMember(this, key, allowedTags);
  }
  /**
   * Does this model derive from another?
   */
  instanceOf(other) {
    return new ModelTraversal().instanceOf(this, other);
  }
  /**
   * Clone the model.  This deep copies all descendant child models but not other properties.
   */
  clone() {
    const Type = this.constructor;
    return new Type(this);
  }
  /**
   * Create an operational extension of the model.  This creates a new model that inherits from this model for
   * operational purposes.
   */
  extend(properties, ...children) {
    const constructor = this.constructor;
    const definition = {
      id: this.id,
      name: this.name,
      ...properties,
      tag: this.tag,
      operationalBase: this
    };
    if (children.length) {
      if (definition.children) {
        definition.children = [...definition.children, ...children];
      } else {
        definition.children = children;
      }
    }
    const extension = new constructor(definition);
    return extension;
  }
  constructor(definition, ...children) {
    if (typeof definition !== "object") {
      throw new ImplementationError(`Model definition must be an object, not ${typeof definition}`);
    }
    const isClone = definition instanceof Model;
    this.#id = definition.id;
    this.#name = definition.name;
    for (const [k, v] of Object.entries(definition)) {
      if (k === "id" || k === "name" || k === "parent" || k === "isGlobal") {
        continue;
      }
      if (v !== void 0) {
        this[k] = v;
      }
    }
    if (this.xref) {
      this.xref = Model.CrossReference.get(this.xref);
    }
    if (isClone) {
      for (const child of definition.children) {
        this.children.push(child.clone());
      }
    }
    if (children.length) {
      this.children.push(...children);
    }
  }
  /**
   * Freeze the model hierarchy rooted at this model.
   *
   * When using a model as operational schema we implement various optimizations that assume the schema is immutable.
   * This function enforces that assumption and caches a few values that only make sense with frozen schema.
   *
   * To make changes to a frozen model use {@link clone}.
   */
  freeze() {
    if (this.#frozen) {
      return;
    }
    const base = this.operationalBase ?? (this.operationalBase = this.base ?? null);
    const shadow = this.operationalShadow ?? (this.operationalShadow = this.shadow ?? null);
    this.#frozen = true;
    Object.freeze(this);
    this.children.freeze();
    base?.freeze();
    shadow?.freeze();
  }
  toString() {
    return `${this.tag}${this.type ? `<${this.type}>` : ""}#${this.path}`;
  }
  static {
    Object.defineProperties(Model.prototype, {
      id: {
        get() {
          return this.#id;
        },
        set(value) {
          const oldId = this.effectiveId;
          this.#id = value;
          this.#parent?.children.updateId(this, oldId);
        },
        enumerable: true
      },
      name: {
        get() {
          return this.#name;
        },
        set(value) {
          const oldName = this.#name;
          this.#name = value;
          this.#parent?.children.updateName(this, oldName);
        },
        enumerable: true
      }
    });
  }
  [inspect](_depth, options, inspect2) {
    const json = this.valueOf();
    const props = {
      name: json.name
    };
    if (json.id !== void 0) {
      props.id = json.id;
    }
    for (const key in json) {
      if (key === "id" || key === "name" || key === "tag") {
        continue;
      }
      props[key] = json[key];
    }
    if (this.#children !== void 0 && this.#children.length) {
      props.children = this.#children.length;
    }
    return `${inspect2(props, options)}`.replace(/^\{/, `${decamelize(this.tag)} {`);
  }
}
((Model2) => {
  class CrossReference {
    document;
    section;
    static instances = {};
    constructor({ document, section }) {
      this.document = document;
      this.section = section;
    }
    toString() {
      return `${this.document}\xA7${this.section}`;
    }
    static get(xref) {
      const key = `${xref.document}:${xref.section}`;
      const canonical = this.instances[key];
      if (canonical) {
        return canonical;
      }
      return this.instances[key] = new CrossReference(xref);
    }
    [inspect](_depth, options, inspect2) {
      return inspect2(this.toString(), options);
    }
  }
  Model2.CrossReference = CrossReference;
})(Model || (Model = {}));
export {
  Model
};
//# sourceMappingURL=Model.js.map
