import { assert } from "../assert.js";

const UINT32_MAX = 4294967295;

/**
 * Returned by {@link Uint32Map#get} when the key is absent. Chosen so that any uint32 (0 .. UINT32_MAX) is
 * unambiguously a valid value. As a JS Number this is a Smi on all V8 builds, so the return doesn't allocate.
 * @type {number}
 */
export const UINT32_MAP_ABSENT = -1;

/**
 * 2^32 / phi, rounded to nearest integer. Multiplier for Fibonacci hashing — scatters sequential integer
 * keys across the index space with one multiply and one shift.
 * @type {number}
 */
const FIB_HASH_MULTIPLIER = 2654435769;

/**
 * @type {number}
 */
const DEFAULT_CAPACITY = 16;

/**
 * Smallest table we ever create. Capacity is always a power of two, so the actual minimum is 2.
 * @type {number}
 */
const MIN_CAPACITY = 2;

/**
 * Grow when `size > capacity * 3/4`. Slater ("Optimizing Open Addressing", thenumb.at/Hashtables) puts
 * the sweet-spot load factor for Robin Hood at ~0.75; past that, max probe length climbs fast even though
 * mean stays low.
 * @type {number}
 */
const LOAD_FACTOR_NUMERATOR = 3;
const LOAD_FACTOR_SHIFT = 2; // divide by 4

/**
 * Non-allocating Map&lt;uint32, uint32&gt; backed by a single Uint32Array.
 *
 * Layout: one slot occupies two consecutive words — `data[2*slot] = key + 1`, `data[2*slot + 1] = value`.
 * Keys are stored offset by +1 so that 0 is unambiguously "empty slot"; this also keeps every stored value
 * inside the V8 Smi range (under pointer compression, Smi is 31-bit signed = max 2^30 − 1), which means
 * reading any non-empty slot does not allocate a HeapNumber. Without the shift, a sentinel of 0xFFFFFFFF
 * would box on every read of an empty slot — the exact opposite of what we want.
 *
 * Collision resolution: Robin Hood linear probing with backward-shift deletion (Knuth Algorithm R). Hash
 * is Fibonacci multiplicative (see Skarupke 2018, "Fibonacci Hashing"), indexing from the high bits of
 * the product. Table capacity is always a power of two; index is computed by right-shift, not by mask of
 * the low bits — the high bits of a multiplicative hash have more entropy.
 *
 * Allowed key range: 0 .. UINT32_MAX - 1 (because we store key + 1). Allowed value range: full uint32.
 * Values are returned via {@link UINT32_MAP_ABSENT} when not found — callers check `=== -1` rather than
 * `=== undefined`, which is the Map API but is allocation-incompatible with typed-array storage.
 */
export class Uint32Map {

    /**
     *
     * @param {number} [initial_capacity] minimum number of slots to reserve. Rounded up to next power of two.
     */
    constructor(initial_capacity = DEFAULT_CAPACITY) {
        assert.isNonNegativeInteger(initial_capacity, 'initial_capacity');

        let cap = MIN_CAPACITY;
        while (cap < initial_capacity) {
            cap <<= 1;
        }

        /**
         * @type {number}
         * @private
         */
        this.__capacity = cap;
        /**
         * @type {number}
         * @private
         */
        this.__mask = cap - 1;
        /**
         * Right-shift amount that extracts log2(cap) high bits from a 32-bit hash.
         * @type {number}
         * @private
         */
        this.__shift = Math.clz32(cap - 1);
        /**
         * @type {number}
         * @private
         */
        this.__grow_threshold = (cap * LOAD_FACTOR_NUMERATOR) >>> LOAD_FACTOR_SHIFT;
        /**
         * @type {number}
         * @private
         */
        this.__size = 0;
        /**
         * Packed (key+1, value) pairs. Length is 2 * capacity. Zero-initialized so every slot starts empty.
         * @type {Uint32Array}
         * @private
         */
        this.__data = new Uint32Array(cap << 1);
    }

    /**
     *
     * @returns {number}
     */
    get size() {
        return this.__size;
    }

    /**
     *
     * @returns {number}
     */
    get capacity() {
        return this.__capacity;
    }

    /**
     *
     * @param {number} id
     * @returns {number} value associated with `id`, or {@link UINT32_MAP_ABSENT} (-1) if not present.
     */
    get(id) {
        const data = this.__data;
        const mask = this.__mask;
        const shift = this.__shift;

        const key_shifted = id + 1;

        let slot = Math.imul(key_shifted, FIB_HASH_MULTIPLIER) >>> shift;
        let dist = 0;

        while (true) {
            const slot2 = slot << 1;
            const k = data[slot2];

            if (k === 0) {
                return UINT32_MAP_ABSENT;
            }
            if (k === key_shifted) {
                return data[slot2 + 1];
            }

            // Robin Hood early-exit: if our displacement exceeds the occupant's, we would have stolen this
            // slot during insert — so the key cannot be further down the probe chain.
            const slot_home = Math.imul(k, FIB_HASH_MULTIPLIER) >>> shift;
            const slot_dist = (slot - slot_home) & mask;
            if (dist > slot_dist) {
                return UINT32_MAP_ABSENT;
            }

            slot = (slot + 1) & mask;
            dist++;
        }
    }

