const getTypeOf = (thing) => typeof thing;
const EMPTY_STRING = '';
const STRING = getTypeOf(EMPTY_STRING);
const FUNCTION = getTypeOf(getTypeOf);
const SUM = 'sum';
const AVG = 'avg';
const MIN = 'min';
const MAX = 'max';

const getIfNotFunction =
  (predicate = isNullish) =>
  (value, then, otherwise) =>
    predicate(value) ? otherwise?.() : then(value);
const math = Math;
const mathMax = math.max;
const mathMin = math.min;
const isFiniteNumber = isFinite;
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 isFunction = (thing) => getTypeOf(thing) == FUNCTION;
const isArray = (thing) => Array.isArray(thing);
const size = (arrayOrString) => arrayOrString.length;
const test = (regex, subject) => regex.test(subject);
const getUndefined = () => void 0;

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 arraySum = (array) => arrayReduce(array, (i, j) => i + j, 0);
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 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 numericAggregators = /* @__PURE__ */ mapNew([
  [
    AVG,
    [
      (numbers, length) => arraySum(numbers) / length,
      (metric, add, length) => metric + (add - metric) / (length + 1),
      (metric, remove, length) => metric + (metric - remove) / (length - 1),
      (metric, add, remove, length) => metric + (add - remove) / length,
    ],
  ],
  [
    MAX,
    [
      (numbers) => mathMax(...numbers),
      (metric, add) => mathMax(add, metric),
      (metric, remove) => (remove == metric ? void 0 : metric),
      (metric, add, remove) =>
        remove == metric ? void 0 : mathMax(add, metric),
    ],
  ],
  [
    MIN,
    [
      (numbers) => mathMin(...numbers),
      (metric, add) => mathMin(add, metric),
      (metric, remove) => (remove == metric ? void 0 : metric),
      (metric, add, remove) =>
        remove == metric ? void 0 : mathMin(add, metric),
    ],
  ],
  [
    SUM,
    [
      (numbers) => arraySum(numbers),
      (metric, add) => metric + add,
      (metric, remove) => metric - remove,
      (metric, add, remove) => metric - remove + add,
    ],
  ],
]);
const getAggregateValue = (
  aggregateValue,
  oldLength,
  newValues,
  changedValues,
  aggregators,
  force = false,
) => {
  if (collIsEmpty(newValues)) {
    return void 0;
  }
  const [aggregate, aggregateAdd, aggregateRemove, aggregateReplace] =
    aggregators;
  force ||= isUndefined(aggregateValue);
  collForEach(changedValues, ([oldValue, newValue]) => {
    if (!force) {
      aggregateValue = isUndefined(oldValue)
        ? aggregateAdd?.(aggregateValue, newValue, oldLength++)
        : isUndefined(newValue)
          ? aggregateRemove?.(aggregateValue, oldValue, oldLength--)
          : aggregateReplace?.(aggregateValue, newValue, oldValue, oldLength);
      force ||= isUndefined(aggregateValue);
    }
  });
  return force
    ? aggregate(collValues(newValues), collSize(newValues))
    : aggregateValue;
};

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 createMetrics = getCreateFunction((store) => {
  const metricListeners = mapNew();
  const [addListener, callListeners, delListenerImpl] = getListenerFunctions(
    () => metrics,
  );
  const [
    getStore,
    getMetricIds,
    forEachMetric,
    hasMetric,
    getTableId,
    getMetric,
    setMetric,
    ,
    setDefinitionAndListen,
    delDefinition,
    addMetricIdsListener,
    destroy,
  ] = getDefinableFunctions(
    store,
    getUndefined,
    (value) =>
      isNaN(value) ||
      isUndefined(value) ||
      value === true ||
      value === false ||
      value === EMPTY_STRING
        ? void 0
        : value * 1,
    addListener,
    callListeners,
  );
  const setMetricDefinition = (
    metricId,
    tableId,
    aggregate,
    getNumber,
    aggregateAdd,
    aggregateRemove,
    aggregateReplace,
  ) => {
    const aggregators = isFunction(aggregate)
      ? [aggregate, aggregateAdd, aggregateRemove, aggregateReplace]
      : (mapGet(numericAggregators, aggregate) ??
        mapGet(numericAggregators, SUM));
    setDefinitionAndListen(
      metricId,
      tableId,
      (change, changedNumbers, _changedSortKeys, numbers, _sortKeys, force) => {
        const oldMetric = getMetric(metricId);
        const oldLength = collSize(numbers);
        force ||= isUndefined(oldMetric);
        change();
        let newMetric = getAggregateValue(
          oldMetric,
          oldLength,
          numbers,
          changedNumbers,
          aggregators,
          force,
        );
        if (!isFiniteNumber(newMetric)) {
          newMetric = void 0;
        }
        if (newMetric != oldMetric) {
          setMetric(metricId, newMetric);
          callListeners(metricListeners, [metricId], newMetric, oldMetric);
        }
      },
      getRowCellFunction(getNumber, 1),
    );
    return metrics;
  };
  const delMetricDefinition = (metricId) => {
    delDefinition(metricId);
    return metrics;
  };
  const addMetricListener = (metricId, listener) =>
    addListener(listener, metricListeners, [metricId]);
  const delListener = (listenerId) => {
    delListenerImpl(listenerId);
    return metrics;
  };
  const getListenerStats = () => ({
    metric: collSize2(metricListeners),
  });
  const metrics = {
    setMetricDefinition,
    delMetricDefinition,
    getStore,
    getMetricIds,
    forEachMetric,
    hasMetric,
    getTableId,
    getMetric,
    addMetricIdsListener,
    addMetricListener,
    delListener,
    destroy,
    getListenerStats,
  };
  return objFreeze(metrics);
});

export {createMetrics};
