import { unreachable, assert } from "../../../shared/src/asserts.js";
import { BTreeSet } from "../../../shared/src/btree-set.js";
import { hasOwn } from "../../../shared/src/has-own.js";
import { once } from "../../../shared/src/iterables.js";
import { transformFilters, createPredicate } from "../builder/filter.js";
import { assertOrderingIncludesPK } from "../query/complete-ordering.js";
import { constraintMatchesRow, primaryKeyConstraintFromFilters, constraintMatchesPrimaryKey } from "./constraint.js";
import { valuesEqual, makeComparator, compareValues } from "./data.js";
import { filterPush } from "./filter-push.js";
import { skipYields } from "./operator.js";
class MemorySource {
  #tableName;
  #columns;
  #primaryKey;
  #primaryIndexSort;
  #indexes = /* @__PURE__ */ new Map();
  #connections = [];
  #overlay;
  #pushEpoch = 0;
  constructor(tableName, columns, primaryKey, primaryIndexData) {
    this.#tableName = tableName;
    this.#columns = columns;
    this.#primaryKey = primaryKey;
    this.#primaryIndexSort = primaryKey.map((k) => [k, "asc"]);
    const comparator = makeBoundComparator(this.#primaryIndexSort);
    this.#indexes.set(JSON.stringify(this.#primaryIndexSort), {
      comparator,
      data: primaryIndexData ?? new BTreeSet(comparator),
      usedBy: /* @__PURE__ */ new Set()
    });
  }
  get tableSchema() {
    return {
      name: this.#tableName,
      columns: this.#columns,
      primaryKey: this.#primaryKey
    };
  }
  fork() {
    const primaryIndex = this.#getPrimaryIndex();
    return new MemorySource(
      this.#tableName,
      this.#columns,
      this.#primaryKey,
      primaryIndex.data.clone()
    );
  }
  get data() {
    return this.#getPrimaryIndex().data;
  }
  #getSchema(connection) {
    return {
      tableName: this.#tableName,
      columns: this.#columns,
      primaryKey: this.#primaryKey,
      sort: connection.sort,
      system: "client",
      relationships: {},
      isHidden: false,
      compareRows: connection.compareRows
    };
  }
  connect(sort, filters, splitEditKeys) {
    const transformedFilters = transformFilters(filters);
    const input = {
      getSchema: () => schema,
      fetch: (req) => this.#fetch(req, connection),
      setOutput: (output) => {
        connection.output = output;
      },
      destroy: () => {
        this.#disconnect(input);
      },
      fullyAppliedFilters: !transformedFilters.conditionsRemoved
    };
    const connection = {
      input,
      output: void 0,
      sort,
      splitEditKeys,
      compareRows: makeComparator(sort),
      filters: transformedFilters.filters ? {
        condition: transformedFilters.filters,
        predicate: createPredicate(transformedFilters.filters)
      } : void 0,
      lastPushedEpoch: 0
    };
    const schema = this.#getSchema(connection);
    assertOrderingIncludesPK(sort, this.#primaryKey);
    this.#connections.push(connection);
    return input;
  }
  #disconnect(input) {
    const idx = this.#connections.findIndex((c) => c.input === input);
    assert(idx !== -1, "Connection not found");
    this.#connections.splice(idx, 1);
  }
  #getPrimaryIndex() {
    const index = this.#indexes.get(JSON.stringify(this.#primaryIndexSort));
    assert(index, "Primary index not found");
    return index;
  }
  #getOrCreateIndex(sort, usedBy) {
    const key = JSON.stringify(sort);
    const index = this.#indexes.get(key);
    if (index) {
      index.usedBy.add(usedBy);
      return index;
    }
    const comparator = makeBoundComparator(sort);
    const data = new BTreeSet(comparator);
    for (const row of this.#getPrimaryIndex().data) {
      data.add(row);
    }
    const newIndex = { comparator, data, usedBy: /* @__PURE__ */ new Set([usedBy]) };
    this.#indexes.set(key, newIndex);
    return newIndex;
  }
  // For unit testing that we correctly clean up indexes.
  getIndexKeys() {
    return [...this.#indexes.keys()];
  }
  *#fetch(req, conn) {
    const { sort: requestedSort, compareRows } = conn;
    const connectionComparator = (r1, r2) => compareRows(r1, r2) * (req.reverse ? -1 : 1);
    const pkConstraint = primaryKeyConstraintFromFilters(
      conn.filters?.condition,
      this.#primaryKey
    );
    const fetchOrPkConstraint = pkConstraint ?? req.constraint;
    const indexSort = [];
    if (fetchOrPkConstraint) {
      for (const key of Object.keys(fetchOrPkConstraint)) {
        indexSort.push([key, "asc"]);
      }
    }
    if (this.#primaryKey.length > 1 || !fetchOrPkConstraint || !constraintMatchesPrimaryKey(fetchOrPkConstraint, this.#primaryKey)) {
      indexSort.push(...requestedSort);
    }
    const index = this.#getOrCreateIndex(indexSort, conn);
    const { data, comparator: compare } = index;
    const indexComparator = (r1, r2) => compare(r1, r2) * (req.reverse ? -1 : 1);
    const startAt = req.start?.row;
    let scanStart;
    if (fetchOrPkConstraint) {
      scanStart = {};
      for (const [key, dir] of indexSort) {
        if (hasOwn(fetchOrPkConstraint, key)) {
          scanStart[key] = fetchOrPkConstraint[key];
        } else {
          if (req.reverse) {
            scanStart[key] = dir === "asc" ? maxValue : minValue;
          } else {
            scanStart[key] = dir === "asc" ? minValue : maxValue;
          }
        }
      }
    } else {
      scanStart = startAt;
    }
    const rowsIterable = generateRows(data, scanStart, req.reverse);
    const withOverlay = generateWithOverlay(
      startAt,
      pkConstraint ? once(rowsIterable) : rowsIterable,
      // use `req.constraint` here and not `fetchOrPkConstraint` since `fetchOrPkConstraint` could be the
      // primary key constraint. The primary key constraint comes from filters and is acting as a filter
      // rather than as the fetch constraint.
      req.constraint,
      this.#overlay,
      conn.lastPushedEpoch,
      // Use indexComparator, generateWithOverlayInner has a subtle dependency
      // on this.  Since generateWithConstraint is done after
      // generateWithOverlay, the generator consumed by generateWithOverlayInner
      // does not end when the constraint stops matching and so the final
      // check to yield an add overlay if not yet yielded is not reached.
      // However, using the indexComparator the add overlay will be less than
      // the first row that does not match the constraint, and so any
      // not yet yielded add overlay will be yielded when the first row
      // not matching the constraint is reached.
      indexComparator,
      conn.filters?.predicate
    );
    const withConstraint = generateWithConstraint(
      skipYields(
        generateWithStart(withOverlay, req.start, connectionComparator)
      ),
      // we use `req.constraint` and not `fetchOrPkConstraint` here because we need to
      // AND the constraint with what could have been the primary key constraint
      req.constraint
    );
    yield* conn.filters ? generateWithFilter(withConstraint, conn.filters.predicate) : withConstraint;
  }
  *push(change) {
    for (const result of this.genPush(change)) {
      if (result === "yield") {
        yield result;
      }
    }
  }
  *genPush(change) {
    const primaryIndex = this.#getPrimaryIndex();
    const { data } = primaryIndex;
    const exists = (row) => data.has(row);
    const setOverlay = (o) => this.#overlay = o;
    const writeChange = (c) => this.#writeChange(c);
    yield* genPushAndWriteWithSplitEdit(
      this.#connections,
      change,
      exists,
      setOverlay,
      writeChange,
      () => ++this.#pushEpoch
    );
  }
  #writeChange(change) {
    for (const { data } of this.#indexes.values()) {
      switch (change.type) {
        case "add": {
          const added = data.add(change.row);
          assert(
            added,
            "MemorySource: add must succeed since row existence was already checked"
          );
          break;
        }
        case "remove": {
          const removed = data.delete(change.row);
          assert(
            removed,
            "MemorySource: remove must succeed since row existence was already checked"
          );
          break;
        }
        case "edit": {
          const removed = data.delete(change.oldRow);
          assert(
            removed,
            "MemorySource: edit remove must succeed since row existence was already checked"
          );
          data.add(change.row);
          break;
        }
        default:
          unreachable();
      }
    }
  }
}
function* generateWithConstraint(it, constraint) {
  for (const node of it) {
    if (constraint && !constraintMatchesRow(constraint, node.row)) {
      break;
    }
    yield node;
  }
}
function* generateWithFilter(it, filter) {
  for (const node of it) {
    if (filter(node.row)) {
      yield node;
    }
  }
}
function* genPushAndWriteWithSplitEdit(connections, change, exists, setOverlay, writeChange, getNextEpoch) {
  let shouldSplitEdit = false;
  if (change.type === "edit") {
    for (const { splitEditKeys } of connections) {
      if (splitEditKeys) {
        for (const key of splitEditKeys) {
          if (!valuesEqual(change.row[key], change.oldRow[key])) {
            shouldSplitEdit = true;
            break;
          }
        }
      }
    }
  }
  if (change.type === "edit" && shouldSplitEdit) {
    yield* genPushAndWrite(
      connections,
      {
        type: "remove",
        row: change.oldRow
      },
      exists,
      setOverlay,
      writeChange,
      getNextEpoch()
    );
    yield* genPushAndWrite(
      connections,
      {
        type: "add",
        row: change.row
      },
      exists,
      setOverlay,
      writeChange,
      getNextEpoch()
    );
  } else {
    yield* genPushAndWrite(
      connections,
      change,
      exists,
      setOverlay,
      writeChange,
      getNextEpoch()
    );
  }
}
function* genPushAndWrite(connections, change, exists, setOverlay, writeChange, pushEpoch) {
  for (const x of genPush(connections, change, exists, setOverlay, pushEpoch)) {
    yield x;
  }
  writeChange(change);
}
function* genPush(connections, change, exists, setOverlay, pushEpoch) {
  switch (change.type) {
    case "add":
      assert(
        !exists(change.row),
        () => `Row already exists ${stringify(change)}`
      );
      break;
    case "remove":
      assert(exists(change.row), () => `Row not found ${stringify(change)}`);
      break;
    case "edit":
      assert(exists(change.oldRow), () => `Row not found ${stringify(change)}`);
      break;
    default:
      unreachable();
  }
  for (const conn of connections) {
    const { output, filters, input } = conn;
    if (output) {
      conn.lastPushedEpoch = pushEpoch;
      setOverlay({ epoch: pushEpoch, change });
      const outputChange = change.type === "edit" ? {
        type: change.type,
        oldNode: {
          row: change.oldRow,
          relationships: {}
        },
        node: {
          row: change.row,
          relationships: {}
        }
      } : {
        type: change.type,
        node: {
          row: change.row,
          relationships: {}
        }
      };
      yield* filterPush(outputChange, output, input, filters?.predicate);
      yield void 0;
    }
  }
  setOverlay(void 0);
}
function* generateWithStart(nodes, start, compare) {
  if (!start) {
    yield* nodes;
    return;
  }
  let started = false;
  for (const node of nodes) {
    if (node === "yield") {
      yield node;
      continue;
    }
    if (!started) {
      if (start.basis === "at") {
        if (compare(node.row, start.row) >= 0) {
          started = true;
        }
      } else if (start.basis === "after") {
        if (compare(node.row, start.row) > 0) {
          started = true;
        }
      }
    }
    if (started) {
      yield node;
    }
  }
}
function* generateWithOverlay(startAt, rows, constraint, overlay, lastPushedEpoch, compare, filterPredicate) {
  let overlayToApply = void 0;
  if (overlay && lastPushedEpoch >= overlay.epoch) {
    overlayToApply = overlay;
  }
  const overlays = computeOverlays(
    startAt,
    constraint,
    overlayToApply,
    compare,
    filterPredicate
  );
  yield* generateWithOverlayInner(rows, overlays, compare);
}
function computeOverlays(startAt, constraint, overlay, compare, filterPredicate) {
  let overlays = {
    add: void 0,
    remove: void 0
  };
  switch (overlay?.change.type) {
    case "add":
      overlays = {
        add: overlay.change.row,
        remove: void 0
      };
      break;
    case "remove":
      overlays = {
        add: void 0,
        remove: overlay.change.row
      };
      break;
    case "edit":
      overlays = {
        add: overlay.change.row,
        remove: overlay.change.oldRow
      };
      break;
  }
  if (startAt) {
    overlays = overlaysForStartAt(overlays, startAt, compare);
  }
  if (constraint) {
    overlays = overlaysForConstraint(overlays, constraint);
  }
  if (filterPredicate) {
    overlays = overlaysForFilterPredicate(overlays, filterPredicate);
  }
  return overlays;
}
function overlaysForStartAt({ add, remove }, startAt, compare) {
  const undefinedIfBeforeStartAt = (row) => row === void 0 || compare(row, startAt) < 0 ? void 0 : row;
  return {
    add: undefinedIfBeforeStartAt(add),
    remove: undefinedIfBeforeStartAt(remove)
  };
}
function overlaysForConstraint({ add, remove }, constraint) {
  const undefinedIfDoesntMatchConstraint = (row) => row === void 0 || !constraintMatchesRow(constraint, row) ? void 0 : row;
  return {
    add: undefinedIfDoesntMatchConstraint(add),
    remove: undefinedIfDoesntMatchConstraint(remove)
  };
}
function overlaysForFilterPredicate({ add, remove }, filterPredicate) {
  const undefinedIfDoesntMatchFilter = (row) => row === void 0 || !filterPredicate(row) ? void 0 : row;
  return {
    add: undefinedIfDoesntMatchFilter(add),
    remove: undefinedIfDoesntMatchFilter(remove)
  };
}
function* generateWithOverlayInner(rowIterator, overlays, compare) {
  let addOverlayYielded = false;
  let removeOverlaySkipped = false;
  for (const row of rowIterator) {
    if (!addOverlayYielded && overlays.add) {
      const cmp = compare(overlays.add, row);
      if (cmp < 0) {
        addOverlayYielded = true;
        yield { row: overlays.add, relationships: {} };
      }
    }
    if (!removeOverlaySkipped && overlays.remove) {
      const cmp = compare(overlays.remove, row);
      if (cmp === 0) {
        removeOverlaySkipped = true;
        continue;
      }
    }
    yield { row, relationships: {} };
  }
  if (!addOverlayYielded && overlays.add) {
    yield { row: overlays.add, relationships: {} };
  }
}
const minValue = Symbol("min-value");
const maxValue = Symbol("max-value");
function makeBoundComparator(sort) {
  return (a, b) => {
    for (const entry of sort) {
      const key = entry[0];
      const cmp = compareBounds(a[key], b[key]);
      if (cmp !== 0) {
        return entry[1] === "asc" ? cmp : -cmp;
      }
    }
    return 0;
  };
}
function compareBounds(a, b) {
  if (a === b) {
    return 0;
  }
  if (a === minValue) {
    return -1;
  }
  if (b === minValue) {
    return 1;
  }
  if (a === maxValue) {
    return 1;
  }
  if (b === maxValue) {
    return -1;
  }
  return compareValues(a, b);
}
function* generateRows(data, scanStart, reverse) {
  yield* data[reverse ? "valuesFromReversed" : "valuesFrom"](
    scanStart
  );
}
function stringify(change) {
  return JSON.stringify(
    change,
    (_, v) => typeof v === "bigint" ? v.toString() : v
  );
}
export {
  MemorySource,
  genPushAndWriteWithSplitEdit,
  generateWithOverlay,
  generateWithOverlayInner,
  generateWithStart,
  overlaysForConstraint as overlaysForConstraintForTest,
  overlaysForStartAt as overlaysForStartAtForTest,
  stringify
};
//# sourceMappingURL=memory-source.js.map
