import { assert, unreachable } from "../../../shared/src/asserts.js";
import { binarySearch } from "../../../shared/src/binary-search.js";
import { emptyArray } from "../../../shared/src/sentinels.js";
import { throwOutput } from "./operator.js";
import { constraintsAreCompatible } from "./constraint.js";
import { buildJoinConstraint, generateWithOverlayNoYield, isJoinMatch, rowEqualsForCompoundKey } from "./join-utils.js";
//#region ../zql/src/ivm/flipped-join.ts
/**
* An *inner* join which fetches nodes from its child input first and then
* fetches their related nodes from its parent input.  Output nodes are the
* nodes from parent input (in parent input order), which have at least one
* related child.  These output nodes have a new relationship added to them,
* which has the name `relationshipName`. The value of the relationship is a
* stream of related nodes from the child input (in child input order).
*/
var FlippedJoin = class {
	#parent;
	#child;
	#parentKey;
	#childKey;
	#relationshipName;
	#schema;
	#output = throwOutput;
	#inprogressChildChange;
	constructor({ parent, child, parentKey, childKey, relationshipName, hidden, system }) {
		assert(parent !== child, "Parent and child must be different operators");
		assert(parentKey.length === childKey.length, "The parentKey and childKey keys must have same length");
		this.#parent = parent;
		this.#child = child;
		this.#parentKey = parentKey;
		this.#childKey = childKey;
		this.#relationshipName = relationshipName;
		const parentSchema = parent.getSchema();
		const childSchema = child.getSchema();
		this.#schema = {
			...parentSchema,
			relationships: {
				...parentSchema.relationships,
				[relationshipName]: {
					...childSchema,
					isHidden: hidden,
					system
				}
			}
		};
		parent.setOutput({ push: (change) => this.#pushParent(change) });
		child.setOutput({ push: (change) => this.#pushChild(change) });
	}
	destroy() {
		this.#child.destroy();
		this.#parent.destroy();
	}
	setOutput(output) {
		this.#output = output;
	}
	getSchema() {
		return this.#schema;
	}
	*fetch(req) {
		const childConstraint = {};
		let hasChildConstraint = false;
		if (req.constraint) for (const [key, value] of Object.entries(req.constraint)) {
			const index = this.#parentKey.indexOf(key);
			if (index !== -1) {
				hasChildConstraint = true;
				childConstraint[this.#childKey[index]] = value;
			}
		}
		const childNodes = [];
		for (const node of this.#child.fetch(hasChildConstraint ? { constraint: childConstraint } : {})) {
			if (node === "yield") {
				yield node;
				continue;
			}
			childNodes.push(node);
		}
		if (this.#inprogressChildChange?.change.type === "remove") {
			const removedNode = this.#inprogressChildChange.change.node;
			const compare = this.#child.getSchema().compareRows;
			const insertPos = binarySearch(childNodes.length, (i) => compare(removedNode.row, childNodes[i].row));
			childNodes.splice(insertPos, 0, removedNode);
		}
		const parentIterators = [];
		let threw = false;
		try {
			for (const childNode of childNodes) {
				const constraintFromChild = buildJoinConstraint(childNode.row, this.#childKey, this.#parentKey);
				if (!constraintFromChild || req.constraint && !constraintsAreCompatible(constraintFromChild, req.constraint)) parentIterators.push(emptyArray[Symbol.iterator]());
				else {
					const iterator = this.#parent.fetch({
						...req,
						constraint: {
							...req.constraint,
							...constraintFromChild
						}
					})[Symbol.iterator]();
					parentIterators.push(iterator);
				}
			}
			const nextParentNodes = [];
			for (let i = 0; i < parentIterators.length; i++) {
				const iter = parentIterators[i];
				let result = iter.next();
				while (!result.done && result.value === "yield") {
					yield result.value;
					result = iter.next();
				}
				nextParentNodes[i] = result.done ? null : result.value;
			}
			while (true) {
				let minParentNode = null;
				let minParentNodeChildIndexes = [];
				for (let i = 0; i < nextParentNodes.length; i++) {
					const parentNode = nextParentNodes[i];
					if (parentNode === null) continue;
					if (minParentNode === null) {
						minParentNode = parentNode;
						minParentNodeChildIndexes.push(i);
					} else {
						const compareResult = this.#schema.compareRows(parentNode.row, minParentNode.row) * (req.reverse ? -1 : 1);
						if (compareResult === 0) minParentNodeChildIndexes.push(i);
						else if (compareResult < 0) {
							minParentNode = parentNode;
							minParentNodeChildIndexes = [i];
						}
					}
				}
				if (minParentNode === null) return;
				const relatedChildNodes = [];
				for (const minParentNodeChildIndex of minParentNodeChildIndexes) {
					relatedChildNodes.push(childNodes[minParentNodeChildIndex]);
					const iter = parentIterators[minParentNodeChildIndex];
					let result = iter.next();
					while (!result.done && result.value === "yield") {
						yield result.value;
						result = iter.next();
					}
					nextParentNodes[minParentNodeChildIndex] = result.done ? null : result.value;
				}
				let overlaidRelatedChildNodes = relatedChildNodes;
				if (this.#inprogressChildChange && this.#inprogressChildChange.position && isJoinMatch(this.#inprogressChildChange.change.node.row, this.#childKey, minParentNode.row, this.#parentKey)) {
					const hasInprogressChildChangeBeenPushedForMinParentNode = this.#parent.getSchema().compareRows(minParentNode.row, this.#inprogressChildChange.position) <= 0;
					if (this.#inprogressChildChange.change.type === "remove") {
						if (hasInprogressChildChangeBeenPushedForMinParentNode) overlaidRelatedChildNodes = relatedChildNodes.filter((n) => n !== this.#inprogressChildChange?.change.node);
					} else if (!hasInprogressChildChangeBeenPushedForMinParentNode) overlaidRelatedChildNodes = [...generateWithOverlayNoYield(relatedChildNodes, this.#inprogressChildChange.change, this.#child.getSchema())];
				}
				if (overlaidRelatedChildNodes.length > 0) yield {
					...minParentNode,
					relationships: {
						...minParentNode.relationships,
						[this.#relationshipName]: () => overlaidRelatedChildNodes
					}
				};
			}
		} catch (e) {
			threw = true;
			for (const iter of parentIterators) try {
				iter.throw?.(e);
			} catch (_cleanupError) {}
			throw e;
		} finally {
			if (!threw) for (const iter of parentIterators) try {
				iter.return?.();
			} catch (_cleanupError) {}
		}
	}
	*#pushChild(change) {
		switch (change.type) {
			case "add":
			case "remove":
				yield* this.#pushChildChange(change);
				break;
			case "edit":
				assert(rowEqualsForCompoundKey(change.oldNode.row, change.node.row, this.#childKey), `Child edit must not change relationship.`);
				yield* this.#pushChildChange(change, true);
				break;
			case "child":
				yield* this.#pushChildChange(change, true);
				break;
		}
	}
	*#pushChildChange(change, exists) {
		this.#inprogressChildChange = {
			change,
			position: void 0
		};
		try {
			const constraint = buildJoinConstraint(change.node.row, this.#childKey, this.#parentKey);
			const parentNodeStream = constraint ? this.#parent.fetch({ constraint }) : [];
			for (const parentNode of parentNodeStream) {
				if (parentNode === "yield") {
					yield "yield";
					continue;
				}
				this.#inprogressChildChange = {
					change,
					position: parentNode.row
				};
				const childNodeStream = () => {
					const constraint = buildJoinConstraint(parentNode.row, this.#parentKey, this.#childKey);
					return constraint ? this.#child.fetch({ constraint }) : [];
				};
				if (!exists) for (const childNode of childNodeStream()) {
					if (childNode === "yield") {
						yield "yield";
						continue;
					}
					if (this.#child.getSchema().compareRows(childNode.row, change.node.row) !== 0) {
						exists = true;
						break;
					}
				}
				if (exists) yield* this.#output.push({
					type: "child",
					node: {
						...parentNode,
						relationships: {
							...parentNode.relationships,
							[this.#relationshipName]: childNodeStream
						}
					},
					child: {
						relationshipName: this.#relationshipName,
						change
					}
				}, this);
				else yield* this.#output.push({
					...change,
					node: {
						...parentNode,
						relationships: {
							...parentNode.relationships,
							[this.#relationshipName]: () => [change.node]
						}
					}
				}, this);
			}
		} finally {
			this.#inprogressChildChange = void 0;
		}
	}
	*#pushParent(change) {
		const childNodeStream = (node) => () => {
			const constraint = buildJoinConstraint(node.row, this.#parentKey, this.#childKey);
			return constraint ? this.#child.fetch({ constraint }) : [];
		};
		const flip = (node) => ({
			...node,
			relationships: {
				...node.relationships,
				[this.#relationshipName]: childNodeStream(node)
			}
		});
		let hasRelatedChild = false;
		for (const node of childNodeStream(change.node)()) if (node === "yield") {
			yield "yield";
			continue;
		} else {
			hasRelatedChild = true;
			break;
		}
		if (!hasRelatedChild) return;
		switch (change.type) {
			case "add":
			case "remove":
			case "child":
				yield* this.#output.push({
					...change,
					node: flip(change.node)
				}, this);
				break;
			case "edit":
				assert(rowEqualsForCompoundKey(change.oldNode.row, change.node.row, this.#parentKey), `Parent edit must not change relationship.`);
				yield* this.#output.push({
					type: "edit",
					oldNode: flip(change.oldNode),
					node: flip(change.node)
				}, this);
				break;
			default: unreachable(change);
		}
	}
};
//#endregion
export { FlippedJoin };

//# sourceMappingURL=flipped-join.js.map