import TrieCursorError from "../../errors/TrieCursorError.js";
import TrieStoreError from "../../errors/TrieStoreError.js";
import { encodeDirtyPath } from "./DirtyPageSet.js";
import TrieLeaf from "./TrieLeaf.js";
/**
 * Shared helpers for {@link TrieCursor}.
 *
 * The cursor class itself stays disciplined under the per-method
 * caps by delegating pure transformations (byte packing, leaf
 * mutation, entry comparison) to this module. Nothing here
 * touches the store or mutates cursor state; every function is
 * pure and testable in isolation.
 */
const VALID_NIBBLE_BITS = [1, 2, 4, 6, 8];
export function nibbleBitsOf(n) {
    for (const candidate of VALID_NIBBLE_BITS) {
        if (candidate === n) {
            return candidate;
        }
    }
    throw new TrieCursorError(`TrieCursor geometry nibbleBits must be 1, 2, 4, 6, or 8; received ${String(n)}`, { code: "E_TRIE_CURSOR_INPUT", context: { nibbleBits: n } });
}
export function validateElement(element) {
    if (typeof element !== "string" || element.length === 0) {
        throw new TrieCursorError("TrieCursor element must be a non-empty string", { code: "E_TRIE_CURSOR_INPUT", context: { element } });
    }
}
export function validateDot(dot) {
    if (!isWellFormedDot(dot)) {
        throw new TrieCursorError("TrieCursor dot must be {writerId: non-empty string, counter: positive integer}", { code: "E_TRIE_CURSOR_INPUT", context: { dot } });
    }
}
function isWellFormedDot(dot) {
    if (dot === null || dot === undefined) {
        return false;
    }
    return hasValidWriterId(dot) && hasValidCounter(dot);
}
function hasValidWriterId(dot) {
    return typeof dot.writerId === "string" && dot.writerId.length > 0;
}
function hasValidCounter(dot) {
    return Number.isInteger(dot.counter) && dot.counter > 0;
}
export function suffixOfRouteKey(routeKey, depth, nibbleBits) {
    // Always pack MSB-first so the representation is consistent
    // across byte-aligned and sub-byte-aligned depths. A consistent
    // format lets `shortenEntries` shift by one nibble without
    // changing format mid-flight.
    return packSuffixMsbFirst(routeKey, depth, nibbleBits);
}
function packSuffixMsbFirst(routeKey, depth, nibbleBits) {
    const maxDepth = Math.floor(256 / nibbleBits);
    const nibblesLeft = maxDepth - depth;
    const byteCount = Math.ceil((nibblesLeft * nibbleBits) / 8);
    const out = new Uint8Array(byteCount);
    for (let i = 0; i < nibblesLeft; i += 1) {
        writeNibbleAt({ out, slot: i, nibbleBits, value: routeKey.nibbleAt(depth + i, nibbleBits) });
    }
    return out;
}
function writeNibbleAt(args) {
    const bitOffset = args.slot * args.nibbleBits;
    for (let bitIndex = 0; bitIndex < args.nibbleBits; bitIndex += 1) {
        const bit = (args.value >>> (args.nibbleBits - bitIndex - 1)) & 1;
        writeBitMsbFirst(args.out, bitOffset + bitIndex, bit);
    }
}
function writeBitMsbFirst(out, bitOffset, bit) {
    const byteIndex = Math.floor(bitOffset / 8);
    const byte = out[byteIndex] ?? 0;
    const bitInByte = bitOffset % 8;
    const mask = 1 << (7 - bitInByte);
    out[byteIndex] = bit === 1 ? byte | mask : byte & ~mask;
}
export function upsertIntoLeaf(args) {
    const suffix = suffixOfRouteKey(args.routeKey, args.leafDepth, args.nibbleBits);
    const entries = [...args.leaf.entries()];
    const existingIndex = args.leaf.binarySearch(suffix);
    if (existingIndex !== -1) {
        return upsertAtExistingIndex({ entries, existingIndex, args });
    }
    const insertAt = findInsertIndex(entries, suffix);
    entries.splice(insertAt, 0, makeEntry(suffix, args.element, new Set([args.encodedDot])));
    return new TrieLeaf(entries, args.leaf.geometry);
}
function upsertAtExistingIndex(args) {
    const existing = args.entries[args.existingIndex];
    if (existing === undefined) {
        return args.args.leaf;
    }
    if (existing.element !== args.args.element) {
        return upsertOnSuffixCollision(args);
    }
    if (existing.tombstonedDots.has(args.args.encodedDot)) {
        return args.args.leaf;
    }
    const nextDots = new Set(existing.dots);
    nextDots.add(args.args.encodedDot);
    args.entries[args.existingIndex] = {
        routeKeySuffix: existing.routeKeySuffix,
        element: existing.element,
        dots: nextDots,
        tombstonedDots: existing.tombstonedDots,
    };
    return new TrieLeaf(args.entries, args.args.leaf.geometry);
}
function upsertOnSuffixCollision(args) {
    const suffix = suffixOfRouteKey(args.args.routeKey, args.args.leafDepth, args.args.nibbleBits);
    const tagged = appendCollisionTag(suffix, args.args.element);
    const insertAt = findInsertIndex(args.entries, tagged);
    args.entries.splice(insertAt, 0, makeEntry(tagged, args.args.element, new Set([args.args.encodedDot])));
    return new TrieLeaf(args.entries, args.args.leaf.geometry);
}
function appendCollisionTag(suffix, element) {
    const tag = (element.charCodeAt(element.length - 1) | 0) & 0xff;
    const out = new Uint8Array(suffix.length + 1);
    out.set(suffix, 0);
    out[suffix.length] = tag === 0 ? 1 : tag;
    return out;
}
export function findInsertIndex(entries, suffix) {
    let lo = 0;
    let hi = entries.length;
    while (lo < hi) {
        const mid = (lo + hi) >>> 1;
        const entry = entries[mid];
        if (entry === undefined) {
            return lo;
        }
        if (compareBytes(entry.routeKeySuffix, suffix) < 0) {
            lo = mid + 1;
        }
        else {
            hi = mid;
        }
    }
    return lo;
}
export function compareBytes(left, right) {
    const len = Math.min(left.length, right.length);
    for (let i = 0; i < len; i += 1) {
        const a = left[i] ?? 0;
        const b = right[i] ?? 0;
        if (a !== b) {
            return a - b;
        }
    }
    return left.length - right.length;
}
export function makeEntry(routeKeySuffix, element, dots) {
    return {
        routeKeySuffix,
        element,
        dots,
        tombstonedDots: new Set(),
    };
}
export function findEntryInLeaf(args) {
    const suffix = suffixOfRouteKey(args.routeKey, args.depth, args.nibbleBits);
    return findEntryByElementNearSuffix(args.leaf, suffix, args.element);
}
function findEntryByElementNearSuffix(leaf, suffix, element) {
    const entries = leaf.entries();
    const exact = leaf.binarySearch(suffix);
    if (exact !== -1) {
        const hit = entries[exact];
        if (hit !== undefined && hit.element === element) {
            return hit;
        }
    }
    // The suffix may have been tagged for a collision; scan a
    // narrow window forward until the shared prefix ends.
    return scanForCollisionTaggedEntry(entries, suffix, element);
}
function scanForCollisionTaggedEntry(entries, suffix, element) {
    const startIndex = lowerBoundOnSuffix(entries, suffix);
    for (let i = startIndex; i < entries.length; i += 1) {
        const entry = entries[i];
        if (entry === undefined) {
            return null;
        }
        if (!hasSuffixPrefix(entry.routeKeySuffix, suffix)) {
            return null;
        }
        if (entry.element === element) {
            return entry;
        }
    }
    return null;
}
function lowerBoundOnSuffix(entries, suffix) {
    let lo = 0;
    let hi = entries.length;
    while (lo < hi) {
        const mid = (lo + hi) >>> 1;
        const entry = entries[mid];
        if (entry === undefined) {
            return lo;
        }
        if (compareBytes(entry.routeKeySuffix, suffix) < 0) {
            lo = mid + 1;
        }
        else {
            hi = mid;
        }
    }
    return lo;
}
function hasSuffixPrefix(candidate, expected) {
    if (candidate.length < expected.length) {
        return false;
    }
    return bytesEqualInRange(candidate, expected, expected.length);
}
function bytesEqualInRange(candidate, expected, length) {
    for (let i = 0; i < length; i += 1) {
        if ((candidate[i] ?? 0) !== (expected[i] ?? 0)) {
            return false;
        }
    }
    return true;
}
export function partitionEntriesByNextNibble(leaf, nibbleBits) {
    const out = new Map();
    for (const entry of leaf.entries()) {
        const nibble = firstNibbleOfSuffix(entry.routeKeySuffix, nibbleBits);
        pushIntoBucket(out, nibble, entry);
    }
    return out;
}
function pushIntoBucket(buckets, nibble, entry) {
    const bucket = buckets.get(nibble);
    if (bucket === undefined) {
        buckets.set(nibble, [entry]);
        return;
    }
    bucket.push(entry);
}
function firstNibbleOfSuffix(suffix, nibbleBits) {
    const byte = suffix[0] ?? 0;
    const shift = 8 - nibbleBits;
    const mask = (1 << nibbleBits) - 1;
    return (byte >>> shift) & mask;
}
export function shortenEntries(entries, nibbleBits, suffixNibbles) {
    const shortened = entries.map((entry) => ({
        routeKeySuffix: shiftSuffixLeftByOneNibble(entry.routeKeySuffix, nibbleBits, suffixNibbles),
        element: entry.element,
        dots: entry.dots,
        tombstonedDots: entry.tombstonedDots,
    }));
    shortened.sort((a, b) => compareBytes(a.routeKeySuffix, b.routeKeySuffix));
    return shortened;
}
/**
 * Shifts a suffix byte array left by `nibbleBits` bits, producing
 * the suffix at the next depth. The last (now-empty) nibble is
 * discarded; if that empties a whole byte, the resulting array is
 * one byte shorter.
 */
