import { assert, assertArray, assertNumber, assertString } from "../../../shared/src/asserts.js";
import { isProd } from "../../../shared/src/config.js";
import { assertJSONValue } from "../../../shared/src/json.js";
import "../config.js";
import { assertDeepFrozen, deepFreeze } from "../frozen-json.js";
import { emptyHash, newRandomHash } from "../hash.js";
import { binarySearch as binarySearch$1 } from "../../../shared/src/binary-search.js";
import { joinIterables } from "../../../shared/src/iterables.js";
import { compareUTF8 } from "compare-utf8";
//#region ../replicache/src/btree/node.ts
function makeNodeChunkData(level, entries, formatVersion) {
	return deepFreeze([level, formatVersion >= 7 ? entries : entries.map((e) => e.slice(0, 2))]);
}
/**
* Finds the leaf where a key is (if present) or where it should go if not
* present.
*/
async function findLeaf(key, hash, source, expectedRootHash) {
	const node = await source.getNode(hash);
	if (expectedRootHash !== source.rootHash) return findLeaf(key, source.rootHash, source, source.rootHash);
	if (isDataNodeImpl(node)) return node;
	const { entries } = node;
	let i = binarySearch(key, entries);
	if (i === entries.length) i--;
	const entry = entries[i];
	return findLeaf(key, entry[1], source, expectedRootHash);
}
/**
* Does a binary search over entries
*
* If the key found then the return value is the index it was found at.
*
* If the key was *not* found then the return value is the index where it should
* be inserted at
*/
function binarySearch(key, entries) {
	return binarySearch$1(entries.length, (i) => compareUTF8(key, entries[i][0]));
}
function binarySearchFound(i, entries, key) {
	return i !== entries.length && entries[i][0] === key;
}
function parseBTreeNode(v, formatVersion, getSizeOfEntry) {
	if (isProd && formatVersion >= 7) return v;
	assertArray(v);
	assertDeepFrozen(v);
	assert(v.length >= 2, "Expected node array to have at least 2 elements");
	const [level, entries] = v;
	assertNumber(level);
	assertArray(entries);
	const f = level > 0 ? assertString : assertJSONValue;
	if (formatVersion >= 7) {
		for (const e of entries) assertEntry(e, f);
		return v;
	}
	return [level, entries.map((e) => convertNonV7Entry(e, f, getSizeOfEntry))];
}
function assertEntry(entry, f) {
	assertArray(entry);
	assert(entry.length >= 3, "Expected entry array to have at least 3 elements");
	assertString(entry[0]);
	f(entry[1]);
	assertNumber(entry[2]);
}
/**
* Converts an entry that was from a format version before V7 to the format
* wanted by V7.
*/
function convertNonV7Entry(entry, f, getSizeOfEntry) {
	assertArray(entry);
	assert(entry.length >= 2, "Expected entry array to have at least 2 elements");
	assertString(entry[0]);
	f(entry[1]);
	const entrySize = getSizeOfEntry(entry[0], entry[1]);
	return [
		entry[0],
		entry[1],
		entrySize
	];
}
var NodeImpl = class {
	entries;
	hash;
	isMutable;
	#childNodeSize = -1;
	constructor(entries, hash, isMutable) {
		this.entries = entries;
		this.hash = hash;
		this.isMutable = isMutable;
	}
	maxKey() {
		return this.entries.at(-1)[0];
	}
	getChildNodeSize(tree) {
		if (this.#childNodeSize !== -1) return this.#childNodeSize;
		let sum = tree.chunkHeaderSize;
		for (const entry of this.entries) sum += entry[2];
		return this.#childNodeSize = sum;
	}
	_updateNode(tree) {
		this.#childNodeSize = -1;
		tree.updateNode(this);
	}
};
function toChunkData(node, formatVersion) {
	return makeNodeChunkData(node.level, node.entries, formatVersion);
}
var DataNodeImpl = class extends NodeImpl {
	level = 0;
	set(key, value, entrySize, tree) {
		let deleteCount;
		const i = binarySearch(key, this.entries);
		if (!binarySearchFound(i, this.entries, key)) deleteCount = 0;
		else deleteCount = 1;
		return Promise.resolve(this.#splice(tree, i, deleteCount, [
			key,
			value,
			entrySize
		]));
	}
	#splice(tree, start, deleteCount, ...items) {
		if (this.isMutable) {
			this.entries.splice(start, deleteCount, ...items);
			this._updateNode(tree);
			return this;
		}
		const entries = readonlySplice(this.entries, start, deleteCount, ...items);
		return tree.newDataNodeImpl(entries);
	}
	del(key, tree) {
		const i = binarySearch(key, this.entries);
		if (!binarySearchFound(i, this.entries, key)) return Promise.resolve(this);
		return Promise.resolve(this.#splice(tree, i, 1));
	}
	async *keys(_tree) {
		for (const entry of this.entries) yield entry[0];
	}
	async *entriesIter(_tree) {
		for (const entry of this.entries) yield entry;
	}
};
function readonlySplice(array, start, deleteCount, ...items) {
	const arr = array.slice(0, start);
	for (let i = 0; i < items.length; i++) arr.push(items[i]);
	for (let i = start + deleteCount; i < array.length; i++) arr.push(array[i]);
	return arr;
}
var InternalNodeImpl = class InternalNodeImpl extends NodeImpl {
	level;
	constructor(entries, hash, level, isMutable) {
		super(entries, hash, isMutable);
		this.level = level;
	}
	async set(key, value, entrySize, tree) {
		let i = binarySearch(key, this.entries);
		if (i === this.entries.length) i--;
		const childHash = this.entries[i][1];
		const childNode = await (await tree.getNode(childHash)).set(key, value, entrySize, tree);
		const childNodeSize = childNode.getChildNodeSize(tree);
		if (childNodeSize > tree.maxSize || childNodeSize < tree.minSize) return this.#mergeAndPartition(tree, i, childNode);
		const newEntry = createNewInternalEntryForNode(childNode, tree.getEntrySize);
		return this.#replaceChild(tree, i, newEntry);
	}
	/**
	* This merges the child node entries with previous or next sibling and then
	* partitions the merged entries.
	*/
	async #mergeAndPartition(tree, i, childNode) {
		const level = this.level - 1;
		const thisEntries = this.entries;
		let values;
		let startIndex;
		let removeCount;
		if (i > 0) {
			const hash = thisEntries[i - 1][1];
			values = joinIterables((await tree.getNode(hash)).entries, childNode.entries);
			startIndex = i - 1;
			removeCount = 2;
		} else if (i < thisEntries.length - 1) {
			const hash = thisEntries[i + 1][1];
			const nextSibling = await tree.getNode(hash);
			values = joinIterables(childNode.entries, nextSibling.entries);
			startIndex = i;
			removeCount = 2;
		} else {
			values = childNode.entries;
			startIndex = i;
			removeCount = 1;
		}
		const partitions = partition(values, (value) => value[2], tree.minSize - tree.chunkHeaderSize, tree.maxSize - tree.chunkHeaderSize);
		const newEntries = [];
		for (const entries of partitions) {
			const newHashEntry = createNewInternalEntryForNode(tree.newNodeImpl(entries, level), tree.getEntrySize);
			newEntries.push(newHashEntry);
		}
		if (this.isMutable) {
			this.entries.splice(startIndex, removeCount, ...newEntries);
			this._updateNode(tree);
			return this;
		}
		const entries = readonlySplice(thisEntries, startIndex, removeCount, ...newEntries);
		return tree.newInternalNodeImpl(entries, this.level);
	}
	#replaceChild(tree, index, newEntry) {
		if (this.isMutable) {
			this.entries.splice(index, 1, newEntry);
			this._updateNode(tree);
			return this;
		}
		const entries = readonlySplice(this.entries, index, 1, newEntry);
		return tree.newInternalNodeImpl(entries, this.level);
	}
	async del(key, tree) {
		const i = binarySearch(key, this.entries);
		if (i === this.entries.length) return this;
		const childHash = this.entries[i][1];
		const oldChildNode = await tree.getNode(childHash);
		const oldHash = oldChildNode.hash;
		const childNode = await oldChildNode.del(key, tree);
		if (childNode.hash === oldHash) return this;
		if (childNode.entries.length === 0) {
			const entries = readonlySplice(this.entries, i, 1);
			return tree.newInternalNodeImpl(entries, this.level);
		}
		if (i === 0 && this.entries.length === 1) return childNode;
		if (childNode.getChildNodeSize(tree) > tree.minSize) {
			const entry = createNewInternalEntryForNode(childNode, tree.getEntrySize);
			return this.#replaceChild(tree, i, entry);
		}
		return this.#mergeAndPartition(tree, i, childNode);
	}
	async *keys(tree) {
		for (const entry of this.entries) yield* (await tree.getNode(entry[1])).keys(tree);
	}
	async *entriesIter(tree) {
		for (const entry of this.entries) yield* (await tree.getNode(entry[1])).entriesIter(tree);
	}
	getChildren(start, length, tree) {
		const ps = [];
		for (let i = start; i < length && i < this.entries.length; i++) ps.push(tree.getNode(this.entries[i][1]));
		return Promise.all(ps);
	}
	async getCompositeChildren(start, length, tree) {
		const { level } = this;
		if (length === 0) return new InternalNodeImpl([], newRandomHash(), level - 1, true);
		const output = await this.getChildren(start, start + length, tree);
		if (level > 1) {
			const entries = [];
			for (const child of output) entries.push(...child.entries);
			return new InternalNodeImpl(entries, newRandomHash(), level - 1, true);
		}
		assert(level === 1, "Expected level to be 1");
		const entries = [];
		for (const child of output) entries.push(...child.entries);
		return new DataNodeImpl(entries, newRandomHash(), true);
	}
};
function newNodeImpl(entries, hash, level, isMutable) {
	if (level === 0) return new DataNodeImpl(entries, hash, isMutable);
	return new InternalNodeImpl(entries, hash, level, isMutable);
}
function isDataNodeImpl(node) {
	return node.level === 0;
}
function partition(values, getSizeOfEntry, min, max) {
	const partitions = [];
	const sizes = [];
	let sum = 0;
	let accum = [];
	for (const value of values) {
		const size = getSizeOfEntry(value);
		if (size >= max) {
			if (accum.length > 0) {
				partitions.push(accum);
				sizes.push(sum);
			}
			partitions.push([value]);
			sizes.push(size);
			sum = 0;
			accum = [];
		} else if (sum + size >= min) {
			accum.push(value);
			partitions.push(accum);
			sizes.push(sum + size);
			sum = 0;
			accum = [];
		} else {
			sum += size;
			accum.push(value);
		}
	}
	if (sum > 0) if (sizes.length > 0 && sum + sizes.at(-1) <= max) partitions.at(-1).push(...accum);
	else partitions.push(accum);
	return partitions;
}
var emptyDataNode = makeNodeChunkData(0, [], 7);
var emptyDataNodeImpl = new DataNodeImpl([], emptyHash, false);
function createNewInternalEntryForNode(node, getSizeOfEntry) {
	const key = node.maxKey();
	const value = node.hash;
	return [
		key,
		value,
		getSizeOfEntry(key, value)
	];
}
//#endregion
export { DataNodeImpl, InternalNodeImpl, binarySearch, binarySearchFound, createNewInternalEntryForNode, emptyDataNode, emptyDataNodeImpl, findLeaf, isDataNodeImpl, newNodeImpl, parseBTreeNode, partition, toChunkData };

//# sourceMappingURL=node.js.map