const EMPTY_STRING = '';

const getIfNotFunction =
  (predicate = isNullish) =>
  (value, then, otherwise) =>
    predicate(value) ? otherwise?.() : then(value);
const isNullish = (thing) => thing == null;
const isUndefined = (thing) => thing === void 0;
const isNull = (thing) => thing === null;
const ifNotUndefined = getIfNotFunction(isUndefined);
const isArray = (thing) => Array.isArray(thing);
const size = (arrayOrString) => arrayOrString.length;
const test = (regex, subject) => regex.test(subject);

const arrayHas = (array, value) => array.includes(value);
const arrayForEach = (array, cb) => array.forEach(cb);
const arrayIsEmpty = (array) => size(array) == 0;
const arrayReduce = (array, cb, initial) => array.reduce(cb, initial);
const arrayClear = (array, to) => array.splice(0, to);
const arrayPush = (array, ...values) => array.push(...values);
const arrayPop = (array) => array.pop();
const arrayUnshift = (array, ...values) => array.unshift(...values);
const arrayShift = (array) => array.shift();

const setOrDelCell = (store, tableId, rowId, cellId, cell) =>
  isUndefined(cell)
    ? store.delCell(tableId, rowId, cellId, true)
    : store.setCell(tableId, rowId, cellId, cell);
const setOrDelValue = (store, valueId, value) =>
  isUndefined(value) ? store.delValue(valueId) : store.setValue(valueId, value);

const collSizeN = (collSizer) => (coll) =>
  arrayReduce(collValues(coll), (total, coll2) => total + collSizer(coll2), 0);
const collSize = (coll) => coll?.size ?? 0;
const collSize2 = collSizeN(collSize);
const collHas = (coll, keyOrValue) => coll?.has(keyOrValue) ?? false;
const collIsEmpty = (coll) => isUndefined(coll) || collSize(coll) == 0;
const collValues = (coll) => [...(coll?.values() ?? [])];
const collForEach = (coll, cb) => coll?.forEach(cb);
const collDel = (coll, keyOrValue) => coll?.delete(keyOrValue);

const object = Object;
const objFreeze = object.freeze;

const mapNew = (entries) => new Map(entries);
const mapGet = (map, key) => map?.get(key);
const mapForEach = (map, cb) =>
  collForEach(map, (value, key) => cb(key, value));
const mapSet = (map, key, value) =>
  value === void 0 ? (collDel(map, key), map) : map?.set(key, value);
const mapEnsure = (map, key, getDefaultValue, hadExistingValue) => {
  if (!collHas(map, key)) {
    mapSet(map, key, getDefaultValue());
  } else {
    hadExistingValue?.(mapGet(map, key));
  }
  return mapGet(map, key);
};
const visitTree = (node, path, ensureLeaf, pruneLeaf, p = 0) =>
  ifNotUndefined(
    (ensureLeaf ? mapEnsure : mapGet)(
      node,
      path[p],
      p > size(path) - 2 ? ensureLeaf : mapNew,
    ),
    (nodeOrLeaf) => {
      if (p > size(path) - 2) {
        if (pruneLeaf?.(nodeOrLeaf)) {
          mapSet(node, path[p]);
        }
        return nodeOrLeaf;
      }
      const leaf = visitTree(nodeOrLeaf, path, ensureLeaf, pruneLeaf, p + 1);
      if (collIsEmpty(nodeOrLeaf)) {
        mapSet(node, path[p]);
      }
      return leaf;
    },
  );

const setNew = (entryOrEntries) =>
  new Set(
    isArray(entryOrEntries) || isUndefined(entryOrEntries)
      ? entryOrEntries
      : [entryOrEntries],
  );
const setAdd = (set, value) => set?.add(value);

const getCreateFunction = (getFunction, initFunction) => {
  const thingsByStore = /* @__PURE__ */ new WeakMap();
  return (store) => {
    if (!thingsByStore.has(store)) {
      thingsByStore.set(store, getFunction(store));
    }
    const thing = thingsByStore.get(store);
    initFunction?.(thing);
    return thing;
  };
};

const INTEGER = /^\d+$/;
const getPoolFunctions = () => {
  const pool = [];
  let nextId = 0;
  return [
    (reuse) => (reuse ? arrayShift(pool) : null) ?? EMPTY_STRING + nextId++,
    (id) => {
      if (test(INTEGER, id) && size(pool) < 1e3) {
        arrayPush(pool, id);
      }
    },
  ];
};

