/**
 * @license
 * Copyright 2022-2025 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { InternalError } from "@matter/general";
import { Access } from "../aspects/index.js";
import { ElementTag, FieldValue, Metatype } from "../common/index.js";
import {
  enum16,
  enum8,
  map16,
  map32,
  map64,
  map8,
  uint16,
  uint32,
  uint64,
  uint8
} from "../standard/elements/definitions.js";
const OPERATION_DEPTH_LIMIT = 20;
let memos;
class ModelTraversal {
  #operationDepth = 0;
  #dismissed;
  /**
   * Perform an operation with iteration tracking.  If iteration depth limit
   */
  operation(operator, toDismiss) {
    if (this.#operationDepth > OPERATION_DEPTH_LIMIT) {
      throw new InternalError("Likely cycle detected (or OPERATION_DEPTH_LIMIT needs to be bumped)");
    }
    if (toDismiss && this.#dismissed?.has(toDismiss)) {
      toDismiss = void 0;
    }
    if (toDismiss) {
      if (!this.#dismissed) {
        this.#dismissed = /* @__PURE__ */ new Set();
      }
      this.#dismissed.add(toDismiss);
    }
    this.#operationDepth++;
    try {
      return operator();
    } finally {
      this.#operationDepth--;
      if (toDismiss) {
        this.#dismissed?.delete(toDismiss);
      }
    }
  }
  /**
   * Perform an operation with a model dismissed from consideration for type lookup.
   */
  operationWithDismissal(toDismiss, operator) {
    return this.operation(operator, toDismiss);
  }
  /**
   * Determine the type for a model.  This is the string name of the base model.  Usually this is simply the type
   * field but we infer the type in some cases where it is not supplied explicitly:
   *
   * - Children of maps and enums are uints of corresponding size
   * - Models that shadow another model in their parent's inheritance effectively inherit from the shadow so have
   *   their own name as the type name
   */
  getTypeName(model) {
    if (!model) {
      return void 0;
    }
    if (model.type) {
      return model.type;
    }
    if (model.tag === ElementTag.Command || model.tag === ElementTag.Event || model.tag === ElementTag.Cluster) {
      return "struct";
    }
    const getTypeNameOp = () => {
      let result;
      const name = model.name;
      this.visitInheritance(this.findParent(model), (ancestor) => {
        if (ancestor.effectiveMetatype) {
          switch (ancestor.name) {
            case enum8.name:
            case map8.name:
              result = uint8.name;
              return false;
            case enum16.name:
            case map16.name:
              result = uint16.name;
              return false;
            case map32.name:
              result = uint32.name;
              return false;
            case map64.name:
              result = uint64.name;
              return false;
          }
        }
        const shadow = ancestor.children.select(name, [model.tag], this.#dismissed);
        if (shadow?.type) {
          result = shadow.name;
          return false;
        }
      });
      return result;
    };
    return this.operation(getTypeNameOp);
  }
  /**
   * Find the model in my inheritance hierarchy that has semantic meaning. This will be the first inherited model with
   * a metatype.
   */
  findMetabase(model) {
    return this.operation(() => {
      while (model && !model.metatype) {
        model = this.findBase(model);
      }
      return model;
    });
  }
  /**
   * Find the model a model derives from, if any.
   */
  findBase(model) {
    if (!model) {
      return;
    }
    if (model.operationalBase) {
      return model.operationalBase;
    }
    if (memos?.bases.has(model)) {
      return memos.bases.get(model);
    }
    const findBaseOp = () => {
      const shadow = this.findShadow(model);
      if (shadow !== void 0 && (model.type === void 0 || model.type === shadow.type || model.type === shadow.name)) {
        return shadow;
      }
      const type = this.getTypeName(model);
      if (type === void 0) {
        return;
      }
      const path = type.split(".");
      if (path.length === 1) {
        for (const tag of model.allowedBaseTags) {
          const found = this.findType(this.findParent(model), path[0], tag);
          if (found) {
            return found;
          }
        }
        return;
      }
      for (const tag of model.allowedBaseTags) {
        const found = this.findQualifiedType(this.findParent(model), path, tag);
        if (found) {
          return found;
        }
      }
    };
    const base = this.operationWithDismissal(model, findBaseOp);
    memos?.bases.set(model, base);
    return base;
  }
  /**
   * Find the first global model this model derives from, if any.
   */
  findGlobalBase(model) {
    if (!model) {
      return;
    }
    let result;
    const globalBaseSearchOp = (model2) => {
      if (model2.isGlobal) {
        result = model2;
        return false;
      }
    };
    this.visitInheritance(model, globalBaseSearchOp);
    return result;
  }
  /**
   * Determine whether this model derives from another.
   */
  instanceOf(model, other) {
    if (!model || !other) {
      return false;
    }
    if (model === other) {
      return true;
    }
    let result = false;
    this.visitInheritance(model, (model2) => {
      if (model2.name === other.name && model2.isGlobal === other.isGlobal) {
        result = true;
        return false;
      }
    });
    return result;
  }
  /**
   * Find an xref from this model or a parent.
   */
  findXref(model) {
    return this.operationWithDismissal(model, () => {
      if (!model) {
        return;
      }
      if (model.xref) {
        return model.xref;
      }
      return this.findXref(this.findParent(model));
    });
  }
  /**
   * Find the model this model derives from that has children, if any.
   */
  findDefiningModel(model) {
    let result;
    this.visitInheritance(model, (model2) => {
      if (!model2.isType) {
        return false;
      }
      if (model2.children.length) {
        result = model2;
        return false;
      }
    });
    return result;
  }
  /**
   * Find a child in the parent's inheritance hierarchy with the same tag and ID/name.
   */
  findShadow(model) {
    if (model === void 0) {
      return void 0;
    }
    if (model.operationalShadow !== void 0) {
      return model.operationalShadow ?? void 0;
    }
    if (memos?.shadows.has(model)) {
      return memos.shadows.get(model);
    }
    let shadow;
    const shadowSearchOp = () => {
      this.visitInheritance(this.findBase(this.findParent(model)), (parent) => {
        if (model.id !== void 0 && model.tag !== ElementTag.Command) {
          shadow = parent.children.select(model.id, [model.tag], this.#dismissed);
          if (shadow) {
            return false;
          }
        }
        shadow = parent.children.select(model.name, [model.tag], this.#dismissed);
        if (shadow) {
          return false;
        }
      });
    };
    this.operationWithDismissal(model, shadowSearchOp);
    memos?.shadows.set(model, shadow);
    return shadow;
  }
  /**
   * Get an aspect that reflects extension of any shadowed aspects.  Note that this searches the parent's inheritance
   * and the model's inheritance. This is because aspects can be inherited by overriding an element in the parent or
   * by direct type inheritance.  Aspects in shadowed elements take priority as they are presumably more specific.
   */
  findAspect(model, symbol) {
    if (!model) {
      return;
    }
    const findAspectOp = () => {
      let aspect = model[symbol];
      const inheritedAspect = this.findAspect(this.findBase(model), symbol);
      if (inheritedAspect) {
        if (aspect) {
          aspect = inheritedAspect.extend(aspect);
        } else {
          aspect = inheritedAspect;
        }
      }
      return aspect;
    };
    return this.operation(findAspectOp);
  }
  /**
   * Constraint aspects are specialized because we infer constraint fields that are referenced in other models.
   */
  findConstraint(model, symbol, field) {
    return this.operation(() => {
      let constraint = this.findAspect(model, symbol);
      if (constraint === void 0) {
        return;
      }
      const bounds = {};
      const resolve = (field2) => {
        const value = constraint[field2];
        const name = FieldValue.referenced(value);
        if (name === void 0) {
          return;
        }
        const referenced = this.findMember(this.findParent(model), name, [
          ElementTag.Attribute,
          ElementTag.Field
        ]);
        if (!referenced) {
          return;
        }
        const otherConstraint = this.findConstraint(referenced, symbol, field2);
        if (otherConstraint?.[field2]) {
          bounds[field2] = otherConstraint[field2];
        }
      };
      if (field) {
        resolve(field);
      } else {
        resolve("value");
        resolve("min");
        resolve("max");
      }
      if (Object.keys(bounds).length) {
        constraint = constraint.extend(bounds);
      }
      return constraint;
    });
  }
  /**
   * Access aspects are specialized because access controls are inherited from the owner if not otherwise defined.
   *
   * That means access controls may come from 5 places, in order of priority:
   *
   *   1. The model itself
   *   2. A shadowed model in the owner hierarchy
   *   3. An overridden model in the model's class hierarchy
   *   4. A model in the parent hierarchy
   *   5. Access.Default
   *
   * This method uses {@link findAspect} for 1-3 then extends the result with 4 & 5 as necessary until
   * {@link Access.complete} is true.
   */
  findAccess(model, symbol, VM) {
    if (model === void 0) {
      return Access.Default;
    }
    return this.operation(() => {
      const access = this.findAspect(model, symbol);
      if (!access) {
        return this.findAccess(this.findOwner(VM, model), symbol, VM);
      }
      if (access.complete) {
        return access;
      }
      return this.findAccess(this.findOwner(VM, model), symbol, VM).extend(access);
    });
  }
  /**
   * Search inherited scope for a named member.
   */
  findMember(scope, key, allowedTags) {
    return this.operation(() => {
      while (scope) {
        const result = scope.children.select(key, allowedTags, this.#dismissed);
        if (result) {
          return result;
        }
        scope = this.findBase(scope);
      }
    });
  }
  /**
   * Search inherited scope for a bit definition.
   */
  findBitDefinition(scope, bit) {
    return this.operation(() => {
      while (scope) {
        if (!scope.isType) {
          return;
        }
        if (scope.effectiveMetatype !== Metatype.bitmap) {
          scope = this.findParent(scope);
          continue;
        }
        for (const c of scope.children) {
          if (c.constraint.test(bit)) {
            return c;
          }
        }
        scope = this.findBase(scope);
      }
    });
  }
  /**
   * Search inherited and structural type scope for a named type.
   */
  findType(owner, name, tag) {
    if (!owner) {
      return;
    }
    const memoKey = `${name} ${tag}`;
    const memosForScope = memos?.types.get(owner);
    if (memosForScope && memoKey in memosForScope) {
      return memosForScope[memoKey];
    }
    const findTypeOp = () => {
      const queue = Array(owner);
      for (let scope = queue.shift(); scope; scope = queue.shift()) {
        if (scope.isScope) {
          const result = scope.children.select(name, tag, this.#dismissed);
          if (result) {
            return result;
          }
        }
        const inheritedScope = this.findBase(scope);
        if (inheritedScope) {
          queue.unshift(inheritedScope);
        }
        const parent = this.findParent(scope);
        if (parent) {
          queue.push(parent);
        }
      }
      if (ModelTraversal.fallbackScope && owner !== ModelTraversal.fallbackScope) {
        return this.findType(ModelTraversal.fallbackScope, name, tag);
      }
    };
    const type = this.operation(findTypeOp);
    if (memos) {
      const memosForScope2 = memos.types.get(owner);
      if (memosForScope2) {
        memosForScope2[memoKey] = type;
      } else {
        memos.types.set(owner, { [memoKey]: type });
      }
    }
    return type;
  }
  /**
   * Similar to findType but operates with a qualified type name.
   *
   * Unlike findType, a qualified type may reference an element parented by any other, not just those within
   * ScopeModels.
   *
   * This is quite complicated and would be painfully slow except in practice we don't use many qualified types and
   * those we do use resolve with few failing branches in the search once the root qualifier of the name matches.
   */
  findQualifiedType(scope, path, tag) {
    if (!scope) {
      return;
    }
    function resolve(scope2, position = 0) {
      if (position === path.length - 1) {
        return scope2.children.select(path[position], tag);
      }
      for (const subscope of scope2.children.selectAll(path[position])) {
        const result = resolve(subscope, position + 1);
        if (result) {
          return result;
        }
      }
      if (position === 0 && scope2.tag === ElementTag.Matter && path[0] === "Matter") {
        const result = resolve(scope2, position + 1);
        if (result) {
          return result;
        }
      }
    }
    return this.operation(() => {
      const queue = Array(scope);
      for (scope = queue.shift(); scope; scope = queue.shift()) {
        const resolved = resolve(scope);
        if (resolved) {
          return resolved;
        }
        const inheritedScope = this.findBase(scope);
        if (inheritedScope) {
          queue.unshift(inheritedScope);
        }
        const parent = this.findParent(scope);
        if (parent) {
          queue.push(parent);
        }
      }
    });
  }
  /**
   * Find the response model for a command.
   */
  findResponse(command) {
    if (command.response && command.response !== "status") {
      return new ModelTraversal().findType(command, command.response, ElementTag.Command);
    }
  }
  /**
   * Find all children of a node that reference a specific type.
   */
  findReferences(scope, type) {
    if (!scope || !type) {
      return [];
    }
    const references = Array();
    this.visit(scope, (model) => {
      if (this.findBase(model) === type) {
        references.push(model);
        return;
      }
      if (model.tag === ElementTag.Command && this.findResponse(model) === type) {
        references.push(model);
        return;
      }
      if (model.isType) {
        const defaultValue = model.default;
        if (FieldValue.is(defaultValue, FieldValue.reference)) {
          if (defaultValue.name === type.name) {
            references.push(model);
          }
        }
      }
    });
    return references;
  }
  /**
   * Find an owning model of a specific type.
   */
  findOwner(constructor, model) {
    const parent = this.findParent(model);
    if (!parent || parent instanceof constructor) {
      return parent;
    }
    return this.operation(() => {
      return this.findOwner(constructor, parent);
    });
  }
  /**
   * Find the root model.
   */
  findRoot(model) {
    if (!model) {
      return void 0;
    }
    const parent = this.findParent(model);
    if (!parent) {
      return model;
    }
    return this.operation(() => {
      return this.findRoot(parent);
    });
  }
  /**
   * Visit all nodes in the model tree.
   */
  visit(model, visitor) {
    return this.operation(() => {
      if (visitor(model) === false) {
        return false;
      }
      for (const c of model.children) {
        if (this.visit(c, visitor) === false) {
          return false;
        }
      }
      return true;
    });
  }
  /**
   * Visit all nodes in the inheritance hierarchy until the visitor returns false.
   */
  visitInheritance(model, visitor) {
    if (!model) {
      return;
    }
    return this.operation(() => {
      if (visitor(model) === false) {
        return false;
      }
      const base = this.findBase(model);
      return this.visitInheritance(base, visitor);
    });
  }
  /**
   * Find the parent for this model.
   */
  findParent(model) {
    if (model === void 0) {
      return;
    }
    if (model.parent) {
      return model.parent;
    }
    if (model.tag === ElementTag.Matter) {
      return void 0;
    }
    return this.findScope(model);
  }
  /**
   * Find an owner that defines type scope for a model.
   */
  findScope(model) {
    if (model === void 0) {
      return;
    }
    if (model.parent) {
      if (model.parent.isScope) {
        return model.parent;
      }
      return this.operationWithDismissal(model, () => this.findScope(model.parent));
    }
    if (model.operationalBase) {
      return this.operationWithDismissal(model, () => this.findScope(model.operationalBase ?? void 0));
    }
    return ModelTraversal.fallbackScope;
  }
  /**
   * If a model is not owned by a MatterModel, global resolution won't work.  This model acts as a fallback to work
   * around this.
   */
  static fallbackScope;
  /**
   * This is a cheap hack to optimize analysis of large static models.  It temporarily memoizes key operations.
   *
   * It's not a huge win but does speed up model validation by approximately 50%
   */
  static memoize(fn) {
    if (memos) {
      fn();
      return;
    }
    try {
      memos = { bases: /* @__PURE__ */ new Map(), shadows: /* @__PURE__ */ new Map(), types: /* @__PURE__ */ new Map() };
      fn();
    } finally {
      memos = void 0;
    }
  }
}
export {
  ModelTraversal
};
//# sourceMappingURL=ModelTraversal.js.map
