/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import {
  deepCopy,
  ImplementationError,
  InternalError,
  isDeepEqual,
  Lifetime,
  Logger,
  MaybePromise,
  Transaction
} from "@matter/general";
import { ExpiredReferenceError, hasRemoteActor, Val } from "@matter/protocol";
import { GlobalConfig, LocalConfig } from "../../supervision/SupervisionConfig.js";
const logger = Logger.get("Datasource");
const FEATURES_KEY = "__features__";
const localInteractionBeginEmitted = /* @__PURE__ */ new WeakSet();
const viewTx = Transaction.open("offline-view", Lifetime.process, "ro");
function Datasource(options) {
  return new DatasourceImpl(options);
}
((Datasource2) => {
  Datasource2.UNKNOWN_VERSION = -1;
})(Datasource || (Datasource = {}));
class DatasourceImpl {
  // From Datasource.Options
  type;
  supervisor;
  location;
  entropy;
  store;
  defaults;
  owner;
  onChange;
  // Computed state
  primaryKey;
  version;
  manageVersion;
  events;
  sessions;
  featuresKey;
  featuresKeyPersisted;
  persistentFields;
  supervisionConfig;
  observedInteractions;
  #values;
  #changedEventIndex;
  #readOnlyView;
  constructor(options) {
    this.type = options.type;
    this.supervisor = options.supervisor;
    this.location = options.location;
    this.entropy = options.entropy;
    this.store = options.store;
    this.defaults = options.defaults;
    this.owner = options.owner;
    this.onChange = options.onChange;
    this.primaryKey = options.primaryKey === "id" ? "id" : "name";
    this.events = options.events ?? {};
    const values = new options.type();
    let storedValues = options.store?.initialValues;
    if (options.supervisor.featureMap.children.length) {
      this.featuresKey = [...options.supervisor.supportedFeatures].join(",");
      const storedFeaturesKey = storedValues?.[FEATURES_KEY];
      if (storedFeaturesKey !== void 0) {
        if (storedFeaturesKey !== this.featuresKey) {
          logger.warn(
            `Ignoring persisted values for ${options.location.path} because features changed from "${storedFeaturesKey}" to "${this.featuresKey}"`
          );
          storedValues = void 0;
        } else {
          this.featuresKeyPersisted = true;
        }
      }
    }
    const initialValues = {
      ...options.defaults,
      ...storedValues
    };
    if (FEATURES_KEY in initialValues) {
      delete initialValues[FEATURES_KEY];
    }
    for (const key in initialValues) {
      values[key] = initialValues[key];
    }
    this.#values = values;
    Object.freeze(options.location);
    this.version = options.entropy.randomUint32;
    this.manageVersion = true;
    this.persistentFields = options.supervisor.persistentKeys(options.primaryKey);
    this.#configureExternalChanges();
  }
  // -- Datasource interface --
  toString() {
    return this.location.path.toString();
  }
  reference(session) {
    let ref = this.sessions?.get(session);
    if (!ref) {
      ref = createReference(this, this, session);
    }
    return ref.managed;
  }
  close() {
    if (this.observedInteractions) {
      for (const observable of this.observedInteractions) {
        observable.off(this.interactionObserver);
      }
      this.observedInteractions = void 0;
    }
    const store = this.store;
    if (store?.consumer === this) {
      store.consumer = void 0;
    }
  }
  validate(session, values) {
    const validate = this.supervisor.validate;
    if (!validate) {
      return;
    }
    validate(values ?? this.#values, session, {
      path: this.location.path,
      config: this.supervisionConfig
    });
  }
  get view() {
    if (!this.#readOnlyView) {
      const session = {
        transaction: viewTx,
        supervisionMode: "global"
      };
      this.#readOnlyView = createReference(this, this, session).managed;
    }
    return this.#readOnlyView;
  }
  // -- Internal methods (used by RootReference) --
  get values() {
    return this.#values;
  }
  set values(newValues) {
    const oldValues = this.#values;
    this.#values = newValues;
    if (this.sessions) {
      for (const context of this.sessions.values()) {
        context.onChange(oldValues);
      }
    }
  }
  interactionObserver = (session) => {
    if (session?.interactionComplete) {
      session.interactionComplete.off(this.interactionObserver);
      this.observedInteractions?.delete(session.interactionComplete);
    }
    if (hasRemoteActor(session)) {
      session.interactionStarted = false;
    } else if (session?.interactionComplete) {
      localInteractionBeginEmitted.delete(session.interactionComplete);
    }
    const location = this.location;
    function handleObserverError(error) {
      logger.warn(`Error in ${location.path} observer:`, error);
    }
    if (this.events?.interactionEnd?.isObserved) {
      try {
        const result = this.events?.interactionEnd?.emit(session);
        if (MaybePromise.is(result)) {
          return MaybePromise.then(result, void 0, handleObserverError);
        }
      } catch (e) {
        handleObserverError(e);
      }
    }
  };
  changedEventFor(key) {
    if (this.#changedEventIndex === void 0) {
      this.#changedEventIndex = /* @__PURE__ */ new Map();
    } else if (this.#changedEventIndex.has(key)) {
      return this.#changedEventIndex.get(key);
    }
    const id = Number.parseInt(key);
    let event;
    if (!Number.isFinite(id)) {
      event = this.events[`${key}$Changed`];
    } else {
      const field = this.supervisor.schema.member(id);
      if (field !== void 0) {
        event = this.events[`${field.propertyName}$Changed`];
      }
    }
    this.#changedEventIndex.set(key, event);
    return event;
  }
  // -- External change handling --
  #configureExternalChanges() {
    const { store } = this;
    if (!store || !("externalSet" in store)) {
      return;
    }
    const externalStore = store;
    this.version = externalStore.version;
    this.manageVersion = false;
    externalStore.consumer = this;
  }
  // -- Datasource.ExternallyMutableStore.Consumer --
  async integrateExternalChange(potentialChanges) {
    const { values } = this;
    const externalStore = this.store;
    let changes;
    let oldValues;
    for (const [key, newValue] of potentialChanges) {
      const name = String(key);
      if (isDeepEqual(values[name], newValue)) {
        continue;
      }
      if (changes === void 0) {
        changes = /* @__PURE__ */ new Map([[name, newValue]]);
        oldValues = /* @__PURE__ */ new Map([[name, values[name]]]);
      } else {
        changes.set(name, newValue);
        oldValues.set(name, values[name]);
      }
    }
    this.version = externalStore.version;
    if (!changes) {
      return;
    }
    this.values = {
      ...this.values,
      ...Object.fromEntries(changes)
    };
    const changedProps = Array.from(changes.keys());
    const onChangePromise = this.onChange?.(changedProps);
    const iterator = changedProps[Symbol.iterator]();
    const self = this;
    if (onChangePromise) {
      return onChangePromise.then(emitChanged);
    }
    return emitChanged();
    function emitChanged() {
      while (true) {
        const n = iterator.next();
        if (n.done) {
          return;
        }
        const name = n.value;
        const event = self.changedEventFor(name);
        if (!event?.isObserved) {
          continue;
        }
        const result = event.emit(changes.get(name), oldValues.get(name));
        if (MaybePromise.is(result)) {
          return Promise.resolve(result).then(emitChanged);
        }
      }
    }
  }
  readValues(keys) {
    const result = {};
    for (const key of keys) {
      if (key in this.#values) {
        result[key] = this.#values[key];
      }
    }
    return result;
  }
  releaseValues() {
    const { values } = this;
    this.values = {};
    return values;
  }
}
class RootReference {
  primaryKey;
  subrefs;
  owner;
  supervisionConfig;
  #values;
  #precommitValues;
  #changes;
  #expired = false;
  #internals;
  #session;
  #resource;
  #fields;
  #context;
  constructor(resource, internals, session) {
    this.#resource = resource;
    this.#internals = internals;
    this.#session = session;
    this.#values = internals.values;
    this.#fields = internals.supervisor.memberNames;
    this.primaryKey = internals.primaryKey;
    const transaction = session.transaction;
    void transaction.onShared(() => {
      if (this.#values !== this.#internals.values) {
        try {
          this.rollback();
        } catch (e) {
          logger.warn(
            `Error resetting reference to ${this.#internals.location.path} after reset of transaction ${transaction.via}:`,
            e
          );
        }
      }
    });
    if (!internals.supervisionConfig) {
      internals.supervisionConfig = new GlobalConfig();
    }
    if (session.supervisionMode === "global") {
      this.supervisionConfig = internals.supervisionConfig;
    } else {
      this.supervisionConfig = new LocalConfig(internals.supervisionConfig);
    }
  }
  /**
   * Complete initialization after the managed value is created.  Must be called immediately after construction.
   */
  initialize() {
    const internals = this.#internals;
    const session = this.#session;
    const transaction = session.transaction;
    this.#context = {
      managed: internals.supervisor.manage(this, session),
      onChange: (oldValues) => {
        if (this.#values === oldValues) {
          this.#values = this.#internals.values;
          this.#refreshSubrefs();
        }
      }
    };
    if (transaction.isolation !== "snapshot") {
      if (!internals.sessions) {
        internals.sessions = /* @__PURE__ */ new Map();
      }
      internals.sessions.set(session, this.#context);
    }
    void transaction.onClose(() => {
      try {
        this.#internals.sessions?.delete(this.#session);
        this.#expired = true;
        this.#refreshSubrefs();
      } catch (e) {
        logger.warn(
          `Error detaching reference to ${this.#internals.location.path} from closed transaction ${transaction.via}:`,
          e
        );
      }
    });
    return this.#context;
  }
  toString() {
    return `ref<${this.#resource}>`;
  }
  // -- ValReference implementation --
  get original() {
    return this.#internals.values;
  }
  get value() {
    if (this.#expired) {
      throw new ExpiredReferenceError(this.location);
    }
    return this.#values;
  }
  set value(_value) {
    throw new InternalError(`Cannot set root reference for ${this.#internals.supervisor.schema.name}`);
  }
  get expired() {
    return this.#expired;
  }
  get location() {
    return this.#internals.location;
  }
  set location(_loc) {
    throw new ImplementationError("Root reference location is immutable");
  }
  get rootOwner() {
    return this.#internals.owner;
  }
  change(mutator) {
    if (this.#expired) {
      throw new ExpiredReferenceError(this.location);
    }
    this.#startWrite();
    this.#session.transaction.beginSync();
    if (this.#values === this.#internals.values) {
      const old = this.#values;
      this.#values = new this.#internals.type();
      const properties = this.#values[Val.properties] ? this.#values[Val.properties](this.rootOwner, this.#session) : void 0;
      const keys = new Set(this.#fields);
      for (const key of Object.keys(old)) {
        keys.add(key);
      }
      for (const key of keys) {
        if (properties && key in properties) {
          continue;
        }
        this.#values[key] = old[key];
      }
      this.#refreshSubrefs();
    }
    mutator();
    this.#refreshSubrefs();
  }
  refresh() {
    throw new InternalError(`Cannot refresh root reference for ${this.#internals.supervisor.schema.name}`);
  }
  // -- Transaction.Participant implementation --
  /**
   * For pre-commit we trigger "fieldName$Changing" events for any fields that have changed since the previous
   * pre-commit cycle.
   *
   * Tracking data here is relatively expensive so we limit to events with registered observers.
   */
  preCommit() {
    const { events } = this.#internals;
    if (!events) {
      return false;
    }
    let mayHaveMutated = false;
    const keyIterator = Object.keys(this.#values)[Symbol.iterator]();
    const nextKey = () => {
      while (true) {
        const n = keyIterator.next();
        if (n.done) {
          return mayHaveMutated;
        }
        const name = n.value;
        const event = events?.[`${name}$Changing`];
        if (!event?.isObserved) {
          continue;
        }
        const change = this.#computePreCommitChange(name);
        if (change) {
          mayHaveMutated = true;
          const result = event.emit(change.newValue, change.oldValue, this.#session);
          if (MaybePromise.is(result)) {
            return result.then(nextKey);
          }
        }
      }
    };
    return nextKey();
  }
  /**
   * For commit phase one we pass values to the persistence layer if present.
   */
  commit1() {
    this.#computePostCommitChanges();
    const persistent = this.#changes?.persistent;
    if (!persistent) {
      return;
    }
    if (this.#internals.featuresKey !== void 0 && !this.#internals.featuresKeyPersisted) {
      persistent[FEATURES_KEY] = this.#internals.featuresKey;
      this.#internals.featuresKeyPersisted = true;
    }
    return this.#internals.store?.set(this.#session.transaction, persistent);
  }
  /**
   * For commit phase two we make the working values canonical and notify listeners.
   */
  commit2() {
    if (!this.#changes) {
      return;
    }
    this.#internals.values = this.#values;
  }
  /**
   * Post-commit logic.  Emit "changed" events.  Observers may be synchronous or asynchronous.
   */
  postCommit() {
    if (!this.#changes) {
      return;
    }
    const iterator = this.#changes.notifications[Symbol.iterator]();
    function emitChanged() {
      while (true) {
        const n = iterator.next();
        if (n.done) {
          return;
        }
        const { event, params } = n.value;
        const result = event.emit(...params);
        if (MaybePromise.is(result)) {
          return Promise.resolve(result).then(emitChanged);
        }
      }
    }
    const onChangePromise = this.#internals.onChange?.([...this.#changes.changeList]);
    if (onChangePromise) {
      return onChangePromise.then(emitChanged);
    }
    return emitChanged();
  }
  /**
   * On rollback, we just replace values and version with the canonical versions.
   */
  rollback() {
    this.#values = this.#internals.values;
    this.#refreshSubrefs();
  }
  // -- Private helpers --
  #startWrite() {
    const transaction = this.#session.transaction;
    transaction.addResourcesSync(this.#resource);
    transaction.addParticipants(this);
    transaction.beginSync();
    const interactionComplete = this.#session.interactionComplete;
    if (interactionComplete && !interactionComplete.isObservedBy(this.#internals.interactionObserver)) {
      let emitBegin;
      if (hasRemoteActor(this.#session)) {
        emitBegin = !this.#session.interactionStarted;
        if (emitBegin) {
          this.#session.interactionStarted = true;
        }
      } else {
        emitBegin = !localInteractionBeginEmitted.has(interactionComplete);
        if (emitBegin) {
          localInteractionBeginEmitted.add(interactionComplete);
        }
      }
      if (emitBegin && this.#internals.events?.interactionBegin?.isObserved) {
        let handleBeginObserverError2 = function(error) {
          logger.warn(`Error in ${location.path} observer:`, error);
        };
        var handleBeginObserverError = handleBeginObserverError2;
        const location = this.#internals.location;
        try {
          const result = this.#internals.events?.interactionBegin?.emit(this.#session);
          if (MaybePromise.is(result)) {
            MaybePromise.then(result, void 0, handleBeginObserverError2);
          }
        } catch (e) {
          handleBeginObserverError2(e);
        }
      }
      if (!this.#internals.observedInteractions) {
        this.#internals.observedInteractions = /* @__PURE__ */ new Set();
      }
      this.#internals.observedInteractions.add(interactionComplete);
      interactionComplete.on(this.#internals.interactionObserver);
    }
  }
  #refreshSubrefs() {
    const subrefs = this.subrefs;
    if (subrefs) {
      for (const key in subrefs) {
        subrefs[key].refresh();
      }
    }
  }
  #incrementVersion() {
    if (!this.#internals.manageVersion) {
      return;
    }
    this.#internals.version++;
    if (this.#internals.version > 4294967295) {
      this.#internals.version = 0;
    }
  }
  #computePreCommitChange(name) {
    let oldValue;
    if (this.#precommitValues && name in this.#precommitValues) {
      oldValue = this.#precommitValues[name];
    } else {
      oldValue = this.#internals.values[name];
    }
    const newValue = this.#values[name];
    if (isDeepEqual(oldValue, newValue)) {
      return;
    }
    if (!this.#precommitValues) {
      this.#precommitValues = {};
    }
    this.#precommitValues[name] = deepCopy(newValue);
    return { newValue: this.#context.managed[name], oldValue };
  }
  #computePostCommitChanges() {
    this.#changes = void 0;
    if (this.#internals.values === this.#values) {
      return;
    }
    for (const name in this.#values) {
      const newval = this.#values[name];
      const oldval = this.#internals.values[name];
      if (oldval !== newval && !isDeepEqual(newval, oldval)) {
        if (!this.#changes) {
          this.#changes = { notifications: [], changeList: /* @__PURE__ */ new Set() };
        }
        this.#changes.changeList.add(name);
        if (this.#internals.persistentFields.has(name)) {
          if (this.#changes.persistent === void 0) {
            this.#changes.persistent = {};
          }
          this.#changes.persistent[name] = this.#values[name];
        }
        const event = this.#internals.changedEventFor(name);
        if (event?.isObserved) {
          this.#changes.notifications.push({
            event,
            params: [this.#values[name], this.#internals.values[name], this.#session]
          });
        }
      }
    }
    if (this.#changes) {
      this.#incrementVersion();
      if (this.#internals.events.stateChanged?.isObserved) {
        this.#changes.notifications.push({
          event: this.#internals.events.stateChanged,
          params: [this.#session]
        });
      }
    }
  }
}
function createReference(resource, internals, session) {
  const ref = new RootReference(resource, internals, session);
  return ref.initialize();
}
export {
  Datasource
};
//# sourceMappingURL=Datasource.js.map
