import TrieLeafError from "../../errors/TrieLeafError.js";
/**
 * The wire-format version this codec emits and accepts.
 *
 * A breaking change to the leaf layout bumps this number; the
 * deserializer rejects unrecognized values with
 * `E_TRIE_LEAF_VERSION`.
 */
export const TRIE_LEAF_WIRE_VERSION = 1;
/**
 * Runtime-backed leaf node of the shadow trie.
 *
 * A leaf holds a sorted array of entries (by `routeKeySuffix`,
 * byte-lex). Construction validates sort order and freezes the
 * instance. `binarySearch` returns the matching index or `-1`.
 * `serialize` / `deserialize` round-trip through a `CodecPort`
 * adapter via a versioned CBOR envelope.
 *
 * The leaf owns the geometry it was constructed under so it can
 * answer split/merge questions without the caller threading the
 * geometry separately.
 */
export default class TrieLeaf {
    geometry;
    sortedEntries;
    constructor(entries, geometry) {
        validateEntriesSorted(entries);
        this.sortedEntries = entries;
        this.geometry = geometry;
        Object.freeze(this);
    }
    /**
     * Number of entries in the leaf.
     */
    size() {
        return this.sortedEntries.length;
    }
    /**
     * Accessor for the sorted entry array. Returns the frozen array
     * the leaf was constructed with — the caller is expected not to
     * mutate it.
     */
    entries() {
        return this.sortedEntries;
    }
    /**
     * Binary search for an exact route-key-suffix match.
     *
     * Returns the index of the matching entry or `-1` if the suffix
     * is not present. Comparison is byte-lex over `Uint8Array`.
     */
    binarySearch(suffix) {
        let lo = 0;
        let hi = this.sortedEntries.length - 1;
        while (lo <= hi) {
            const mid = (lo + hi) >>> 1;
            const entry = this.sortedEntries[mid];
            if (entry === undefined) {
                return -1;
            }
            const cmp = compareBytes(entry.routeKeySuffix, suffix);
            if (cmp === 0) {
                return mid;
            }
            if (cmp < 0) {
                lo = mid + 1;
            }
            else {
                hi = mid - 1;
            }
        }
        return -1;
    }
    /**
     * Returns true when the leaf has exceeded the geometry's capacity.
     */
    requiresSplit(geometry) {
        return geometry.splitRequired(this.sortedEntries.length);
    }
    /**
     * Returns true when the leaf has dropped below the geometry's
     * merge floor.
     */
    requiresMerge(geometry) {
        return geometry.mergeRequired(this.sortedEntries.length);
    }
    /**
     * Serialize this leaf to CBOR bytes via the supplied codec.
     *
     * The wire format is a versioned envelope; see
     * `TRIE_LEAF_WIRE_VERSION` for the current value.
     */
    serialize(codec) {
        const wire = {
            version: TRIE_LEAF_WIRE_VERSION,
            entries: this.sortedEntries.map(entryToWire),
        };
        return codec.encode(wire);
    }
    /**
     * Decode bytes into a `TrieLeaf` under the supplied geometry.
     *
     * Validates the envelope version, the envelope shape, and the
     * entries' sort order. Raises `TrieLeafError` with a typed code
     * on any violation.
     */
    static deserialize(bytes, geometry, codec) {
        const decoded = codec.decode(bytes);
        if (!isLeafWireFormat(decoded)) {
            throw new TrieLeafError("TrieLeaf.deserialize received bytes that do not match the leaf envelope shape", { code: "E_TRIE_LEAF_WIRE_SHAPE" });
        }
        if (decoded.version !== TRIE_LEAF_WIRE_VERSION) {
            throw new TrieLeafError(`TrieLeaf.deserialize saw unrecognized wire version ${String(decoded.version)}; expected ${String(TRIE_LEAF_WIRE_VERSION)}`, { code: "E_TRIE_LEAF_VERSION" });
        }
        const entries = decoded.entries.map(entryFromWire);
        return new TrieLeaf(entries, geometry);
    }
}
function entryToWire(entry) {
    return [
        entry.routeKeySuffix,
        entry.element,
        [...entry.dots],
        [...entry.tombstonedDots],
    ];
}
function entryFromWire(raw) {
    const [routeKeySuffix, element, dots, tombstonedDots] = raw;
    return {
        routeKeySuffix,
        element,
        dots: new Set(dots),
        tombstonedDots: new Set(tombstonedDots),
    };
}
function validateEntriesSorted(entries) {
    for (let i = 1; i < entries.length; i += 1) {
        const prev = entries[i - 1];
        const curr = entries[i];
        if (prev === undefined || curr === undefined) {
            throw new TrieLeafError("TrieLeaf constructor received a sparse entry array", { code: "E_TRIE_LEAF_ENTRY_SHAPE" });
        }
        if (compareBytes(prev.routeKeySuffix, curr.routeKeySuffix) >= 0) {
            throw new TrieLeafError(`TrieLeaf entries must be strictly sorted by routeKeySuffix; violation at index ${String(i)}`, { code: "E_TRIE_LEAF_UNSORTED" });
        }
    }
}
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;
}
function isDecodedRecord(value) {
    return value !== null && typeof value === "object";
}
function isDecodedArray(value) {
    return Array.isArray(value);
}
function isLeafWireFormat(value) {
    if (!isDecodedRecord(value)) {
        return false;
    }
    const { version, entries } = value;
    if (typeof version !== "number" || !isDecodedArray(entries)) {
        return false;
    }
    return entries.every(isLeafWireEntry);
}
function isLeafWireEntry(value) {
    if (!isDecodedArray(value) || value.length !== 4) {
        return false;
    }
    return isLeafWireEntryFields(value);
}
function isLeafWireEntryFields(value) {
    const [suffix, element, dots, tombstoned] = value;
    if (!(suffix instanceof Uint8Array) || typeof element !== "string") {
        return false;
    }
    return isStringArray(dots) && isStringArray(tombstoned);
}
function isStringArray(value) {
    return isDecodedArray(value) && value.every((v) => typeof v === "string");
}
