/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { ElementTag } from "#common/ElementTag.js";
import { camelize, ImplementationError } from "@matter/general";
let ModelConstructor = void 0;
class ChildList {
  #children;
  #reified = false;
  #indices;
  #onNameChanged;
  #position;
  #proxy;
  constructor(initial, position) {
    this.#children = Array();
    this.#position = position;
    const impl = this;
    this.#proxy = new Proxy(this.#children, {
      get(target, name, receiver) {
        return impl.#proxyGet(target, name, receiver);
      },
      set(_target, name, value, receiver) {
        return impl.#proxySet(name, value, receiver);
      },
      deleteProperty(_target, p) {
        return impl.#proxyDeleteProperty(p);
      }
    });
    initial = [...initial];
    this.#proxy.push(...initial);
  }
  get proxy() {
    return this.#proxy;
  }
  /**
   * Enter "reified" mode.  Converts all element children to Model instances.
   */
  #reify() {
    if (this.#reified) {
      return;
    }
    for (let i = 0; i < this.#children.length; i++) {
      let child = this.#children[i];
      if (child instanceof ModelConstructor) {
        continue;
      }
      child = ModelConstructor.create(child);
      this.#doAdopt(child);
      this.#children[i] = child;
    }
    this.#reified = true;
  }
  /**
   * Determine if an element has any Model children.  If so we need to upgrade to a model on insertion.
   */
  #hasModel(child) {
    if (child instanceof ModelConstructor) {
      return true;
    }
    return child.children?.some((c) => this.#hasModel(c)) ?? false;
  }
  /**
   * Convert a new child to "insertion" form.  The input may be an element or model.  If we are reified, we upgrade
   * elements to models.  If child or any descendents is a model, we reify so models will have the correct parent
   * after insertion.
   */
  #insertionFormOf(child) {
    if (child instanceof ModelConstructor) {
      return child;
    }
    if (this.#reified || this.#hasModel(child)) {
      return ModelConstructor.create(child);
    }
    return child;
  }
  /**
   * Add a model to a name or ID index.
   */
  #indexInsert(index, key, model) {
    const existing = index[key];
    if (existing) {
      if (Array.isArray(existing)) {
        existing.push(model);
      } else {
        index[key] = [existing, model];
      }
    } else {
      index[key] = model;
    }
  }
  /**
   * Remove a model from a name or ID index.
   */
  #indexDelete(index, key, model) {
    const existing = index[key];
    if (existing === model) {
      delete index[key];
    }
    if (Array.isArray(existing)) {
      const pos = existing.indexOf(model);
      if (pos === -1) {
        return;
      }
      existing.splice(pos, 1);
      if (existing.length === 1) {
        index[key] = existing[0];
      }
      return;
    }
  }
  /**
   * Populate id and name indices for a specific child type.
   */
  #buildIndex(type) {
    this.#reify();
    const byId = Array();
    const byName = {};
    for (const child of this.#children) {
      if (child instanceof type) {
        const id = child.effectiveId;
        if (id !== void 0) {
          this.#indexInsert(byId, id, child);
        }
        this.#indexInsert(byName, child.name, child);
      }
    }
    const slot = { byId, byName };
    if (!this.#indices) {
      this.#indices = /* @__PURE__ */ new Map();
    }
    this.#indices.set(type, slot);
    return slot;
  }
  /**
   * Remove a child of the model.  Clears model from indices and clears "parent" field.
   */
  #deleteChild(child) {
    if (this.#indices) {
      for (const [type, slot] of this.#indices.entries()) {
        if (child instanceof type) {
          if (child.id) {
            this.#indexDelete(slot.byId, child.id, child);
          }
          this.#indexDelete(slot.byName, child.name, child);
        }
      }
    }
    this.#onNameChanged?.(child.name, void 0);
    this.#doDisown(child);
  }
  /**
   * Add a child of the model.  Adopts the model and adds to any applicable indices.
   */
  #addChild(child) {
    if (child.parent?.children === this.#children) {
      return;
    }
    if (this.#indices) {
      for (const [type, slot] of this.#indices.entries()) {
        if (child instanceof type) {
          if (child.id) {
            this.#indexInsert(slot.byId, child.id, child);
          }
          this.#indexInsert(slot.byName, child.name, child);
        }
      }
    }
    this.#doAdopt(child);
  }
  /**
   * Recursively reroot a single child & descendents.
   *
   * Invoked when the child's parent changes.
   */
  #doReroot(child, isOwned) {
    if (!this.#position.reroot(child, isOwned) || !child.hasChildren) {
      return;
    }
    child.children.rerootAll(isOwned);
  }
  /**
   * Recursively reroot all children & descendents.
   *
   * Invoked when the owner's root changes.  Only affects reified models.
   */
  #rerootAll(isOwned) {
    for (const child of this.#children) {
      if (child instanceof ModelConstructor) {
        this.#doReroot(child, isOwned);
      }
    }
  }
  #doAdopt(child) {
    this.#position.adopt(child);
    this.#onNameChanged?.(child.name, child);
    this.#doReroot(child, true);
  }
  #doDisown(child) {
    this.#onNameChanged?.(child.name, void 0);
    if (this.#position.disown(child)) {
      this.#doReroot(child, false);
    }
  }
  #get(type, idOrName) {
    const value = this.#all(type, idOrName);
    if (Array.isArray(value)) {
      return value[0];
    }
    return value;
  }
  #all(type, idOrName) {
    const slot = this.#indices?.get(type) ?? this.#buildIndex(type);
    if (idOrName === void 0) {
      return Object.values(slot.byName).flatMap((entry) => entry);
    }
    let result;
    if (typeof idOrName === "number") {
      result = slot.byId[idOrName];
    } else {
      result = slot.byName[idOrName];
    }
    if (result === void 0) {
      return [];
    }
    if (Array.isArray(result)) {
      return result;
    }
    return [result];
  }
  #selectTypes(tags) {
    if (tags === void 0 || tags === "*") {
      return [ModelConstructor];
    }
    if (typeof tags === "string") {
      tags = [tags];
    }
    const result = Array();
    for (const tag of tags) {
      const type = ModelConstructor.types[tag];
      if (type === void 0) {
        throw new ImplementationError(`Unknown element tag "${tag}"`);
      }
      result.push(type);
    }
    return result;
  }
  #indexLookup(selector, indexName, allowedTags, except, processor) {
    for (const type of this.#selectTypes(allowedTags)) {
      let slot = this.#indices?.get(type);
      if (slot === void 0) {
        slot = this.#buildIndex(type);
      }
      const index = slot[indexName];
      const entry = index[selector];
      if (Array.isArray(entry)) {
        for (const subentry of entry) {
          if (except?.has(subentry)) {
            continue;
          }
          const result = processor(subentry);
          if (result !== void 0) {
            return result;
          }
        }
        continue;
      }
      if (entry) {
        if (except?.has(entry)) {
          continue;
        }
        const result = processor(entry);
        if (result) {
          return result;
        }
      }
    }
  }
  #indexApply(selector, allowedTags, except) {
    for (const type of this.#selectTypes(allowedTags)) {
      let index = this.#indices?.get(type)?.byName;
      if (!index) {
        index = this.#buildIndex(type).byName;
      }
      for (const key in index) {
        const entry = index[key];
        if (Array.isArray(entry)) {
          for (const subentry of entry) {
            if (except?.has(subentry)) {
              continue;
            }
            if (selector(subentry)) {
              return subentry;
            }
          }
          continue;
        }
        if (except?.has(entry)) {
          continue;
        }
        if (selector(entry)) {
          return entry;
        }
      }
    }
  }
  #select(selector, allowedTags, except) {
    this.#reify();
    if (typeof selector === "string") {
      return this.#indexLookup(selector, "byName", allowedTags, except, (model) => model);
    }
    if (typeof selector === "number") {
      return this.#indexLookup(selector, "byId", allowedTags, except, (model) => model);
    }
    return this.#indexApply(selector, allowedTags, except);
  }
  #selectAll(selector, allowedTags, except) {
    this.#reify();
    const results = Array();
    if (typeof selector === "string") {
      this.#indexLookup(selector, "byName", allowedTags, except, (model) => {
        results.push(model);
      });
    } else {
      this.#indexLookup(selector, "byId", allowedTags, except, (model) => {
        results.push(model);
      });
    }
    return results;
  }
  #updateId(child, oldId) {
    if (!this.#indices) {
      return;
    }
    for (const [type, slot] of this.#indices.entries()) {
      if (child instanceof type) {
        if (oldId !== void 0) {
          this.#indexDelete(slot.byId, oldId, child);
        }
        if (child.id !== void 0) {
          this.#indexInsert(slot.byId, child.id, child);
        }
      }
    }
  }
  #updateName(child, oldName) {
    if (this.#onNameChanged) {
      this.#onNameChanged(oldName, void 0);
      this.#onNameChanged(child.name, child);
    }
    if (!this.#indices) {
      return;
    }
    for (const [type, slot] of this.#indices.entries()) {
      if (child instanceof type) {
        if (oldName !== void 0) {
          this.#indexDelete(slot.byName, oldName, child);
        }
        if (child.name !== void 0) {
          this.#indexInsert(slot.byName, child.name, child);
        }
      }
    }
  }
  // We implement "splice" for efficiency...  The default implementation moves elements one at a time, forcing us to
  // search the array to see if it's already present each time
  #splice(index, deleteCount, ...toAdd) {
    toAdd = toAdd.map((child) => {
      child = this.#insertionFormOf(child);
      if (child instanceof ModelConstructor) {
        this.#doAdopt(child);
      }
      return child;
    });
    const result = this.#children.splice(index, deleteCount ?? 0, ...toAdd);
    return result.map((child) => {
      if (child instanceof ModelConstructor) {
        this.#doDisown(child);
      } else {
        child = ModelConstructor.create(child);
      }
      return child;
    });
  }
  #finalize() {
    for (const child of this.#proxy) {
      child.finalize();
    }
    Object.freeze(this.#children);
  }
  #validateChild(value) {
    if (value instanceof ModelConstructor) {
      return;
    }
    if (value === void 0 || value === null) {
      throw new ImplementationError(`Child cannot be ${value}`);
    }
    if (typeof value !== "object") {
      throw new ImplementationError(`Child must be an object (child is typeof ${typeof value})`);
    }
    const { tag } = value;
    if (typeof tag !== "string") {
      throw new ImplementationError(`Child must have a string tag (tag is typeof ${typeof tag})`);
    }
    if (tag[0] < "a" || tag[0] > "z" || !(camelize(tag, true) in ElementTag)) {
      throw new ImplementationError(`Child tag "${tag}" is unknown`);
    }
  }
  #proxyGet(_target, name, receiver) {
    if (typeof name === "string" && name.match(/^\d+$/)) {
      let child = this.#children[name];
      if (child && !(child instanceof ModelConstructor)) {
        child = ModelConstructor.create(child);
        this.#addChild(child);
        this.#children[name] = child;
      }
      return child;
    }
    switch (name) {
      case "get":
        return (type, idOrName) => this.#get(type, idOrName);
      case "all":
        return (type, idOrName) => this.#all(type, idOrName);
      case "select":
        return (selector, allowedTags, except) => this.#select(selector, allowedTags, except);
      case "selectAll":
        return (selector, allowedTags, except) => this.#selectAll(selector, allowedTags, except);
      case "updateId":
        return (child, oldId) => this.#updateId(child, oldId);
      case "updateName":
        return (child, oldName) => this.#updateName(child, oldName);
      case "splice":
        return (index, deleteCount, ...toAdd) => this.#splice(index, deleteCount, ...toAdd);
      case "freeze":
        return () => this.#finalize();
      case "toString":
        return () => `[Children: ${this.#children.length}]`;
      case "rerootAll":
        return (isOwned) => this.#rerootAll(isOwned);
    }
    return Reflect.get(this.#children, name, receiver);
  }
  #proxySet(name, value, receiver) {
    if (typeof name !== "string" || !name.match(/^\d+$/)) {
      switch (name) {
        case "length":
          if (value > this.#children.length) {
            return true;
          }
          break;
        case "onNameChanged":
          this.#reify();
          this.#onNameChanged = value;
          return true;
      }
      return Reflect.set(this.#children, name, value, receiver);
    }
    this.#validateChild(value);
    const existing = this.#children[name];
    if (existing !== void 0) {
      if (existing === value) {
        return true;
      }
      if (existing instanceof ModelConstructor) {
        this.#deleteChild(existing);
      }
    }
    let targetIndex = name;
    if (value.parent?.children === this.#proxy) {
      const currentIndex = this.#children.indexOf(value);
      if (currentIndex !== -1) {
        this.#children.splice(currentIndex, 1);
        if (currentIndex < targetIndex) {
          targetIndex--;
        }
      }
    } else {
      value = this.#insertionFormOf(value);
      if (value instanceof ModelConstructor) {
        this.#addChild(value);
      }
    }
    if (targetIndex > this.#children.length) {
      targetIndex = this.#children.length;
    }
    this.#children[targetIndex] = value;
    return true;
  }
  #proxyDeleteProperty(p) {
    let child;
    if (typeof p === "string" && p.match(/^\d+$/)) {
      child = this.#children[p];
    }
    delete this.#children[p];
    if (child instanceof ModelConstructor && !this.#children.includes(child)) {
      this.#deleteChild(child);
    }
    return true;
  }
}
function Children(initial, position) {
  return new ChildList(initial, position).proxy;
}
Children.installModelConstructor = (constructor) => ModelConstructor = constructor;
export {
  Children
};
//# sourceMappingURL=Children.js.map