export function shiftSuffixLeftByOneNibble(suffix, nibbleBits, suffixNibbles) {
    const totalBits = (suffixNibbles - 1) * nibbleBits;
    if (totalBits <= 0) {
        return new Uint8Array(0);
    }
    const newLength = Math.ceil(totalBits / 8);
    const out = new Uint8Array(newLength);
    for (let bitIndex = 0; bitIndex < totalBits; bitIndex += 1) {
        writeBitMsbFirst(out, bitIndex, readBitMsbFirst(suffix, nibbleBits + bitIndex));
    }
    return out;
}
function readBitMsbFirst(bytes, bitOffset) {
    const byteIndex = Math.floor(bitOffset / 8);
    const byte = bytes[byteIndex] ?? 0;
    const bitInByte = bitOffset % 8;
    return (byte >>> (7 - bitInByte)) & 1;
}
export function tombstoneEntry(entry, observedDots) {
    const nextLive = new Set();
    const nextTombstoned = new Set(entry.tombstonedDots);
    let changed = false;
    for (const dot of entry.dots) {
        if (observedDots.has(dot)) {
            nextTombstoned.add(dot);
            changed = true;
        }
        else {
            nextLive.add(dot);
        }
    }
    return {
        entry: {
            routeKeySuffix: entry.routeKeySuffix,
            element: entry.element,
            dots: nextLive,
            tombstonedDots: nextTombstoned,
        },
        changed,
    };
}
export function pendingChildOid(parentPath, childNibble) {
    return `pending:${encodeDirtyPath([...parentPath, childNibble])}`;
}
export function isMissingStoreError(raw) {
    return raw instanceof TrieStoreError && raw.code === "E_TRIE_STORE_MISSING";
}
export function wrapStoreError(args) {
    if (args.raw instanceof TrieCursorError) {
        return args.raw;
    }
    const { message } = args.raw;
    return new TrieCursorError(`TrieCursor ${args.op} failed at path=${encodeDirtyPath(args.path)} oid=${args.oid}: ${message}`, {
        code: "E_TRIE_CURSOR_STORE",
        context: {
            op: args.op,
            path: encodeDirtyPath(args.path),
            oid: args.oid,
            cause: message,
        },
    });
}
export function wrapDecodeError(args) {
    if (args.raw instanceof TrieCursorError) {
        return args.raw;
    }
    const { message } = args.raw;
    return new TrieCursorError(`TrieCursor ${args.op} decode failed at path=${encodeDirtyPath(args.path)} oid=${args.oid}: ${message}`, {
        code: "E_TRIE_CURSOR_DECODE",
        context: {
            op: args.op,
            path: encodeDirtyPath(args.path),
            oid: args.oid,
            cause: message,
        },
    });
}
