const getTypeOf = (thing) => typeof thing;
const EMPTY_STRING = '';
const STRING = getTypeOf(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 isString = (thing) => getTypeOf(thing) == STRING;
const isArray = (thing) => Array.isArray(thing);
const size = (arrayOrString) => arrayOrString.length;
const test = (regex, subject) => regex.test(subject);

const arrayEvery = (array, cb) => array.every(cb);
const arrayIsEqual = (array1, array2) =>
  size(array1) === size(array2) &&
  arrayEvery(array1, (value1, index) => array2[index] === value1);
const arrayForEach = (array, cb) => array.forEach(cb);
const arrayIsEmpty = (array) => size(array) == 0;
const arrayReduce = (array, cb, initial) => array.reduce(cb, initial);
const arrayPush = (array, ...values) => array.push(...values);
const arrayShift = (array) => array.shift();

const collSizeN = (collSizer) => (coll) =>
  arrayReduce(collValues(coll), (total, coll2) => total + collSizer(coll2), 0);
const collSize = (coll) => coll?.size ?? 0;
const collSize2 = collSizeN(collSize);
const collSize3 = collSizeN(collSize2);
const collHas = (coll, keyOrValue) => coll?.has(keyOrValue) ?? false;
const collIsEmpty = (coll) => isUndefined(coll) || collSize(coll) == 0;
const collValues = (coll) => [...(coll?.values() ?? [])];
const collClear = (coll) => coll.clear();
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 mapKeys = (map) => [...(map?.keys() ?? [])];
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 getDefinableFunctions = (
  store,
  getDefaultThing,
  validateRowValue,
  addListener,
  callListeners,
) => {
  const hasRow = store.hasRow;
  const tableIds = mapNew();
  const things = mapNew();
  const thingIdListeners = mapNew();
  const allRowValues = mapNew();
  const allSortKeys = mapNew();
  const storeListenerIds = mapNew();
  const getStore = () => store;
  const getThingIds = () => mapKeys(tableIds);
  const forEachThing = (cb) => mapForEach(things, cb);
  const hasThing = (id) => collHas(things, id);
  const getTableId = (id) => mapGet(tableIds, id);
  const getThing = (id) => mapGet(things, id);
  const setThing = (id, thing) => mapSet(things, id, thing);
  const addStoreListeners = (id, andCall, ...listenerIds) => {
    const set = mapEnsure(storeListenerIds, id, setNew);
    arrayForEach(
      listenerIds,
      (listenerId) =>
        setAdd(set, listenerId) && andCall && store.callListener(listenerId),
    );
    return listenerIds;
  };
  const delStoreListeners = (id, ...listenerIds) =>
    ifNotUndefined(mapGet(storeListenerIds, id), (allListenerIds) => {
      arrayForEach(
        arrayIsEmpty(listenerIds) ? collValues(allListenerIds) : listenerIds,
        (listenerId) => {
          store.delListener(listenerId);
          collDel(allListenerIds, listenerId);
        },
      );
      if (collIsEmpty(allListenerIds)) {
        mapSet(storeListenerIds, id);
      }
    });
  const setDefinition = (id, tableId) => {
    mapSet(tableIds, id, tableId);
    if (!collHas(things, id)) {
      mapSet(things, id, getDefaultThing());
      mapSet(allRowValues, id, mapNew());
      mapSet(allSortKeys, id, mapNew());
      callListeners(thingIdListeners);
    }
  };
  const setDefinitionAndListen = (
    id,
    tableId,
    onChanged,
    getRowValue,
    getSortKey,
  ) => {
    setDefinition(id, tableId);
    const changedRowValues = mapNew();
    const changedSortKeys = mapNew();
    const rowValues = mapGet(allRowValues, id);
    const sortKeys = mapGet(allSortKeys, id);
    const processRow = (rowId) => {
      const getCell = (cellId) => store.getCell(tableId, rowId, cellId);
      const oldRowValue = mapGet(rowValues, rowId);
      const newRowValue = hasRow(tableId, rowId)
        ? validateRowValue(getRowValue(getCell, rowId))
        : void 0;
      if (
        !(
          oldRowValue === newRowValue ||
          (isArray(oldRowValue) &&
            isArray(newRowValue) &&
            arrayIsEqual(oldRowValue, newRowValue))
        )
      ) {
        mapSet(changedRowValues, rowId, [oldRowValue, newRowValue]);
      }
      if (!isUndefined(getSortKey)) {
        const oldSortKey = mapGet(sortKeys, rowId);
        const newSortKey = hasRow(tableId, rowId)
          ? getSortKey(getCell, rowId)
          : void 0;
        if (oldSortKey != newSortKey) {
          mapSet(changedSortKeys, rowId, newSortKey);
        }
      }
    };
    const processTable = (force) => {
      onChanged(
        () => {
          collForEach(changedRowValues, ([, newRowValue], rowId) =>
            mapSet(rowValues, rowId, newRowValue),
          );
          collForEach(changedSortKeys, (newSortKey, rowId) =>
            mapSet(sortKeys, rowId, newSortKey),
          );
        },
        changedRowValues,
        changedSortKeys,
        rowValues,
        sortKeys,
        force,
      );
      collClear(changedRowValues);
      collClear(changedSortKeys);
    };
    mapForEach(rowValues, processRow);
    if (store.hasTable(tableId)) {
      arrayForEach(store.getRowIds(tableId), (rowId) => {
        if (!collHas(rowValues, rowId)) {
          processRow(rowId);
        }
      });
    }
    processTable(true);
    delStoreListeners(id);
    addStoreListeners(
      id,
      0,
      store.addRowListener(tableId, null, (_store, _tableId, rowId) =>
        processRow(rowId),
      ),
      store.addTableListener(tableId, () => processTable()),
    );
  };
  const delDefinition = (id) => {
    mapSet(tableIds, id);
    mapSet(things, id);
    mapSet(allRowValues, id);
    mapSet(allSortKeys, id);
    delStoreListeners(id);
    callListeners(thingIdListeners);
  };
  const addThingIdsListener = (listener) =>
    addListener(listener, thingIdListeners);
  const destroy = () => mapForEach(storeListenerIds, delDefinition);
  return [
    getStore,
    getThingIds,
    forEachThing,
    hasThing,
    getTableId,
    getThing,
    setThing,
    setDefinition,
    setDefinitionAndListen,
    delDefinition,
    addThingIdsListener,
    destroy,
    addStoreListeners,
    delStoreListeners,
  ];
};
const getRowCellFunction = (getRowCell, defaultCellValue) =>
  isString(getRowCell)
    ? (getCell) => getCell(getRowCell)
    : (getRowCell ?? (() => defaultCellValue ?? EMPTY_STRING));
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);
    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 createRelationships = getCreateFunction((store) => {
  const remoteTableIds = mapNew();
  const remoteRowIdListeners = mapNew();
  const localRowIdsListeners = mapNew();
  const linkedRowIdsListeners = mapNew();
  const [addListener, callListeners, delListenerImpl] = getListenerFunctions(
    () => relationships,
  );
  const [
    getStore,
    getRelationshipIds,
    forEachRelationshipImpl,
    hasRelationship,
    getLocalTableId,
    getRelationship,
    ,
    ,
    setDefinitionAndListen,
    delDefinition,
    addRelationshipIdsListener,
    destroy,
  ] = getDefinableFunctions(
    store,
    () => [mapNew(), mapNew(), mapNew(), mapNew()],
    (value) => (isUndefined(value) ? void 0 : value + EMPTY_STRING),
    addListener,
    callListeners,
  );
  const getLinkedRowIdsCache = (relationshipId, firstRowId, skipCache) =>
    ifNotUndefined(
      getRelationship(relationshipId),
      ([remoteRows, , linkedRowsCache]) => {
        if (!collHas(linkedRowsCache, firstRowId)) {
          const linkedRows = setNew();
          if (
            getLocalTableId(relationshipId) != getRemoteTableId(relationshipId)
          ) {
            setAdd(linkedRows, firstRowId);
          } else {
            let rowId = firstRowId;
            while (!isUndefined(rowId) && !collHas(linkedRows, rowId)) {
              setAdd(linkedRows, rowId);
              rowId = mapGet(remoteRows, rowId);
            }
          }
          if (skipCache) {
            return linkedRows;
          }
          mapSet(linkedRowsCache, firstRowId, linkedRows);
        }
        return mapGet(linkedRowsCache, firstRowId);
      },
    );
  const delLinkedRowIdsCache = (relationshipId, firstRowId) =>
    ifNotUndefined(getRelationship(relationshipId), ([, , linkedRowsCache]) =>
      mapSet(linkedRowsCache, firstRowId),
    );
  const setRelationshipDefinition = (
    relationshipId,
    localTableId,
    remoteTableId,
    getRemoteRowId2,
  ) => {
    mapSet(remoteTableIds, relationshipId, remoteTableId);
    setDefinitionAndListen(
      relationshipId,
      localTableId,
      (change, changedRemoteRowIds) => {
        const changedLocalRows = setNew();
        const changedRemoteRows = setNew();
        const changedLinkedRows = setNew();
        const [localRows, remoteRows] = getRelationship(relationshipId);
        collForEach(
          changedRemoteRowIds,
          ([oldRemoteRowId, newRemoteRowId], localRowId) => {
            if (!isUndefined(oldRemoteRowId)) {
              setAdd(changedRemoteRows, oldRemoteRowId);
              ifNotUndefined(
                mapGet(remoteRows, oldRemoteRowId),
                (oldRemoteRow) => {
                  collDel(oldRemoteRow, localRowId);
                  if (collIsEmpty(oldRemoteRow)) {
                    mapSet(remoteRows, oldRemoteRowId);
                  }
                },
              );
            }
            if (!isUndefined(newRemoteRowId)) {
              setAdd(changedRemoteRows, newRemoteRowId);
              if (!collHas(remoteRows, newRemoteRowId)) {
                mapSet(remoteRows, newRemoteRowId, setNew());
              }
              setAdd(mapGet(remoteRows, newRemoteRowId), localRowId);
            }
            setAdd(changedLocalRows, localRowId);
            mapSet(localRows, localRowId, newRemoteRowId);
            mapForEach(
              mapGet(linkedRowIdsListeners, relationshipId),
              (firstRowId) => {
                if (
                  collHas(
                    getLinkedRowIdsCache(relationshipId, firstRowId),
                    localRowId,
                  )
                ) {
                  setAdd(changedLinkedRows, firstRowId);
                }
              },
            );
          },
        );
        change();
        collForEach(changedLocalRows, (localRowId) =>
          callListeners(remoteRowIdListeners, [relationshipId, localRowId]),
        );
        collForEach(changedRemoteRows, (remoteRowId) =>
          callListeners(localRowIdsListeners, [relationshipId, remoteRowId]),
        );
        collForEach(changedLinkedRows, (firstRowId) => {
          delLinkedRowIdsCache(relationshipId, firstRowId);
          callListeners(linkedRowIdsListeners, [relationshipId, firstRowId]);
        });
      },
      getRowCellFunction(getRemoteRowId2),
    );
    return relationships;
  };
  const forEachRelationship = (relationshipCallback) =>
    forEachRelationshipImpl((relationshipId) =>
      relationshipCallback(relationshipId, (rowCallback) =>
        store.forEachRow(getLocalTableId(relationshipId), rowCallback),
      ),
    );
  const delRelationshipDefinition = (relationshipId) => {
    mapSet(remoteTableIds, relationshipId);
    delDefinition(relationshipId);
    return relationships;
  };
  const getRemoteTableId = (relationshipId) =>
    mapGet(remoteTableIds, relationshipId);
  const getRemoteRowId = (relationshipId, localRowId) =>
    mapGet(getRelationship(relationshipId)?.[0], localRowId);
  const getLocalRowIds = (relationshipId, remoteRowId) =>
    collValues(mapGet(getRelationship(relationshipId)?.[1], remoteRowId));
  const getLinkedRowIds = (relationshipId, firstRowId) =>
    isUndefined(getRelationship(relationshipId))
      ? [firstRowId]
      : collValues(getLinkedRowIdsCache(relationshipId, firstRowId, true));
  const addRemoteRowIdListener = (relationshipId, localRowId, listener) =>
    addListener(listener, remoteRowIdListeners, [relationshipId, localRowId]);
  const addLocalRowIdsListener = (relationshipId, remoteRowId, listener) =>
    addListener(listener, localRowIdsListeners, [relationshipId, remoteRowId]);
  const addLinkedRowIdsListener = (relationshipId, firstRowId, listener) => {
    getLinkedRowIdsCache(relationshipId, firstRowId);
    return addListener(listener, linkedRowIdsListeners, [
      relationshipId,
      firstRowId,
    ]);
  };
  const delListener = (listenerId) => {
    delLinkedRowIdsCache(...(delListenerImpl(listenerId) ?? []));
    return relationships;
  };
  const getListenerStats = () => ({
    remoteRowId: collSize3(remoteRowIdListeners),
    localRowIds: collSize3(localRowIdsListeners),
    linkedRowIds: collSize3(linkedRowIdsListeners),
  });
  const relationships = {
    setRelationshipDefinition,
    delRelationshipDefinition,
    getStore,
    getRelationshipIds,
    forEachRelationship,
    hasRelationship,
    getLocalTableId,
    getRemoteTableId,
    getRemoteRowId,
    getLocalRowIds,
    getLinkedRowIds,
    addRelationshipIdsListener,
    addRemoteRowIdListener,
    addLocalRowIdsListener,
    addLinkedRowIdsListener,
    delListener,
    destroy,
    getListenerStats,
  };
  return objFreeze(relationships);
});

export {createRelationships};