    /**
     *
     * @param {number} id
     * @returns {boolean}
     */
    has(id) {
        return this.get(id) !== UINT32_MAP_ABSENT;
    }

    /**
     *
     * @param {number} id
     * @param {number} value
     */
    set(id, value) {
        // hot path — asserts commented per codebase convention. Pre-conditions:
        //   id    must be a non-negative integer <= UINT32_MAX - 1
        //   value must be a non-negative integer (full uint32 range)
        // assert.isNonNegativeInteger(id, 'id');
        // assert.lessThanOrEqual(id, UINT32_MAX - 1, 'id must be <= (2^32 - 2)');
        // assert.isNonNegativeInteger(value, 'value');

        if (this.__size >= this.__grow_threshold) {
            this.__resize(this.__capacity << 1);
        }

        this.__insert_no_grow(id + 1, value);
    }

    /**
     * Insert a (already key+1 shifted) entry assuming there is at least one empty slot. Performs Robin Hood
     * swapping: when the new entry has been displaced further than the current occupant, swap them and
     * continue placing the displaced occupant.
     *
     * @private
     * @param {number} key_shifted
     * @param {number} value
     */
    __insert_no_grow(key_shifted, value) {
        const data = this.__data;
        const mask = this.__mask;
        const shift = this.__shift;

        let ks = key_shifted;
        let v = value;
        let slot = Math.imul(ks, FIB_HASH_MULTIPLIER) >>> shift;
        let dist = 0;

        while (true) {
            const slot2 = slot << 1;
            const k = data[slot2];

            if (k === 0) {
                data[slot2] = ks;
                data[slot2 + 1] = v;
                this.__size++;
                return;
            }

            if (k === ks) {
                // existing entry — overwrite value, do not change size
                data[slot2 + 1] = v;
                return;
            }

            const slot_home = Math.imul(k, FIB_HASH_MULTIPLIER) >>> shift;
            const slot_dist = (slot - slot_home) & mask;

            if (slot_dist < dist) {
                // steal: occupant is closer to home than the entry we are carrying. Swap.
                const tmp_k = k;
                const tmp_v = data[slot2 + 1];
                data[slot2] = ks;
                data[slot2 + 1] = v;
                ks = tmp_k;
                v = tmp_v;
                dist = slot_dist;
            }

            slot = (slot + 1) & mask;
            dist++;
        }
    }

    /**
     *
     * @param {number} id
     * @returns {boolean} true if the entry existed and was removed.
     */
    delete(id) {
        const data = this.__data;
        const mask = this.__mask;
        const shift = this.__shift;

        const key_shifted = id + 1;

        let slot = Math.imul(key_shifted, FIB_HASH_MULTIPLIER) >>> shift;
        let dist = 0;

        // locate the slot, applying the same Robin Hood early-exit as `get`
        while (true) {
            const slot2 = slot << 1;
            const k = data[slot2];

            if (k === 0) {
                return false;
            }
            if (k === key_shifted) {
                break;
            }

            const slot_home = Math.imul(k, FIB_HASH_MULTIPLIER) >>> shift;
            const slot_dist = (slot - slot_home) & mask;
            if (dist > slot_dist) {
                return false;
            }

            slot = (slot + 1) & mask;
            dist++;
        }

        // backward-shift deletion: walk forward, pulling each displaced successor into the freed slot,
        // until we hit an empty slot or an occupant already at its home.
        while (true) {
            const next = (slot + 1) & mask;
            const next2 = next << 1;
            const next_k = data[next2];

            if (next_k === 0) {
                break;
            }

            const next_home = Math.imul(next_k, FIB_HASH_MULTIPLIER) >>> shift;
            const next_dist = (next - next_home) & mask;
            if (next_dist === 0) {
                // successor is at its home — moving it back would break its lookup
                break;
            }

            const slot2 = slot << 1;
            data[slot2] = next_k;
            data[slot2 + 1] = data[next2 + 1];
            slot = next;
        }

        // clear the final emptied slot
        data[slot << 1] = 0;

        this.__size--;
        return true;
    }

    /**
     * Remove all entries. Capacity is unchanged.
     */
    clear() {
        this.__data.fill(0);
        this.__size = 0;
    }

    /**
     * Grow the table and rehash every entry.
     *
     * @private
     * @param {number} new_capacity power of two
     */
    __resize(new_capacity) {
        const old_data = this.__data;
        const old_capacity = this.__capacity;

        this.__capacity = new_capacity;
        this.__mask = new_capacity - 1;
        this.__shift = Math.clz32(new_capacity - 1);
        this.__grow_threshold = (new_capacity * LOAD_FACTOR_NUMERATOR) >>> LOAD_FACTOR_SHIFT;
        this.__data = new Uint32Array(new_capacity << 1);
        this.__size = 0;

        for (let i = 0; i < old_capacity; i++) {
            const i2 = i << 1;
            const k = old_data[i2];
            if (k !== 0) {
                this.__insert_no_grow(k, old_data[i2 + 1]);
            }
        }
    }
}
