/**
 * @license
 * Copyright 2022-2024 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import {
  Crypto,
  deepCopy,
  ImplementationError,
  InternalError,
  isDeepEqual,
  Logger,
  MaybePromise
} from "#general";
import { AccessLevel } from "#model";
import { ExpiredReferenceError } from "../../errors.js";
import { ReadOnlyTransaction } from "../transaction/Tx.js";
const logger = Logger.get("Datasource");
function Datasource(options) {
  const internals = configure(options);
  let readOnlyView;
  return {
    toString() {
      return internals.path.toString();
    },
    reference(session) {
      let context = internals.sessions?.get(session);
      if (!context) {
        context = createSessionContext(this, internals, session);
      }
      return context.managed;
    },
    get version() {
      return internals.version;
    },
    validate(session, values) {
      const validate = internals.supervisor.validate;
      if (!validate) {
        return;
      }
      validate(values ?? internals.values, session, { path: internals.path });
    },
    get view() {
      if (!readOnlyView) {
        const session = {
          offline: true,
          authorizedFor(desiredAccessLevel) {
            return desiredAccessLevel === AccessLevel.View;
          },
          transaction: ReadOnlyTransaction
        };
        readOnlyView = createSessionContext(this, internals, session).managed;
      }
      return readOnlyView;
    }
  };
}
function configure(options) {
  const values = new options.type();
  const initialValues = {
    ...options.defaults,
    ...options.store?.initialValues
  };
  for (const key in initialValues) {
    values[key] = initialValues[key];
  }
  return {
    ...options,
    version: Crypto.getRandomUInt32(),
    values,
    interactionObserver() {
      function handleObserverError(error) {
        logger.error(`Error in ${options.path} observer:`, error);
      }
      try {
        const result = options.events?.interactionEnd?.emit();
        if (MaybePromise.is(result)) {
          return result.then(handleObserverError);
        }
      } catch (e) {
        handleObserverError(e);
      }
    }
  };
}
function createSessionContext(resource, internals, session) {
  let values = internals.values;
  let precommitValues;
  let changes;
  let expired = false;
  const participant = {
    toString() {
      return internals.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.path} after reset of transaction ${transaction.via}:`,
          e
        );
      }
    }
  });
  const fields = internals.supervisor.memberNames;
  const persistentFields = internals.supervisor.persistentNames;
  const reference = {
    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 { path: internals.path, cluster: internals.cluster };
    },
    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();
        for (const index of fields) {
          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 (!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.path} from closed transaction ${transaction.via}:`,
        e
      );
    }
  });
  return context;
  function startWrite() {
    transaction.addResourcesSync(resource);
    transaction.addParticipants(participant);
    transaction.beginSync();
    if (session.interactionComplete && !session.interactionComplete.isObservedBy(internals.interactionObserver)) {
      internals.events?.interactionBegin?.emit();
      session.interactionComplete.on(internals.interactionObserver);
    }
  }
  function refreshSubrefs() {
    const subrefs = reference.subrefs;
    if (subrefs) {
      for (const key in subrefs) {
        subrefs[key].refresh();
      }
    }
  }
  function incrementVersion() {
    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: [] };
        }
        if (persistentFields.has(name)) {
          if (changes.persistent === void 0) {
            changes.persistent = {};
          }
          changes.persistent[name] = values[name];
        }
        const event = internals.events?.[`${name}$Changed`];
        if (event?.isObserved) {
          changes.notifications.push({
            event,
            params: [values[name], internals.values[name], session]
          });
        }
      }
    }
    if (changes) {
      incrementVersion();
    }
  }
  function commit1() {
    computePostCommitChanges();
    const persistent = changes?.persistent;
    if (!persistent) {
      return;
    }
    return internals.store?.set(session.transaction, persistent);
  }
  function commit2() {
    if (!changes) {
      return;
    }
    const oldValues = internals.values;
    internals.values = values;
    if (internals.sessions) {
      for (const context2 of internals.sessions.values()) {
        context2.onChange(oldValues);
      }
    }
    if (session.trace && changes.persistent) {
      let mutations = session.trace.mutations;
      if (!mutations) {
        mutations = session.trace.mutations = [];
      }
      mutations.push({
        path: internals.path,
        values: changes.persistent
      });
    }
  }
  function postCommit() {
    if (!changes || !internals.events) {
      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);
        }
      }
    }
    return emitChanged();
  }
  function rollback() {
    ({ values } = internals);
    refreshSubrefs();
  }
}
export {
  Datasource
};
//# sourceMappingURL=Datasource.js.map
