/**
 * @license
 * Copyright 2022-2025 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { Abort, deepCopy, Gate, Millis } from "#general";
import { DatatypeModel, FieldElement } from "#model";
import { Node } from "#node/Node.js";
import { ChangeNotificationService } from "./ChangeNotificationService.js";
const DEFAULT_COALESCE_INTERVAL = Millis(250);
function StateStream(node, { nodes: nodeFilter, clusters: clusterFilter, versions, coalesceInterval, abort } = {}) {
  const changeService = node.env.get(ChangeNotificationService);
  const gate = new Gate();
  coalesceInterval ??= DEFAULT_COALESCE_INTERVAL;
  let coalescenceTimer;
  const nodes = /* @__PURE__ */ new Map();
  let queueHead;
  let queueTail;
  installInitialVersions();
  const filter = generateFilter();
  return stream();
  async function* stream() {
    try {
      changeService.change.on(changeListener);
      for (const n of [node, ...node.peers]) {
        for (const endpoint of n.endpoints) {
          for (const behavior of Object.values(endpoint.behaviors.supported)) {
            if (filter && !filter(endpoint.id, behavior.id)) {
              continue;
            }
            enqueue({ node: n, endpoint, behavior });
          }
        }
      }
      while (true) {
        await Abort.race(abort, gate);
        if (Abort.is(abort)) {
          break;
        }
        while (queueHead) {
          if (Abort.is(abort)) {
            break;
          }
          const { node: node2, endpoint, behavior } = queueHead;
          dequeue(queueHead);
          if (!behavior) {
            yield { kind: "delete", node: node2, endpoint };
            continue;
          }
          const state = stateOfBehavior(node2.id, endpoint.number, behavior.id);
          state.queueEntry = void 0;
          let changes = endpoint.stateOf(behavior);
          if (state.dirty) {
            changes = Object.fromEntries(
              [...state.dirty].map((name) => [name, changes[name]])
            );
            state.dirty = void 0;
          } else {
            changes = deepCopy(changes);
          }
          yield {
            kind: "update",
            node: node2,
            endpoint,
            behavior,
            changes,
            version: state.version = endpoint.behaviors.versionOf(behavior)
          };
        }
        gate.close();
      }
    } finally {
      changeService.change.off(changeListener);
      coalescenceTimer?.stop();
    }
  }
  function changeListener(change) {
    switch (change.kind) {
      case "update":
        enqueueUpdate(change);
        break;
      case "delete":
        enqueueDelete(change);
        break;
    }
  }
  function stateOfEndpoint(node2, endpoint) {
    let nodeState = nodes.get(node2);
    if (nodeState === void 0) {
      nodes.set(node2, nodeState = /* @__PURE__ */ new Map());
    }
    let endpointState = nodeState.get(endpoint);
    if (endpointState === void 0) {
      nodeState.set(endpoint, endpointState = { behaviors: /* @__PURE__ */ new Map() });
    }
    return endpointState;
  }
  function stateOfBehavior(node2, endpoint, behavior) {
    const endpointState = stateOfEndpoint(node2, endpoint);
    let behaviorState = endpointState.behaviors.get(behavior);
    if (behaviorState === void 0) {
      endpointState.behaviors.set(behavior, behaviorState = {});
    }
    return behaviorState;
  }
  function installInitialVersions() {
    if (!versions) {
      return;
    }
    for (const { node: node2, endpoint, cluster, version } of versions) {
      stateOfBehavior(node2, endpoint, cluster).version = version;
    }
  }
  function generateFilter() {
    if (!nodeFilter && !clusterFilter) {
      return;
    }
    const whitelistedNodes = nodeFilter ? new Set(nodeFilter) : void 0;
    const whitelistedBehaviors = clusterFilter ? new Set(clusterFilter) : void 0;
    if (whitelistedNodes) {
      if (whitelistedBehaviors) {
        return (node2, behavior) => whitelistedNodes.has(node2) && (!behavior || whitelistedBehaviors.has(behavior));
      }
      return (node2) => whitelistedNodes.has(node2);
    }
    if (whitelistedBehaviors) {
      return (_node, behavior) => !behavior || whitelistedBehaviors.has(behavior);
    }
  }
  function enqueueUpdate(change) {
    const { endpoint, behavior } = change;
    const node2 = endpoint.env.get(Node);
    if (filter && !filter(node2.id, behavior.id)) {
      return;
    }
    const behaviorState = stateOfBehavior(node2.id, endpoint.number, behavior.id);
    if (behaviorState.version === change.version) {
      return;
    }
    if (behaviorState.queueEntry) {
      if (change.properties) {
        if (behaviorState.dirty) {
          for (const prop of change.properties) {
            behaviorState.dirty.add(prop);
          }
        }
      } else {
        behaviorState.dirty = void 0;
      }
      behaviorState.version = change.version;
      return;
    }
    behaviorState.dirty = change.properties ? new Set(change.properties) : void 0;
    behaviorState.queueEntry = { endpoint, node: node2, behavior };
    enqueue(behaviorState.queueEntry);
  }
  function enqueueDelete(change) {
    const { endpoint } = change;
    const node2 = endpoint.env.get(Node);
    if (filter && !filter(node2.id)) {
      return;
    }
    const endpointState = stateOfEndpoint(node2.id, endpoint.number);
    for (const { queueEntry } of endpointState.behaviors.values()) {
      if (queueEntry) {
        dequeue(queueEntry);
      }
    }
    if (endpoint === node2) {
      nodes.delete(node2.id);
    } else {
      nodes.get(node2.id)?.delete(endpoint.number);
    }
    enqueue({ endpoint, node: node2 });
  }
  function enqueue(entry) {
    if (queueTail) {
      queueTail.next = entry;
      entry.prev = queueTail;
      queueTail = entry;
    } else {
      queueHead = queueTail = entry;
    }
    gate.open();
  }
  function dequeue(entry) {
    if (queueHead === entry) {
      queueHead = entry.next;
    }
    if (queueTail === entry) {
      queueTail = entry.prev;
    }
    if (entry.prev) {
      entry.prev.next = entry.next;
    }
    if (entry.next) {
      entry.next.prev = entry.prev;
    }
  }
}
((StateStream2) => {
  function WireChange(change) {
    switch (change.kind) {
      case "update":
        return {
          kind: "update",
          node: change.node.id,
          endpoint: change.endpoint.number,
          version: change.version,
          behavior: change.behavior.id,
          changes: change.changes
        };
      case "delete":
        return {
          kind: "delete",
          node: change.node.id,
          endpoint: change.endpoint.number
        };
    }
  }
  StateStream2.WireChange = WireChange;
  StateStream2.OptionsSchema = new DatatypeModel(
    { name: "ChangeOptions", type: "struct", quality: "X" },
    FieldElement({ name: "nodes", type: "list" }, FieldElement({ name: "entry", type: "string" })),
    FieldElement({ name: "clusters", type: "list" }, FieldElement({ name: "entry", type: "string" })),
    FieldElement(
      { name: "versions", type: "list" },
      FieldElement(
        { name: "entry", type: "struct" },
        FieldElement({ name: "node", type: "string", conformance: "M" }),
        FieldElement({ name: "endpoint", type: "endpoint-no", conformance: "M" }),
        FieldElement({ name: "cluster", type: "string", conformance: "M" }),
        FieldElement({ name: "version", type: "data-ver", conformance: "M" })
      )
    ),
    FieldElement({ name: "coalesceInterval", type: "duration" })
  );
  StateStream2.WireUpdateSchema = new DatatypeModel(
    { name: "UpdateNotification", type: "struct" },
    FieldElement({ name: "node", type: "string" }),
    FieldElement({ name: "endpoint", type: "endpoint-no" }),
    FieldElement({ name: "version", type: "data-ver" }),
    FieldElement({ name: "cluster", type: "string" }),
    FieldElement({ name: "changes", type: "any" })
  );
  StateStream2.WireDeleteSchema = new DatatypeModel(
    { name: "DeleteNotification", type: "struct" },
    FieldElement({ name: "node", type: "string" }),
    FieldElement({ name: "endpoint", type: "endpoint-no" })
  );
})(StateStream || (StateStream = {}));
export {
  DEFAULT_COALESCE_INTERVAL,
  StateStream
};
//# sourceMappingURL=StateStream.js.map