const getWildcardedLeaves = (deepIdSet, path = [EMPTY_STRING]) => {
  const leaves = [];
  const deep = (node, p) =>
    p == size(path)
      ? arrayPush(leaves, node)
      : path[p] === null
        ? collForEach(node, (node2) => deep(node2, p + 1))
        : arrayForEach([path[p], null], (id) => deep(mapGet(node, id), p + 1));
  deep(deepIdSet, 0);
  return leaves;
};
const getListenerFunctions = (getThing) => {
  let thing;
  const [getId, releaseId] = getPoolFunctions();
  const allListeners = mapNew();
  const addListener = (
    listener,
    idSetNode,
    path,
    pathGetters = [],
    extraArgsGetter = () => [],
  ) => {
    thing ??= getThing();
    const id = getId(1);
    mapSet(allListeners, id, [
      listener,
      idSetNode,
      path,
      pathGetters,
      extraArgsGetter,
    ]);
    setAdd(visitTree(idSetNode, path ?? [EMPTY_STRING], setNew), id);
    return id;
  };
  const callListeners = (idSetNode, ids, ...extraArgs) =>
    arrayForEach(getWildcardedLeaves(idSetNode, ids), (set) =>
      collForEach(set, (id) =>
        mapGet(allListeners, id)[0](thing, ...(ids ?? []), ...extraArgs),
      ),
    );
  const delListener = (id) =>
    ifNotUndefined(mapGet(allListeners, id), ([, idSetNode, idOrNulls]) => {
      visitTree(idSetNode, idOrNulls ?? [EMPTY_STRING], void 0, (idSet) => {
        collDel(idSet, id);
        return collIsEmpty(idSet) ? 1 : 0;
      });
      mapSet(allListeners, id);
      releaseId(id);
      return idOrNulls;
    });
  const callListener = (id) =>
    ifNotUndefined(
      mapGet(allListeners, id),
      ([listener, , path = [], pathGetters, extraArgsGetter]) => {
        const callWithIds = (...ids) => {
          const index = size(ids);
          if (index == size(path)) {
            listener(thing, ...ids, ...extraArgsGetter(ids));
          } else if (isNull(path[index])) {
            arrayForEach(pathGetters[index]?.(...ids) ?? [], (id2) =>
              callWithIds(...ids, id2),
            );
          } else {
            callWithIds(...ids, path[index]);
          }
        };
        callWithIds();
      },
    );
  return [addListener, callListeners, delListener, callListener];
};

