/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { Datasource } from "#behavior/state/managed/Datasource.js";
import { Endpoint } from "#endpoint/Endpoint.js";
import { EndpointType } from "#endpoint/type/EndpointType.js";
import { RootEndpoint } from "#endpoints/root";
import { DatasourceCache } from "#storage/client/DatasourceCache.js";
import {
  capitalize,
  Diagnostic,
  hex,
  InternalError,
  isDeepEqual,
  Lifecycle,
  Logger,
  MaybePromise,
  Mutex,
  Observable
} from "@matter/general";
import {
  AcceptedCommandList,
  AttributeList,
  ClusterRevision,
  DeviceClassification,
  FeatureMap,
  GeneratedCommandList,
  Matter
} from "@matter/model";
import { ReadScope } from "@matter/protocol";
import { Status } from "@matter/types";
import { Descriptor } from "@matter/types/clusters/descriptor";
import { ClientStructureEvents } from "./ClientStructureEvents.js";
import { PeerBehavior } from "./PeerBehavior.js";
const logger = Logger.get("ClientStructure");
const MAX_PENDING_JOBS = 100;
const DESCRIPTOR_ID = Descriptor.id;
const DEVICE_TYPE_LIST_ATTR_ID = Descriptor.attributes.deviceTypeList.id;
const SERVER_LIST_ATTR_ID = Descriptor.attributes.serverList.id;
const PARTS_LIST_ATTR_ID = Descriptor.attributes.partsList.id;
const DEVICE_TYPE_LIST_ATTR_NAME = "deviceTypeList";
const SERVER_LIST_ATTR_NAME = "serverList";
const PARTS_LIST_ATTR_NAME = "partsList";
function getStoreValue(values, id, name) {
  if (values === void 0) {
    return void 0;
  }
  return id in values ? values[id] : values[name];
}
class ClientStructure {
  #storeFactory;
  #endpoints = /* @__PURE__ */ new Map();
  #node;
  #subscribedFabricFiltered;
  #pendingChanges = /* @__PURE__ */ new Map();
  #pendingStructureEvents = Array();
  #delayedClusterEvents = new Array();
  #clustersWithDataThisInteraction = /* @__PURE__ */ new Set();
  #events;
  #changed = Observable();
  #commandFactory;
  /**
   * Optional event emitter for Matter protocol events.  Set after construction.
   */
  eventEmitter;
  constructor(node, storeFactory, options) {
    this.#node = node;
    this.#storeFactory = storeFactory;
    this.#commandFactory = options?.commandFactory;
    this.#endpoints.set(node.number, {
      endpoint: node,
      clusters: /* @__PURE__ */ new Map()
    });
    this.#events = this.#node.env.get(ClientStructureEvents);
  }
  get changed() {
    return this.#changed;
  }
  /**
   * The node this structure manages.
   */
  get node() {
    return this.#node;
  }
  /**
   * Load initial structure from cached endpoint stores.
   *
   * Each entry in {@link endpointStores} must expose an `id` (stringified endpoint number) and `knownBehaviors`
   * (iterable of stringified cluster IDs).
   */
  loadCache(endpointStores) {
    for (const store of endpointStores) {
      const id = store.id;
      const number = Number.parseInt(id);
      if (!Number.isFinite(number)) {
        continue;
      }
      const endpoint = this.#endpointFor(number);
      const knownBehaviors = [...store.knownBehaviors].map((idStr) => Number.parseInt(idStr)).filter((id2) => Number.isFinite(id2));
      const descriptorIndex = knownBehaviors.indexOf(DESCRIPTOR_ID);
      if (descriptorIndex !== -1) {
        knownBehaviors.splice(descriptorIndex, 1);
        knownBehaviors.unshift(DESCRIPTOR_ID);
      }
      for (const id2 of knownBehaviors) {
        this.#synchronizeCluster(endpoint, this.#clusterFor(endpoint, id2));
      }
    }
    const changes = this.#pendingChanges;
    this.#pendingChanges = /* @__PURE__ */ new Map();
    for (const [structure, change] of changes.entries()) {
      if (!change.install || change.erase || change.rebuild) {
        throw new InternalError(
          `Unexpected erase and/or rebuild during initialization of ${structure.endpoint}`
        );
      }
      this.#pendingChanges.delete(structure);
      this.#install(structure);
    }
    this.#emitPendingStructureEvents();
  }
  /**
   * Obtain the store for a remote cluster.
   */
  storeForRemote(endpoint, type) {
    const endpointStructure = this.#endpointFor(endpoint.number);
    const clusterStructure = this.#clusterFor(endpointStructure, type.cluster.id);
    return clusterStructure.store;
  }
  /**
   * Inject version filters into a Read or Subscribe request.
   */
  injectVersionFilters(request) {
    const scope = ReadScope(request);
    let result = request;
    for (const {
      endpoint: { number: endpointId },
      clusters
    } of this.#endpoints.values()) {
      for (const {
        id: clusterId,
        store: { version }
      } of clusters.values()) {
        if (!scope.isRelevant(endpointId, clusterId)) {
          continue;
        }
        if (version === Datasource.UNKNOWN_VERSION) {
          continue;
        }
        if (result === request) {
          result = { ...request };
        }
        if (result.dataVersionFilters === void 0) {
          result.dataVersionFilters = [];
        }
        result.dataVersionFilters.push({ path: { endpointId, clusterId }, dataVersion: version });
      }
    }
    return result;
  }
  /**
   * Update the node structure by applying attribute changes from a Matter protocol interaction.
   */
  async *mutate(request, changes) {
    this.#clustersWithDataThisInteraction.clear();
    let currentUpdates;
    const queue = new Mutex(this);
    const jobErrors = new Array();
    let pendingJobs = 0;
    const q = {
      endpointsWithData: /* @__PURE__ */ new Set(),
      enqueue: (job) => {
        pendingJobs++;
        queue.run(async () => {
          try {
            await job();
          } catch (error) {
            jobErrors.push(error);
            logger.warn("Deferred data report job failed:", error);
          } finally {
            pendingJobs--;
          }
        });
      }
    };
    const scope = ReadScope(request);
    try {
      for await (const chunk of changes) {
        const chunkData = new Array();
        for await (const change of chunk) {
          chunkData.push(change);
          switch (change.kind) {
            case "attr-value":
              currentUpdates = this.#mutateAttribute(change, scope, currentUpdates, q);
              break;
            case "event-value":
              this.#emitEvent(change, q);
              break;
            case "attr-status":
            case "event-status":
              logger.debug(
                "Received status for",
                change.kind === "attr-status" ? "attribute" : "event",
                Diagnostic.strong(Diagnostic.dict(change.path)),
                `: ${Status[change.status]}#${change.status}${change.clusterStatus !== void 0 ? `/${Status[change.clusterStatus]}#${change.clusterStatus}` : ""}`
              );
              break;
          }
          if (pendingJobs > MAX_PENDING_JOBS) {
            await queue;
          }
        }
        yield chunkData;
      }
      if (currentUpdates) {
        const toFlush = currentUpdates;
        q.enqueue(() => this.#updateCluster(toFlush));
      }
    } finally {
      await queue;
    }
    if (jobErrors.length) {
      logger.warn(`${jobErrors.length} deferred data report job(s) failed during interaction`);
    }
    for (const [endpoint, change] of this.#pendingChanges.entries()) {
      this.#pendingChanges.delete(endpoint);
      if (change.erase) {
        await this.#erase(endpoint);
        continue;
      }
      if (change.rebuild) {
        await this.#rebuild(endpoint);
      }
      if (change.install) {
        this.#install(endpoint);
      }
    }
    this.#emitPendingStructureEvents();
    await this.#emitPendingEvents();
  }
  /**
   * Apply {@link StateStream.WireChange}s to the structure.
   *
   * Values are keyed by property name (not attribute ID).
   */
  async applyWireChanges(changes) {
    this.#clustersWithDataThisInteraction.clear();
    for (const change of changes) {
      switch (change.kind) {
        case "update": {
          const endpointNumber = change.endpoint;
          const endpoint = this.#endpointFor(endpointNumber);
          const cluster = this.#clusterForBehavior(endpoint, change.behavior);
          const values = new Map(Object.entries(change.changes));
          if (typeof change.version === "number") {
            values.set(DatasourceCache.VERSION_KEY, change.version);
          }
          this.#clustersWithDataThisInteraction.add(cluster);
          this.#preserveAbsentCluster(endpoint.endpoint, cluster);
          await cluster.store.externalSet(values);
          this.#synchronizeCluster(endpoint, cluster);
          break;
        }
        case "delete": {
          const endpointNumber = change.endpoint;
          const endpoint = this.#endpoints.get(endpointNumber);
          if (endpoint) {
            this.#scheduleStructureChange(endpoint, "erase");
          }
          break;
        }
      }
    }
    for (const [endpoint, change] of this.#pendingChanges.entries()) {
      this.#pendingChanges.delete(endpoint);
      if (change.erase) {
        await this.#erase(endpoint);
        continue;
      }
      if (change.rebuild) {
        await this.#rebuild(endpoint);
      }
      if (change.install) {
        this.#install(endpoint);
      }
    }
    this.#emitPendingStructureEvents();
  }
  /**
   * Determines if the subscription is fabric filtered.
   */
  get subscribedFabricFiltered() {
    if (this.#subscribedFabricFiltered === void 0) {
      this.#subscribedFabricFiltered = true;
      try {
        const state = this.#node.state;
        const network = state?.network;
        const defaultSubscription = network?.defaultSubscription;
        if (defaultSubscription) {
          this.#subscribedFabricFiltered = ("isFabricFiltered" in defaultSubscription ? defaultSubscription.isFabricFiltered : "fabricFiltered" in defaultSubscription ? defaultSubscription.fabricFiltered : true) ?? true;
        }
        const events = this.#node.events;
        const networkEvents = events?.network;
        const changedEvent = networkEvents?.defaultSubscription$Changed;
        changedEvent?.on((newSubscription) => {
          this.#subscribedFabricFiltered = newSubscription?.isFabricFiltered ?? true;
        });
      } catch {
      }
    }
    return this.#subscribedFabricFiltered;
  }
  #mutateAttribute(change, scope, currentUpdates, q) {
    if (this.subscribedFabricFiltered !== scope.isFabricFiltered) {
      return currentUpdates;
    }
    const { endpointId, clusterId, attributeId } = change.path;
    q.endpointsWithData.add(endpointId);
    if (currentUpdates && (currentUpdates.endpointId !== endpointId || currentUpdates.clusterId !== clusterId)) {
      const toFlush = currentUpdates;
      q.enqueue(() => this.#updateCluster(toFlush));
      currentUpdates = void 0;
    }
    if (currentUpdates === void 0) {
      currentUpdates = {
        endpointId,
        clusterId,
        values: /* @__PURE__ */ new Map([[attributeId, change.value]])
      };
      if (scope.isWildcard(endpointId, clusterId)) {
        currentUpdates.values.set(DatasourceCache.VERSION_KEY, change.version);
      }
    } else {
      currentUpdates.values.set(attributeId, change.value);
    }
    return currentUpdates;
  }
  #emitEvent(occurrence, q) {
    const emitter = this.eventEmitter;
    if (!emitter) {
      return;
    }
    const { endpointId } = occurrence.path;
    const endpoint = this.#endpoints.get(endpointId);
    if (endpoint === void 0 || !endpoint.endpoint.lifecycle.isInstalled || q.endpointsWithData.has(endpointId) || this.#pendingChanges?.has(endpoint)) {
      this.#delayedClusterEvents.push(occurrence);
    } else {
      q.enqueue(() => Promise.resolve(emitter(occurrence)));
    }
  }
  /**
   * Obtain the cluster namespace for an {@link EndpointNumber} and {@link ClusterId}.
   */
  clusterFor(endpoint, cluster) {
    const ep = this.#endpointFor(endpoint);
    if (!ep) {
      return;
    }
    return this.#clusterFor(ep, cluster)?.behavior?.cluster;
  }
  /**
   * Obtain the {@link Endpoint} for a {@link EndpointNumber}.
   */
  endpointFor(endpoint) {
    return this.#endpoints.get(endpoint)?.endpoint;
  }
  /**
   * Cancel a deletion scheduled for a cluster the peer is still sending data for.
   *
   * A peer that omits a cluster from its descriptor server list but continues to report the cluster's attributes is
   * buggy, but we tolerate it by keeping the cluster — aka "Schrödinger's cluster".
   */
  #preserveAbsentCluster(endpoint, cluster) {
    if (!cluster.pendingDelete) {
      return;
    }
    logger.info(
      `Cluster 0x${hex.fixed(cluster.id, 8)} on ${endpoint} is absent from descriptor server list but peer sent attribute data for it; keeping cluster`
    );
    delete cluster.pendingDelete;
  }
  /**
   * Apply new attribute values for a specific endpoint / cluster.
   *
   * This is invoked in a batch when we've collected all sequential values for the current endpoint/cluster.
   */
  async #updateCluster(attrs) {
    const endpoint = this.#endpointFor(attrs.endpointId);
    const cluster = this.#clusterFor(endpoint, attrs.clusterId);
    this.#clustersWithDataThisInteraction.add(cluster);
    this.#preserveAbsentCluster(endpoint.endpoint, cluster);
    if (cluster.behavior && attrs.values.has(FeatureMap.id)) {
      if (!isDeepEqual(cluster.features, attrs.values.get(FeatureMap.id))) {
        cluster.behavior = void 0;
      }
    }
    if (cluster.behavior && attrs.values.has(AttributeList.id)) {
      const attributeList = attrs.values.get(AttributeList.id);
      if (Array.isArray(attributeList) && attributeList.length && !isDeepEqual(
        cluster.attributes,
        [...attributeList].sort((a, b) => a - b)
      )) {
        cluster.behavior = void 0;
      }
    }
    if (cluster.behavior && attrs.values.has(AcceptedCommandList.id)) {
      const acceptedCommands = attrs.values.get(AcceptedCommandList.id);
      if (Array.isArray(acceptedCommands) && !isDeepEqual(
        cluster.commands,
        [...acceptedCommands].sort((a, b) => a - b)
      )) {
        cluster.behavior = void 0;
      }
    }
    await cluster.store.externalSet(attrs.values);
    this.#synchronizeCluster(endpoint, cluster);
  }
  /**
   * If enough attributes are present, installs a behavior on an endpoint.
   *
   * If the cluster is Descriptor, performs additional {@link Endpoint} configuration such as installing parts and
   * device types.
   *
   * Invoked once we've loaded all attributes in an interaction.
   */
  #synchronizeCluster(structure, cluster) {
    const { endpoint } = structure;
    if (cluster.behavior === void 0) {
      const values = cluster.store.currentValues;
      if (values) {
        const clusterRevision = getStoreValue(values, ClusterRevision.id, "clusterRevision");
        const features = getStoreValue(values, FeatureMap.id, "featureMap");
        const attributeList = getStoreValue(values, AttributeList.id, "attributeList");
        const commandList = getStoreValue(values, AcceptedCommandList.id, "acceptedCommandList");
        const generatedCommandList = getStoreValue(values, GeneratedCommandList.id, "generatedCommandList");
        if (typeof clusterRevision === "number") {
          cluster.revision = clusterRevision;
        }
        if (typeof features === "object" && features !== null && !Array.isArray(features)) {
          cluster.features = features;
        }
        if (Array.isArray(attributeList) && attributeList.length) {
          cluster.attributes = attributeList.filter((attr) => typeof attr === "number").sort(
            (a, b) => a - b
          );
        } else {
          const received = Object.keys(values).map(Number).filter((id) => Number.isInteger(id) && id >= 0);
          if (received.length) {
            cluster.attributes = received.sort((a, b) => a - b);
          }
        }
        if (Array.isArray(commandList)) {
          cluster.commands = commandList.filter((cmd) => typeof cmd === "number").sort(
            (a, b) => a - b
          );
        }
        if (Array.isArray(generatedCommandList)) {
          cluster.generatedCommands = generatedCommandList.filter((cmd) => typeof cmd === "number").sort((a, b) => a - b);
        }
      }
      if (
        // All global attributes have fallbacks, so we can't wait until we're sure we have them all.  Instead,
        // wait until we are sure there is something useful.  We therefore rely on unspecified behavior that all
        // attributes travel consecutively to ensure we initialize fully as we have no other choice
        cluster.attributes?.length || cluster.commands?.length || cluster.generatedCommands?.length
      ) {
        const shape = cluster;
        if (this.#commandFactory) {
          shape.commandFactory = this.#commandFactory;
        }
        const behaviorType = PeerBehavior(shape);
        if (endpoint.lifecycle.isInstalled) {
          cluster.pendingBehavior = behaviorType;
          this.#preserveAbsentCluster(endpoint, cluster);
          this.#scheduleStructureChange(
            structure,
            endpoint.behaviors.supported[behaviorType.id] ? "rebuild" : "install"
          );
        } else {
          cluster.behavior = behaviorType;
          endpoint.behaviors.inject(behaviorType);
        }
      }
    }
    if (cluster.id === Descriptor.id) {
      let attrs;
      if (cluster.behavior && endpoint.behaviors.isActive(cluster.behavior.id)) {
        attrs = endpoint.stateOf(cluster.behavior);
      } else {
        attrs = cluster.store.currentValues ?? {};
      }
      this.#synchronizeDescriptor(structure, attrs);
    }
  }
  #synchronizeDescriptor(structure, attrs) {
    const { endpoint } = structure;
    const deviceTypeList = getStoreValue(attrs, DEVICE_TYPE_LIST_ATTR_ID, DEVICE_TYPE_LIST_ATTR_NAME);
    if (Array.isArray(deviceTypeList)) {
      const endpointType = endpoint.type;
      for (const dt of deviceTypeList) {
        if (typeof dt?.deviceType !== "number") {
          continue;
        }
        let isApp = false;
        const model = Matter.deviceTypes(dt.deviceType);
        if (model !== void 0) {
          isApp = DeviceClassification.isApplication(model.classification);
        }
        if (!endpoint.number && endpointType.deviceType !== RootEndpoint.deviceType) {
          endpointType.deviceRevision = dt.revision;
          break;
        }
        if (endpointType.deviceType !== void 0 && !isApp) {
          continue;
        }
        endpointType.deviceType = dt.deviceType;
        endpointType.deviceRevision = dt.revision;
        endpointType.deviceClass = model?.classification ?? DeviceClassification.Simple;
        endpointType.name = model?.name ?? `Unknown#${hex.word(dt.deviceType)}`;
        if (isApp) {
          break;
        }
      }
    }
    const serverList = getStoreValue(attrs, SERVER_LIST_ATTR_ID, SERVER_LIST_ATTR_NAME);
    if (Array.isArray(serverList)) {
      const currentlySupported = new Set(
        Object.values(endpoint.behaviors.supported).map((type) => type.cluster?.id).filter((id) => id !== void 0)
      );
      for (const cluster of serverList) {
        if (typeof cluster === "number") {
          this.#clusterFor(structure, cluster);
          currentlySupported.delete(cluster);
        }
      }
      if (currentlySupported.size) {
        let anyPendingDelete = false;
        for (const id of currentlySupported) {
          const clusterStructure = this.#clusterFor(structure, id);
          if (!clusterStructure.pendingBehavior && !this.#clustersWithDataThisInteraction.has(clusterStructure)) {
            clusterStructure.pendingDelete = true;
            anyPendingDelete = true;
          }
        }
        if (anyPendingDelete) {
          this.#scheduleStructureChange(structure, "rebuild");
        }
      }
    }
    const partsList = getStoreValue(attrs, PARTS_LIST_ATTR_ID, PARTS_LIST_ATTR_NAME);
    if (!Array.isArray(partsList)) {
      return;
    }
    for (const partNo of partsList) {
      if (typeof partNo !== "number") {
        continue;
      }
      const part = this.#endpointFor(partNo);
      let isAlreadyDescendant = false;
      for (let owner = this.#ownerOf(part); owner; owner = this.#ownerOf(owner)) {
        if (owner === structure) {
          isAlreadyDescendant = true;
          break;
        }
      }
      if (isAlreadyDescendant) {
        continue;
      }
      part.pendingOwner = structure;
      this.#scheduleStructureChange(part, "install");
    }
    if (endpoint.maybeNumber === 0) {
      const numbersUsed = new Set(partsList);
      for (const descendent of endpoint.endpoints) {
        if (!descendent.maybeNumber) {
          continue;
        }
        if (!numbersUsed.has(descendent.number)) {
          const endpoint2 = this.#endpoints.get(descendent.number);
          if (endpoint2) {
            this.#scheduleStructureChange(endpoint2, "erase");
          }
        }
      }
    }
  }
  #endpointFor(number) {
    let endpoint = this.#endpoints.get(number);
    if (endpoint) {
      return endpoint;
    }
    endpoint = {
      endpoint: new Endpoint({
        id: `ep${number}`,
        number,
        type: EndpointType({
          name: "Unknown",
          deviceType: EndpointType.UNKNOWN_DEVICE_TYPE,
          deviceRevision: EndpointType.UNKNOWN_DEVICE_REVISION
        })
      }),
      clusters: /* @__PURE__ */ new Map()
    };
    this.#endpoints.set(number, endpoint);
    return endpoint;
  }
  #clusterFor(endpoint, id) {
    let cluster = endpoint.clusters.get(id);
    if (cluster) {
      return cluster;
    }
    cluster = {
      kind: "discovered",
      id,
      store: this.#storeFactory(endpoint.endpoint, id.toString(), "id"),
      behavior: void 0,
      pendingBehavior: void 0,
      pendingDelete: void 0
    };
    endpoint.clusters.set(id, cluster);
    return cluster;
  }
  /**
   * Look up or create a cluster structure for a behavior identified by name (for {@link StateStream.WireChange}s).
   *
   * If the behavior name matches a known cluster by camelized name, we map to the cluster ID. Otherwise this creates
   * a name-keyed cluster entry.
   */
  #clusterForBehavior(endpoint, behaviorId) {
    const supported = endpoint.endpoint.behaviors.supported[behaviorId];
    if (supported?.cluster) {
      return this.#clusterFor(endpoint, supported.cluster.id);
    }
    for (const cluster2 of endpoint.clusters.values()) {
      if (cluster2.behavior?.id === behaviorId) {
        return cluster2;
      }
    }
    const numericId = Number.parseInt(behaviorId);
    if (Number.isFinite(numericId)) {
      return this.#clusterFor(endpoint, numericId);
    }
    const clusterModel = Matter.clusters(capitalize(behaviorId));
    if (clusterModel) {
      return this.#clusterFor(endpoint, clusterModel.id);
    }
    const existing = endpoint.clusters.get(behaviorId);
    if (existing) {
      return existing;
    }
    const cluster = {
      kind: "discovered",
      id: 0,
      store: this.#storeFactory(endpoint.endpoint, behaviorId, "name"),
      behavior: void 0,
      pendingBehavior: void 0,
      pendingDelete: void 0
    };
    endpoint.clusters.set(behaviorId, cluster);
    return cluster;
  }
  #ownerOf(endpoint) {
    if (endpoint.pendingOwner) {
      return endpoint.pendingOwner;
    }
    if (endpoint.endpoint.number === 0) {
      return;
    }
    const ownerNumber = endpoint.endpoint.owner?.maybeNumber;
    if (ownerNumber !== void 0) {
      return this.#endpointFor(ownerNumber);
    }
  }
  /**
   * Erase an endpoint that disappeared from the peer.
   */
  async #erase(structure) {
    const { endpoint } = structure;
    logger.debug(
      "Removing endpoint",
      Diagnostic.strong(endpoint.toString()),
      "because it is no longer present on the peer"
    );
    this.#endpoints.delete(endpoint.number);
    if (endpoint.construction.status === Lifecycle.Status.Destroyed) {
      return;
    }
    try {
      await endpoint.delete();
    } catch (e) {
      logger.warn(`Error erasing peer endpoint ${endpoint}:`, e);
    }
  }
  /**
   * Replace clusters after activation because fixed global attributes have changed.
   *
   * Currently, we apply granular updates to clusters.  This will possibly result in subtle errors if peers change in
   * incompatible ways, but the backings are designed to be fairly resilient to this.  This is simpler for API users
   * to deal with in the common case where they can just ignore. If it becomes problematic, we can revert to replacing
   * entire endpoints or behaviors when there are structural changes.
   */
  async #rebuild(structure) {
    const { endpoint, clusters } = structure;
    for (const cluster of clusters.values()) {
      const { behavior, pendingBehavior, pendingDelete } = cluster;
      if (pendingDelete) {
        if (!behavior) {
          continue;
        }
        await endpoint.behaviors.drop(behavior.id);
        try {
          await MaybePromise.then(
            cluster.store.erase?.()
          );
        } catch (e) {
          logger.warn("Error clearing cluster storage:", e);
        }
        this.#pendingStructureEvents.push({
          kind: "cluster",
          endpoint: structure,
          cluster,
          subkind: "delete"
        });
        continue;
      }
      if (!pendingBehavior) {
        continue;
      }
      const subkind = pendingBehavior.id in endpoint.behaviors.supported ? "replace" : "add";
      endpoint.behaviors.inject(pendingBehavior);
      cluster.behavior = pendingBehavior;
      delete cluster.pendingBehavior;
      this.#pendingStructureEvents.push({
        kind: "cluster",
        subkind,
        endpoint: structure,
        cluster
      });
    }
  }
  /**
   * Install the endpoint and/or new behaviors.
   */
  #install(structure) {
    const { endpoint, pendingOwner, clusters } = structure;
    if (pendingOwner) {
      endpoint.owner = pendingOwner.endpoint;
      structure.pendingOwner = void 0;
      this.#pendingStructureEvents.push({ kind: "endpoint", endpoint: structure });
    }
    for (const cluster of clusters.values()) {
      const { pendingBehavior } = cluster;
      if (!pendingBehavior || endpoint.behaviors.supported[pendingBehavior.id]) {
        continue;
      }
      endpoint.behaviors.inject(pendingBehavior);
      cluster.behavior = pendingBehavior;
      cluster.pendingBehavior = void 0;
      if (!pendingOwner) {
        this.#pendingStructureEvents.push({
          kind: "cluster",
          subkind: "add",
          endpoint: structure,
          cluster
        });
      }
    }
  }
  /**
   * Queue a structural change for processing once a read response is fully processed.
   */
  #scheduleStructureChange(endpoint, kind) {
    const pending = this.#pendingChanges.get(endpoint);
    if (pending) {
      pending[kind] = true;
    } else {
      this.#pendingChanges.set(endpoint, { [kind]: true });
    }
  }
  /**
   * Emit pending events.
   *
   * We do this after all structural updates are complete so that listeners can expect composed parts and dependent
   * behaviors to be installed.
   */
  #emitPendingStructureEvents() {
    const structureEvents = this.#pendingStructureEvents;
    this.#pendingStructureEvents = [];
    for (const event of structureEvents) {
      switch (event.kind) {
        case "endpoint": {
          const {
            endpoint: { endpoint, clusters }
          } = event;
          this.#events.emitEndpoint(endpoint);
          for (const { behavior } of clusters.values()) {
            if (behavior) {
              this.#events.emitCluster(endpoint, behavior);
            }
          }
          break;
        }
        case "cluster": {
          const {
            endpoint: { endpoint },
            cluster: { behavior }
          } = event;
          if (!behavior) {
            break;
          }
          switch (event.subkind) {
            case "add":
              this.#events.emitCluster(endpoint, behavior);
              break;
            case "delete":
              this.#events.emitClusterDeleted(endpoint, behavior);
              break;
            case "replace":
              this.#events.emitClusterReplaced(endpoint, behavior);
          }
          break;
        }
      }
    }
    this.#changed.emit();
  }
  async #emitPendingEvents() {
    if (!this.eventEmitter) {
      return;
    }
    const clusterEvents = this.#delayedClusterEvents;
    this.#delayedClusterEvents = [];
    for (const occurrence of clusterEvents) {
      await this.eventEmitter(occurrence);
    }
  }
}
export {
  ClientStructure
};
//# sourceMappingURL=ClientStructure.js.map
