import { assert } from "../../../core/assert.js";
import { ceilPowerOfTwo } from "../../../core/binary/operations/ceilPowerOfTwo.js";
import {
    generate_next_linear_congruential_index
} from "../../../core/collection/map/generate_next_linear_congruential_index.js";
import { isPowerOfTwo } from "../../../core/math/isPowerOfTwo.js";

/**
 * Reserved sentinel for "empty slot." Network IDs are unsigned 32-bit; we
 * forbid `0xFFFFFFFF` as a usable ID (the slot table never allocates that
 * many slots, so this is safe in practice).
 * @type {number}
 */
const EMPTY = 0xFFFFFFFF;

/**
 * Maximum load factor before {@link ChangedEntitySet#add} grows the backing
 * array. 0.5 keeps probe chains short (mean probe distance ≈ 1.5 with random
 * keys) at the cost of using twice the slots; the right tradeoff for a hot-path
 * set where O(1)-expected `add` matters more than memory.
 * @type {number}
 */
const LOAD_FACTOR = 0.5;

/**
 * Open-addressed hash set keyed by `uint32` network IDs. Backed by a single
 * `Uint32Array`; empty slots hold `0xFFFFFFFF`. Probing uses
 * {@link generate_next_linear_congruential_index} (the same scheme as
 * `core/collection/map/HashMap.js`).
 *
 * Designed for the per-tick "which entities changed this tick?" use case:
 *   - `clear()` is one `fill(0xFFFFFFFF)` call (vectorisable, JIT-friendly).
 *   - `add(network_id)` is O(1) expected with a load factor < 0.5.
 *   - `compact_into(buffer)` writes each non-empty slot as a `varint` and
 *     returns the count, suitable for appending to a `MutationLedger` slice.
 *
 * Capacity grows automatically when load factor exceeds 0.5 (doubles + rehashes,
 * O(N) amortised). Construction-time `capacity` is the *initial* size —
 * pick at least 2× your typical peak per-tick mutation count to avoid the
 * grow path on the hot loop.
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
export class ChangedEntitySet {
    #mask;
    #slots;
    #size;


    /**
     * @param {{ capacity?: number }} [options] capacity is rounded up to a power of two; default 256
     */
    constructor({ capacity = 256 } = {}) {
        assert.isNonNegativeInteger(capacity, 'capacity');

        // Round up to power of two so we can use a bitmask instead of modulo.
        const cap = ceilPowerOfTwo(capacity);

        /** @type {number} */
        this.capacity = cap;

        /** @private @type {number} */
        this.#mask = cap - 1;

        /**
         * @private
         * @type {Uint32Array}
         */
        this.#slots = new Uint32Array(cap).fill(EMPTY);

        /** @private @type {number} */
        this.#size = 0;
    }

    /**
     * Reallocate the backing array at `new_capacity` and re-insert every
     * present ID. Items are placed by their hash against the new mask, so
     * resize is the only correct way to change capacity (a flat copy would
     * leave entries at indices that no longer match `id & mask`).
     *
     * @param {number} new_capacity power of two; must be at least 2× current size
     */
    resize(new_capacity) {
        assert.isNonNegativeInteger(new_capacity, 'new_capacity');
        assert.ok(isPowerOfTwo(new_capacity), 'new_capacity must be a power of two');
        assert.ok(new_capacity >= this.#size * 2, 'new_capacity too small to host current size at load factor 0.5');

        // Snapshot the old slots, swap in the new ones, then re-insert. Doing
        // it via add() handles the new hash positions for free; the trick is
        // resetting __size first so add()'s "already present" path doesn't
        // miscount and so the load-factor guard inside add() doesn't recurse.
        const old_slots = this.#slots;

        this.#slots = new Uint32Array(new_capacity).fill(EMPTY);
        this.#mask = new_capacity - 1;
        this.capacity = new_capacity;
        this.#size = 0;

        for (let i = 0; i < old_slots.length; i++) {
            const v = old_slots[i];
            if (v !== EMPTY) this.add(v);
        }
    }

    /**
     * Double the capacity. Called automatically by {@link add} when the load
     * factor would exceed `LOAD_FACTOR` after the next insert.
     */
    grow() {
        this.resize(this.capacity * 2);
    }

    /**
     * Add a network ID. No-op if already present. Grows the backing array
     * (and re-hashes) if adding this ID would exceed the load factor.
     *
     * @param {number} network_id non-negative integer < 0xFFFFFFFF
     */
    add(network_id) {
        // The EMPTY sentinel collides with itself: the dedup check would match
        // an empty slot and silently no-op while `has(EMPTY)` would later report
        // true — `size`/`for_each`/`has` would disagree.
        assert.notEqual(network_id, EMPTY, 'ChangedEntitySet.add: 0xFFFFFFFF is reserved as the EMPTY sentinel');

        // Pre-grow check: if this insert would push us over the load factor,
        // double capacity *first* so the insert lands in the bigger table.
        // Equivalent to (size+1)/capacity > LOAD_FACTOR; written without the
        // division to keep the hot path branch-light.
        if ((this.#size + 1) > this.capacity * LOAD_FACTOR) {
            this.grow();
        }

        // No assert in the hot path — this is called per-mutation. Bad input
        // produces wrong results (collisions with the EMPTY sentinel) which
        // higher layers will notice; the cost of asserting per call isn't worth it.
        const slots = this.#slots;
        const mask = this.#mask;

        let slot_index = network_id & mask;

        const capacity = this.capacity;

        for (let probe = 0; probe < capacity; probe++) {

            const v = slots[slot_index];

            if (v === network_id) {
                // already present
                return;
            }

            if (v === EMPTY) {
                slots[slot_index] = network_id;
                this.#size++;
                return;
            }

            slot_index = generate_next_linear_congruential_index(slot_index, mask);
        }

        // Unreachable: the load-factor guard above keeps occupancy under 0.5,
        // so the probe loop is guaranteed to find an EMPTY slot.
        throw new Error(`ChangedEntitySet.add: probe loop exhausted at capacity ${this.capacity}, size ${this.#size} — this is a bug`);
    }

    /**
     * @param {number} network_id
     * @returns {boolean}
     */
    has(network_id) {
        const slots = this.#slots;
        const mask = this.#mask;

        let slot_index = network_id & mask;

        for (let probe = 0; probe < this.capacity; probe++) {

            const v = slots[slot_index];

            if (v === network_id) {
                return true;
            }

            if (v === EMPTY) {
                return false;
            }

            slot_index = generate_next_linear_congruential_index(slot_index, mask);
        }
        return false;
    }

    /**
     * Reset every slot to empty.
     */
    clear() {
        this.#slots.fill(EMPTY);
        this.#size = 0;
    }

    /**
     * Number of unique network IDs currently in the set.
     * @returns {number}
     */
    size() {
        return this.#size;
    }

    /**
     * Iterate every present network ID. Order is implementation-defined
     * (slot order, not insertion or sorted) — callers must not rely on it.
     *
     * @param {function(number): void} fn
     */
    for_each(fn) {
        const slots = this.#slots;
        const cap = this.capacity;
        for (let i = 0; i < cap; i++) {
            const v = slots[i];
            if (v !== EMPTY) fn(v);
        }
    }

    /**
     * Append every present network ID into `buffer` as varints. Returns the
     * number of IDs written. Caller is responsible for any framing (count
     * prefix, length prefix, etc.).
     *
     * @param {BinaryBuffer} buffer
     * @returns {number} count written
     */
    compact_into(buffer) {
        let written = 0;

        const slots = this.#slots;
        const cap = this.capacity;
        for (let i = 0; i < cap; i++) {
            const v = slots[i];
            if (v !== EMPTY) {
                buffer.writeUintVar(v);
                written++;
            }
        }

        return written;
    }
}

/**
 * Sentinel value reserved for empty slots — exposed so external callers can
 * recognise it if they peek at raw storage.
 * @readonly
 * @type {number}
 */
ChangedEntitySet.EMPTY = EMPTY;
