import { assert, assertArray, assertNumber, unreachable } from "../../../shared/src/asserts.js";
import { must } from "../../../shared/src/must.js";
import { skipYields } from "./operator.js";
import "./data.js";
//#region ../zql/src/ivm/view-apply-change.ts
var refCountSymbol = Symbol("rc");
var idSymbol = Symbol("id");
function applyChange(parentEntry, change, schema, relationship, format, withIDs = false) {
	if (schema.isHidden) switch (change.type) {
		case "add":
		case "remove":
			for (const [relationship, children] of Object.entries(change.node.relationships)) {
				const childSchema = must(schema.relationships[relationship]);
				for (const node of skipYields(children())) applyChange(parentEntry, {
					type: change.type,
					node
				}, childSchema, relationship, format, withIDs);
			}
			return;
		case "edit": return;
		case "child": {
			const childSchema = must(schema.relationships[change.child.relationshipName]);
			applyChange(parentEntry, change.child.change, childSchema, relationship, format, withIDs);
			return;
		}
		default: unreachable(change);
	}
	const { singular, relationships: childFormats } = format;
	switch (change.type) {
		case "add": {
			let newEntry;
			if (singular) {
				const oldEntry = parentEntry[relationship];
				if (oldEntry !== void 0) {
					assert(schema.compareRows(oldEntry, change.node.row) === 0, `Singular relationship '${relationship}' should not have multiple rows. You may need to declare this relationship with the \`many\` helper instead of the \`one\` helper in your schema.`);
					oldEntry[refCountSymbol]++;
				} else {
					newEntry = makeNewMetaEntry(change.node.row, schema, withIDs, 1);
					parentEntry[relationship] = newEntry;
				}
			} else newEntry = addToView(change.node.row, getChildEntryList(parentEntry, relationship), schema, withIDs);
			if (newEntry) for (const [relationship, children] of Object.entries(change.node.relationships)) {
				const childSchema = must(schema.relationships[relationship]);
				const childFormat = childFormats[relationship];
				if (childFormat === void 0) continue;
				const newView = childFormat.singular ? void 0 : [];
				newEntry[relationship] = newView;
				for (const node of skipYields(children())) applyChange(newEntry, {
					type: "add",
					node
				}, childSchema, relationship, childFormat, withIDs);
			}
			break;
		}
		case "remove":
			if (singular) {
				const oldEntry = parentEntry[relationship];
				assert(oldEntry !== void 0, "node does not exist");
				if (oldEntry[refCountSymbol] === 1) parentEntry[relationship] = void 0;
				oldEntry[refCountSymbol]--;
			} else removeAndUpdateRefCount(getChildEntryList(parentEntry, relationship), change.node.row, schema.compareRows);
			break;
		case "child": {
			let existing;
			if (singular) existing = getSingularEntry(parentEntry, relationship);
			else {
				const view = getChildEntryList(parentEntry, relationship);
				const bsResult = binarySearch(view, change.node.row, schema.compareRows);
				assert(bsResult >= 0, "node does not exist");
				existing = view[bsResult];
			}
			const childSchema = must(schema.relationships[change.child.relationshipName]);
			const childFormat = format.relationships[change.child.relationshipName];
			if (childFormat !== void 0) applyChange(existing, change.child.change, childSchema, change.child.relationshipName, childFormat, withIDs);
			break;
		}
		case "edit":
			if (singular) {
				const existing = parentEntry[relationship];
				assertMetaEntry(existing);
				applyEdit(existing, change, schema, withIDs);
			} else {
				const view = getChildEntryList(parentEntry, relationship);
				if (schema.compareRows(change.oldNode.row, change.node.row) !== 0) {
					const oldBsResult = binarySearch(view, change.oldNode.row, schema.compareRows);
					assert(oldBsResult >= 0, "old node does not exist");
					const oldPos = oldBsResult;
					const oldEntry = view[oldPos];
					const newBsResult = binarySearch(view, change.node.row, schema.compareRows);
					const found = newBsResult >= 0;
					const pos = found ? newBsResult : ~newBsResult;
					if (oldEntry[refCountSymbol] === 1 && (pos === oldPos || pos - 1 === oldPos)) applyEdit(oldEntry, change, schema, withIDs);
					else {
						oldEntry[refCountSymbol]--;
						let adjustedPos = pos;
						if (oldEntry[refCountSymbol] === 0) {
							view.splice(oldPos, 1);
							adjustedPos = oldPos < pos ? pos - 1 : pos;
						}
						let entryToEdit;
						if (found) entryToEdit = view[adjustedPos];
						else {
							view.splice(adjustedPos, 0, oldEntry);
							entryToEdit = oldEntry;
							if (oldEntry[refCountSymbol] > 0) view[oldPos] = { ...oldEntry };
						}
						entryToEdit[refCountSymbol]++;
						applyEdit(entryToEdit, change, schema, withIDs);
					}
				} else {
					const bsResult = binarySearch(view, change.oldNode.row, schema.compareRows);
					assert(bsResult >= 0, "node does not exist");
					applyEdit(view[bsResult], change, schema, withIDs);
				}
			}
			break;
		default: unreachable(change);
	}
}
function applyEdit(existing, change, schema, withIDs) {
	Object.assign(existing, change.node.row);
	if (withIDs) existing[idSymbol] = makeID(change.node.row, schema);
}
function addToView(row, view, schema, withIDs) {
	const bsResult = binarySearch(view, row, schema.compareRows);
	if (bsResult >= 0) {
		view[bsResult][refCountSymbol]++;
		return;
	}
	const pos = ~bsResult;
	const newEntry = makeNewMetaEntry(row, schema, withIDs, 1);
	view.splice(pos, 0, newEntry);
	return newEntry;
}
function removeAndUpdateRefCount(view, row, compareRows) {
	const bsResult = binarySearch(view, row, compareRows);
	assert(bsResult >= 0, "node does not exist");
	const oldEntry = view[bsResult];
	if (oldEntry[refCountSymbol] === 1) view.splice(bsResult, 1);
	oldEntry[refCountSymbol]--;
	return oldEntry;
}
/**
* Binary search a sorted view.
*
* Returns the index of `target` if found (a non-negative integer), or
* `~insertionIndex` if not found (always negative, following Java's
* `Arrays.binarySearch` convention). To get the insertion index when
* not found use the bitwise NOT: `~result`.
*
* This avoids allocating a `{pos, found}` result object on every call.
*/
function binarySearch(view, target, comparator) {
	let low = 0;
	let high = view.length - 1;
	while (low <= high) {
		const mid = low + high >>> 1;
		const comparison = comparator(view[mid], target);
		if (comparison < 0) low = mid + 1;
		else if (comparison > 0) high = mid - 1;
		else return mid;
	}
	return ~low;
}
function getChildEntryList(parentEntry, relationship) {
	const view = parentEntry[relationship];
	assertArray(view);
	return view;
}
function assertMetaEntry(v) {
	assertNumber(v[refCountSymbol]);
}
function getSingularEntry(parentEntry, relationship) {
	const e = parentEntry[relationship];
	assertNumber(e[refCountSymbol]);
	return e;
}
function makeNewMetaEntry(row, schema, withIDs, rc) {
	if (withIDs) return {
		...row,
		[refCountSymbol]: rc,
		[idSymbol]: makeID(row, schema)
	};
	return {
		...row,
		[refCountSymbol]: rc
	};
}
function makeID(row, schema) {
	if (schema.primaryKey.length === 1) return JSON.stringify(row[schema.primaryKey[0]]);
	return JSON.stringify(schema.primaryKey.map((k) => row[k]));
}
//#endregion
export { applyChange, idSymbol, refCountSymbol };

//# sourceMappingURL=view-apply-change.js.map