const createCheckpoints = getCreateFunction(
  (store) => {
    let backwardIdsSize = 100;
    let currentId;
    let cellsDelta = mapNew();
    let valuesDelta = mapNew();
    let listening = 1;
    let nextCheckpointId;
    let checkpointsChanged;
    const checkpointIdsListeners = mapNew();
    const checkpointListeners = mapNew();
    const [addListener, callListeners, delListenerImpl] = getListenerFunctions(
      () => checkpoints,
    );
    const deltas = mapNew();
    const labels = mapNew();
    const backwardIds = [];
    const forwardIds = [];
    const updateStore = (oldOrNew, checkpointId) => {
      listening = 0;
      store.transaction(() => {
        const [cellsDelta2, valuesDelta2] = mapGet(deltas, checkpointId);
        collForEach(cellsDelta2, (table, tableId) =>
          collForEach(table, (row, rowId) =>
            collForEach(row, (oldNew, cellId) =>
              setOrDelCell(store, tableId, rowId, cellId, oldNew[oldOrNew]),
            ),
          ),
        );
        collForEach(valuesDelta2, (oldNew, valueId) =>
          setOrDelValue(store, valueId, oldNew[oldOrNew]),
        );
      });
      listening = 1;
    };
    const clearCheckpointId = (checkpointId) => {
      mapSet(deltas, checkpointId);
      mapSet(labels, checkpointId);
      callListeners(checkpointListeners, [checkpointId]);
    };
    const clearCheckpointIds = (checkpointIds, to) =>
      arrayForEach(
        arrayClear(checkpointIds, to ?? size(checkpointIds)),
        clearCheckpointId,
      );
    const trimBackwardsIds = () =>
      clearCheckpointIds(backwardIds, size(backwardIds) - backwardIdsSize);
    const storeChanged = () =>
      ifNotUndefined(currentId, () => {
        arrayPush(backwardIds, currentId);
        trimBackwardsIds();
        clearCheckpointIds(forwardIds);
        currentId = void 0;
        checkpointsChanged = 1;
      });
    const storeUnchanged = () => {
      currentId = arrayPop(backwardIds);
      checkpointsChanged = 1;
    };
    let cellListenerId;
    let valueListenerId;
    const addCheckpointImpl = (label = EMPTY_STRING) => {
      if (isUndefined(currentId)) {
        currentId = EMPTY_STRING + nextCheckpointId++;
        mapSet(deltas, currentId, [cellsDelta, valuesDelta]);
        setCheckpoint(currentId, label);
        cellsDelta = mapNew();
        valuesDelta = mapNew();
        checkpointsChanged = 1;
      }
      return currentId;
    };
    const goBackwardImpl = () => {
      if (!arrayIsEmpty(backwardIds)) {
        arrayUnshift(forwardIds, addCheckpointImpl());
        updateStore(0, currentId);
        currentId = arrayPop(backwardIds);
        checkpointsChanged = 1;
      }
    };
    const goForwardImpl = () => {
      if (!arrayIsEmpty(forwardIds)) {
        arrayPush(backwardIds, currentId);
        currentId = arrayShift(forwardIds);
        updateStore(1, currentId);
        checkpointsChanged = 1;
      }
    };
    const callListenersIfChanged = () => {
      if (checkpointsChanged) {
        callListeners(checkpointIdsListeners);
        checkpointsChanged = 0;
      }
    };
    const setSize = (size2) => {
      backwardIdsSize = size2;
      trimBackwardsIds();
      return checkpoints;
    };
    const addCheckpoint = (label) => {
      const id = addCheckpointImpl(label);
      callListenersIfChanged();
      return id;
    };
    const setCheckpoint = (checkpointId, label) => {
      if (
        hasCheckpoint(checkpointId) &&
        mapGet(labels, checkpointId) !== label
      ) {
        mapSet(labels, checkpointId, label);
        callListeners(checkpointListeners, [checkpointId]);
      }
      return checkpoints;
    };
    const getStore = () => store;
    const getCheckpointIds = () => [
      [...backwardIds],
      currentId,
      [...forwardIds],
    ];
    const forEachCheckpoint = (checkpointCallback) =>
      mapForEach(labels, checkpointCallback);
    const hasCheckpoint = (checkpointId) => collHas(deltas, checkpointId);
    const getCheckpoint = (checkpointId) => mapGet(labels, checkpointId);
    const goBackward = () => {
      goBackwardImpl();
      callListenersIfChanged();
      return checkpoints;
    };
    const goForward = () => {
      goForwardImpl();
      callListenersIfChanged();
      return checkpoints;
    };
    const goTo = (checkpointId) => {
      const action = arrayHas(backwardIds, checkpointId)
        ? goBackwardImpl
        : arrayHas(forwardIds, checkpointId)
          ? goForwardImpl
          : void 0;
      while (!isUndefined(action) && checkpointId != currentId) {
        action();
      }
      callListenersIfChanged();
      return checkpoints;
    };
    const addCheckpointIdsListener = (listener) =>
      addListener(listener, checkpointIdsListeners);
    const addCheckpointListener = (checkpointId, listener) =>
      addListener(listener, checkpointListeners, [checkpointId]);
    const delListener = (listenerId) => {
      delListenerImpl(listenerId);
      return checkpoints;
    };
    const clear = () => {
      clearCheckpointIds(backwardIds);
      clearCheckpointIds(forwardIds);
      if (!isUndefined(currentId)) {
        clearCheckpointId(currentId);
      }
      currentId = void 0;
      nextCheckpointId = 0;
      addCheckpoint();
      return checkpoints;
    };
    const clearForward = () => {
      if (!arrayIsEmpty(forwardIds)) {
        clearCheckpointIds(forwardIds);
        callListeners(checkpointIdsListeners);
      }
      return checkpoints;
    };
    const destroy = () => {
      store.delListener(cellListenerId);
      store.delListener(valueListenerId);
    };
    const getListenerStats = () => ({
      checkpointIds: collSize2(checkpointIdsListeners),
      checkpoint: collSize2(checkpointListeners),
    });
    const _registerListeners = () => {
      cellListenerId = store.addCellListener(
        null,
        null,
        null,
        (_store, tableId, rowId, cellId, newCell, oldCell) => {
          if (listening) {
            storeChanged();
            const table = mapEnsure(cellsDelta, tableId, mapNew);
            const row = mapEnsure(table, rowId, mapNew);
            const oldNew = mapEnsure(row, cellId, () => [oldCell, void 0]);
            oldNew[1] = newCell;
            if (
              oldNew[0] === newCell &&
              collIsEmpty(mapSet(row, cellId)) &&
              collIsEmpty(mapSet(table, rowId)) &&
              collIsEmpty(mapSet(cellsDelta, tableId))
            ) {
              storeUnchanged();
            }
            callListenersIfChanged();
          }
        },
      );
      valueListenerId = store.addValueListener(
        null,
        (_store, valueId, newValue, oldValue) => {
          if (listening) {
            storeChanged();
            const oldNew = mapEnsure(valuesDelta, valueId, () => [
              oldValue,
              void 0,
            ]);
            oldNew[1] = newValue;
            if (
              oldNew[0] === newValue &&
              collIsEmpty(mapSet(valuesDelta, valueId))
            ) {
              storeUnchanged();
            }
            callListenersIfChanged();
          }
        },
      );
    };
    const checkpoints = {
      setSize,
      addCheckpoint,
      setCheckpoint,
      getStore,
      getCheckpointIds,
      forEachCheckpoint,
      hasCheckpoint,
      getCheckpoint,
      goBackward,
      goForward,
      goTo,
      addCheckpointIdsListener,
      addCheckpointListener,
      delListener,
      clear,
      clearForward,
      destroy,
      getListenerStats,
      _registerListeners,
    };
    return objFreeze(checkpoints.clear());
  },
  (checkpoints) => checkpoints._registerListeners(),
);

export {createCheckpoints};
