import { assert, unreachable } from "../../../shared/src/asserts.js";
import { hasOwn } from "../../../shared/src/has-own.js";
import { once } from "../../../shared/src/iterables.js";
import { must } from "../../../shared/src/must.js";
import { skipYields } from "./operator.js";
import { compareValues, makeComparator, valuesEqual } from "./data.js";
import { filterPush } from "./filter-push.js";
import { constraintMatchesPrimaryKey, constraintMatchesRow, primaryKeyConstraintFromFilters } from "./constraint.js";
import { assertOrderingIncludesPK } from "../query/complete-ordering.js";
import { createPredicate, transformFilters } from "../builder/filter.js";
import { BTreeSet } from "../../../shared/src/btree-set.js";
//#region ../zql/src/ivm/memory-source.ts
/**
* A `MemorySource` is a source that provides data to the pipeline from an
* in-memory data source.
*
* This data is kept in sorted order as downstream pipelines will always expect
* the data they receive from `pull` to be in sorted order.
*/
var MemorySource = 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, unordered) {
		return {
			tableName: this.#tableName,
			columns: this.#columns,
			primaryKey: this.#primaryKey,
			sort: unordered ? void 0 : connection.sort,
			system: "client",
			relationships: {},
			isHidden: false,
			compareRows: connection.compareRows
		};
	}
	connect(sort, filters, splitEditKeys) {
		const transformedFilters = transformFilters(filters);
		const unordered = sort === void 0;
		const internalSort = sort ?? this.#primaryIndexSort;
		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: internalSort,
			splitEditKeys,
			compareRows: makeComparator(internalSort),
			filters: transformedFilters.filters ? {
				condition: transformedFilters.filters,
				predicate: createPredicate(transformedFilters.filters)
			} : void 0,
			lastPushedEpoch: 0
		};
		const schema = this.#getSchema(connection, unordered);
		if (!unordered) assertOrderingIncludesPK(internalSort, 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: new Set([usedBy])
		};
		this.#indexes.set(key, newIndex);
		return newIndex;
	}
	getIndexKeys() {
		return [...this.#indexes.keys()];
	}
	*#fetch(req, conn) {
		const requestedSort = must(conn.sort);
		const { compareRows } = conn;
		const connectionComparator = req.reverse ? (r1, r2) => compareRows(r2, r1) : compareRows;
		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 { data, comparator: compare } = this.#getOrCreateIndex(indexSort, conn);
		const indexComparator = req.reverse ? (r1, r2) => compare(r2, r1) : compare;
		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 withConstraint = generateWithConstraint(skipYields(generateWithStart(generateWithOverlay(startAt, pkConstraint ? once(rowsIterable) : rowsIterable, req.constraint, this.#overlay, conn.lastPushedEpoch, indexComparator, conn.filters?.predicate), req.start, connectionComparator)), 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 { data } = this.#getPrimaryIndex();
		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":
				assert(data.add(change.row), "MemorySource: add must succeed since row existence was already checked");
				break;
			case "remove":
				assert(data.delete(change.row), "MemorySource: remove must succeed since row existence was already checked");
				break;
			case "edit":
				assert(data.delete(change.oldRow), "MemorySource: edit remove must succeed since row existence was already checked");
				data.add(change.row);
				break;
			default: unreachable(change);
		}
	}
};
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(change);
	}
	for (const conn of connections) {
		const { output, filters, input } = conn;
		if (output) {
			conn.lastPushedEpoch = pushEpoch;
			setOverlay({
				epoch: pushEpoch,
				change
			});
			yield* filterPush(change.type === "edit" ? {
				type: change.type,
				oldNode: {
					row: change.oldRow,
					relationships: {}
				},
				node: {
					row: change.row,
					relationships: {}
				}
			} : {
				type: change.type,
				node: {
					row: change.row,
					relationships: {}
				}
			}, 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;
	}
}
/**
* Takes an iterator and overlay.
* Splices the overlay into the iterator at the correct position.
*
* @param startAt - if there is a lower bound to the stream. If the lower bound of the stream
* is above the overlay, the overlay will be skipped.
* @param rows - the stream into which the overlay should be spliced
* @param constraint - constraint that was applied to the rowIterator and should
* also be applied to the overlay.
* @param overlay - the overlay values to splice in
* @param compare - the comparator to use to find the position for the overlay
*/
function* generateWithOverlay(startAt, rows, constraint, overlay, lastPushedEpoch, compare, filterPredicate) {
	let overlayToApply = void 0;
	if (overlay && lastPushedEpoch >= overlay.epoch) overlayToApply = overlay;
	yield* generateWithOverlayInner(rows, computeOverlays(startAt, constraint, overlayToApply, compare, filterPredicate), 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) {
			if (compare(overlays.add, row) < 0) {
				addOverlayYielded = true;
				yield {
					row: overlays.add,
					relationships: {}
				};
			}
		}
		if (!removeOverlaySkipped && overlays.remove) {
			if (compare(overlays.remove, row) === 0) {
				removeOverlaySkipped = true;
				continue;
			}
		}
		yield {
			row,
			relationships: {}
		};
	}
	if (!addOverlayYielded && overlays.add) yield {
		row: overlays.add,
		relationships: {}
	};
}
/**
* Like {@link generateWithOverlay} but for unordered streams.
* No `startAt` or comparator needed. Injects remove/old-edit rows eagerly
* at the start, and suppresses add/new-edit rows inline by PK match.
*/
function* generateWithOverlayUnordered(rows, constraint, overlay, lastPushedEpoch, primaryKey, filterPredicate) {
	let overlayToApply = void 0;
	if (overlay && lastPushedEpoch >= overlay.epoch) overlayToApply = overlay;
	let overlays = {
		add: void 0,
		remove: void 0
	};
	switch (overlayToApply?.change.type) {
		case "add":
			overlays = {
				add: overlayToApply.change.row,
				remove: void 0
			};
			break;
		case "remove":
			overlays = {
				add: void 0,
				remove: overlayToApply.change.row
			};
			break;
		case "edit":
			overlays = {
				add: overlayToApply.change.row,
				remove: overlayToApply.change.oldRow
			};
			break;
	}
	if (constraint) overlays = overlaysForConstraint(overlays, constraint);
	if (filterPredicate) overlays = overlaysForFilterPredicate(overlays, filterPredicate);
	yield* generateWithOverlayInnerUnordered(rows, overlays, primaryKey);
}
function* generateWithOverlayInnerUnordered(rowIterator, overlays, primaryKey) {
	if (overlays.add) yield {
		row: overlays.add,
		relationships: {}
	};
	let removeSkipped = false;
	for (const row of rowIterator) {
		if (!removeSkipped && overlays.remove && rowMatchesPK(overlays.remove, row, primaryKey)) {
			removeSkipped = true;
			continue;
		}
		yield {
			row,
			relationships: {}
		};
	}
}
function rowMatchesPK(a, b, primaryKey) {
	for (const key of primaryKey) if (!valuesEqual(a[key], b[key])) return false;
	return true;
}
var minValue = Symbol("min-value");
var maxValue = Symbol("max-value");
function makeBoundComparator(sort) {
	const len = sort.length;
	const k0 = sort[0][0];
	const a0 = sort[0][1] === "asc";
	const k1 = len > 1 ? sort[1][0] : "";
	const a1 = len > 1 ? sort[1][1] === "asc" : true;
	return (a, b) => {
		const c0 = a0 ? compareBounds(a[k0], b[k0]) : compareBounds(b[k0], a[k0]);
		if (len === 1 || c0 !== 0) return c0;
		const c1 = a1 ? compareBounds(a[k1], b[k1]) : compareBounds(b[k1], a[k1]);
		if (len === 2 || c1 !== 0) return c1;
		for (let i = 2; i < len; i++) {
			const cmp = compareBounds(a[sort[i][0]], b[sort[i][0]]);
			if (cmp !== 0) return sort[i][1] === "asc" ? cmp : -cmp;
		}
		return 0;
	};
}
function compareBounds(a, b) {
	if (a === b) return 0;
	if (typeof a === "symbol") return a === minValue ? -1 : 1;
	if (typeof b === "symbol") return b === minValue ? 1 : -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);
}
//#endregion
export { MemorySource, genPushAndWriteWithSplitEdit, generateWithOverlay, generateWithOverlayUnordered, generateWithStart };

//# sourceMappingURL=memory-source.js.map