/**
 * @license
 * Copyright 2022-2025 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import {
  camelize,
  deepCopy,
  ImplementationError,
  InternalError,
  isDeepEqual,
  Lifetime,
  Logger,
  MaybePromise,
  Transaction
} from "#general";
import { ExpiredReferenceError, hasRemoteActor, Val } from "#protocol";
const logger = Logger.get("Datasource");
const FEATURES_KEY = "__features__";
const viewTx = Transaction.open("offline-view", Lifetime.process, "ro");
function Datasource(options) {
  const internals = configure(options);
  configureExternalChanges(internals);
  let readOnlyView;
  return {
    toString() {
      return internals.location.path.toString();
    },
    reference(session) {
      let ref = internals.sessions?.get(session);
      if (!ref) {
        ref = createReference(this, internals, session);
      }
      return ref.managed;
    },
    get version() {
      return internals.version;
    },
    get location() {
      return internals.location;
    },
    get events() {
      return internals.events;
    },
    validate(session, values) {
      const validate = internals.supervisor.validate;
      if (!validate) {
        return;
      }
      validate(values ?? internals.values, session, { path: internals.location.path });
    },
    get view() {
      if (!readOnlyView) {
        const session = {
          transaction: viewTx
        };
        readOnlyView = createReference(this, internals, session).managed;
      }
      return readOnlyView;
    }
  };
}
((Datasource2) => {
  Datasource2.UNKNOWN_VERSION = -1;
})(Datasource || (Datasource = {}));
function configure(options) {
  let values = new options.type();
  let storedValues = options.store?.initialValues;
  let featuresKey;
  if (options.supervisor.featureMap.children.length) {
    featuresKey = [...options.supervisor.supportedFeatures].join(",");
    const storedFeaturesKey = storedValues?.[FEATURES_KEY];
    if (storedFeaturesKey !== void 0 && storedFeaturesKey !== featuresKey) {
      logger.warn(
        `Ignoring persisted values for ${options.location.path} because features changed from "${storedFeaturesKey}" to "${featuresKey}"`
      );
      storedValues = void 0;
    }
  }
  const initialValues = {
    ...options.defaults,
    ...storedValues
  };
  if (FEATURES_KEY in initialValues) {
    delete initialValues[FEATURES_KEY];
  }
  for (const key in initialValues) {
    values[key] = initialValues[key];
  }
  Object.freeze(options.location);
  const events = options.events ?? {};
  let changedEventIndex;
  const persistentFields = options.supervisor.persistentKeys(options.primaryKey);
  return {
    ...options,
    primaryKey: options.primaryKey === "id" ? "id" : "name",
    events,
    version: options.entropy.randomUint32,
    featuresKey,
    manageVersion: true,
    persistentFields,
    get values() {
      return values;
    },
    set values(newValues) {
      const oldValues = this.values;
      values = newValues;
      if (this.sessions) {
        for (const context of this.sessions.values()) {
          context.onChange(oldValues);
        }
      }
    },
    interactionObserver(session) {
      function handleObserverError(error) {
        logger.error(`Error in ${options.location.path} observer:`, error);
      }
      if (options.events?.interactionEnd?.isObserved) {
        try {
          const result = options.events?.interactionEnd?.emit(session);
          if (MaybePromise.is(result)) {
            return MaybePromise.then(result, void 0, handleObserverError);
          }
        } catch (e) {
          handleObserverError(e);
        }
      }
    },
    changedEventFor(key) {
      if (changedEventIndex === void 0) {
        changedEventIndex = /* @__PURE__ */ new Map();
      } else if (changedEventIndex.has(key)) {
        return changedEventIndex.get(key);
      }
      const id = Number.parseInt(key);
      let event;
      if (!Number.isFinite(id)) {
        event = events[`${key}$Changed`];
      } else {
        const field = options.supervisor.schema.member(id);
        if (field !== void 0) {
          event = events[`${camelize(field.name)}$Changed`];
        }
      }
      changedEventIndex.set(key, event);
      return event;
    }
  };
}
function isExternal(store) {
  return !!store && "externalSet" in store;
}
function configureExternalChanges(internals) {
  const { store } = internals;
  if (!isExternal(store)) {
    return;
  }
  internals.version = store.version;
  internals.manageVersion = false;
  store.externalChangeListener = async (potentialChanges) => {
    const { values } = internals;
    let changes;
    let oldValues;
    for (const name in potentialChanges) {
      if (isDeepEqual(values[name], potentialChanges[name])) {
        continue;
      }
      if (changes === void 0) {
        changes = { [name]: potentialChanges[name] };
        oldValues = { [name]: values[name] };
      } else {
        changes[name] = potentialChanges[name];
        oldValues[name] = values[name];
      }
    }
    internals.version = store.version;
    if (!changes) {
      return;
    }
    internals.values = {
      ...internals.values,
      ...changes
    };
    const changedProps = Object.keys(changes);
    const onChangePromise = internals.onChange?.(Array.from(changedProps));
    const iterator = changedProps[Symbol.iterator]();
    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 = internals.changedEventFor(name);
        if (!event?.isObserved) {
          continue;
        }
        const result = event.emit(changes[name], oldValues[name]);
        if (MaybePromise.is(result)) {
          return Promise.resolve(result).then(emitChanged);
        }
      }
    }
  };
  store.releaseValues = () => {
    const { values } = internals;
    internals.values = {};
    return values;
  };
}
function createReference(resource, internals, session) {
  let values = internals.values;
  let precommitValues;
  let changes;
  let expired = false;
  const participant = {
    toString() {
      return internals.location.path.toString();
    },
    preCommit,
    commit1,
    commit2,
    postCommit,
    rollback
  };
  const transaction = session.transaction;
  void transaction.onShared(() => {
    if (values !== internals.values) {
      try {
        rollback();
      } catch (e) {
        logger.error(
          `Error resetting reference to ${internals.location.path} after reset of transaction ${transaction.via}:`,
          e
        );
      }
    }
  });
  const fields = internals.supervisor.memberNames;
  const reference = {
    primaryKey: internals.primaryKey,
    get original() {
      return internals.values;
    },
    get value() {
      if (expired) {
        throw new ExpiredReferenceError(this.location);
      }
      return values;
    },
    set value(_value) {
      throw new InternalError(`Cannot set root reference for ${internals.supervisor.schema.name}`);
    },
    get expired() {
      return expired;
    },
    get location() {
      return internals.location;
    },
    set location(_loc) {
      throw new ImplementationError("Root reference location is immutable");
    },
    get rootOwner() {
      return internals.owner;
    },
    change(mutator) {
      if (expired) {
        throw new ExpiredReferenceError(this.location);
      }
      startWrite();
      transaction.beginSync();
      if (values === internals.values) {
        const old = values;
        values = new internals.type();
        const properties = values[Val.properties] ? values[Val.properties](this.rootOwner, session) : void 0;
        for (const index of fields) {
          if (properties && index in properties) {
          } else {
            values[index] = old[index];
          }
        }
        refreshSubrefs();
      }
      mutator();
      refreshSubrefs();
    },
    refresh() {
      throw new InternalError(`Cannot refresh root reference for ${internals.supervisor.schema.name}`);
    }
  };
  reference.toString = () => `ref<${resource}>`;
  const context = {
    managed: internals.supervisor.manage(reference, session),
    onChange(oldValues) {
      if (values === oldValues) {
        values = internals.values;
        refreshSubrefs();
      }
    }
  };
  if (session.transaction.isolation !== "snapshot") {
    if (!internals.sessions) {
      internals.sessions = /* @__PURE__ */ new Map();
    }
    internals.sessions.set(session, context);
  }
  void transaction.onClose(() => {
    try {
      internals.sessions?.delete(session);
      expired = true;
      refreshSubrefs();
    } catch (e) {
      logger.error(
        `Error detaching reference to ${internals.location.path} from closed transaction ${transaction.via}:`,
        e
      );
    }
  });
  return context;
  function startWrite() {
    transaction.addResourcesSync(resource);
    transaction.addParticipants(participant);
    transaction.beginSync();
    if (hasRemoteActor(session) && !session.interactionStarted && session.interactionComplete && !session.interactionComplete.isObservedBy(internals.interactionObserver)) {
      session.interactionStarted = true;
      if (internals.events?.interactionBegin?.isObserved) {
        internals.events?.interactionBegin?.emit(session);
      }
      session.interactionComplete.on(internals.interactionObserver);
    }
  }
  function refreshSubrefs() {
    const subrefs = reference.subrefs;
    if (subrefs) {
      for (const key in subrefs) {
        subrefs[key].refresh();
      }
    }
  }
  function incrementVersion() {
    if (!internals.manageVersion) {
      return;
    }
    internals.version++;
    if (internals.version > 4294967295) {
      internals.version = 0;
    }
  }
  function computePreCommitChange(name) {
    let oldValue;
    if (precommitValues && name in precommitValues) {
      oldValue = precommitValues[name];
    } else {
      oldValue = internals.values[name];
    }
    const newValue = values[name];
    if (isDeepEqual(oldValue, newValue)) {
      return;
    }
    if (!precommitValues) {
      precommitValues = {};
    }
    precommitValues[name] = deepCopy(newValue);
    return { newValue: context.managed[name], oldValue };
  }
  function preCommit() {
    const { events } = internals;
    if (!events) {
      return false;
    }
    let mayHaveMutated = false;
    const keyIterator = Object.keys(values)[Symbol.iterator]();
    function 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 = computePreCommitChange(name);
        if (change) {
          mayHaveMutated = true;
          const result = event.emit(change.newValue, change.oldValue, session);
          if (MaybePromise.is(result)) {
            return result.then(nextKey);
          }
        }
      }
    }
    return nextKey();
  }
  function computePostCommitChanges() {
    changes = void 0;
    if (internals.values === values) {
      return changes;
    }
    for (const name in values) {
      const newval = values[name];
      const oldval = internals.values[name];
      if (oldval !== newval && !isDeepEqual(newval, oldval)) {
        if (!changes) {
          changes = { notifications: [], changeList: /* @__PURE__ */ new Set() };
        }
        changes.changeList.add(name);
        if (internals.persistentFields.has(name)) {
          if (changes.persistent === void 0) {
            changes.persistent = {};
          }
          changes.persistent[name] = values[name];
        }
        const event = internals.changedEventFor(name);
        if (event?.isObserved) {
          changes.notifications.push({
            event,
            params: [values[name], internals.values[name], session]
          });
        }
      }
    }
    if (changes) {
      incrementVersion();
      if (internals.events.stateChanged?.isObserved) {
        changes.notifications.push({
          event: internals.events.stateChanged,
          params: [session]
        });
      }
    }
  }
  function commit1() {
    computePostCommitChanges();
    const persistent = changes?.persistent;
    if (!persistent) {
      return;
    }
    if (internals.featuresKey !== void 0) {
      persistent[FEATURES_KEY] = internals.featuresKey;
    }
    return internals.store?.set(session.transaction, persistent);
  }
  function commit2() {
    if (!changes) {
      return;
    }
    internals.values = values;
  }
  function postCommit() {
    if (!changes) {
      return;
    }
    const iterator = 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 = internals.onChange?.([...changes.changeList]);
    if (onChangePromise) {
      return onChangePromise.then(emitChanged);
    }
    return emitChanged();
  }
  function rollback() {
    ({ values } = internals);
    refreshSubrefs();
  }
}
export {
  Datasource
};
//# sourceMappingURL=Datasource.js.map
