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

/**
 * Bidirectional map between local entity IDs and stable, peer-shared network IDs.
 *
 * The same entity is represented by different local IDs on different peers (each
 * peer's `EntityComponentDataset` assigns IDs independently). The network ID is
 * the peer-shared identifier for "this object" in serialized form.
 *
 * Network IDs are recyclable: when a slot is freed, its `generation` counter
 * increments. A stale reference like `(network_id=5, generation=2)` will not
 * match the live `(network_id=5, generation=3)` slot, so use-after-free can
 * be detected at parse time on the receiving peer.
 *
 * Storage is two parallel typed arrays indexed by `network_id`, plus a small
 * free-list of reusable IDs. No JS object per slot.
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
export class ReplicationSlotTable {
    #entity_id_by_network;
    #generation_by_network;
    #network_id_by_entity;
    #free_list;
    #highwater;
    #capacity;


    /**
     * @param {{ initial_capacity?: number }} [options]
     */
    constructor({ initial_capacity = 256 } = {}) {
        /**
         * `entity_id` for each network_id slot, or -1 if free.
         * @type {Int32Array}
         * @private
         */
        this.#entity_id_by_network = new Int32Array(initial_capacity).fill(-1);

        /**
         * Generation counter per slot. Bumped on free.
         * @type {Uint32Array}
         * @private
         */
        this.#generation_by_network = new Uint32Array(initial_capacity);

        /**
         * Reverse map for entity_id -> network_id lookup.
         * Sparse; only populated for live mappings.
         * @type {Map<number, number>}
         * @private
         */
        this.#network_id_by_entity = new Map();

        /**
         * Recyclable network_ids.
         * @type {number[]}
         * @private
         */
        this.#free_list = [];

        /**
         * Highest network_id ever assigned.
         * @type {number}
         * @private
         */
        this.#highwater = 0;

        /**
         * @type {number}
         * @private
         */
        this.#capacity = initial_capacity;
    }

    /**
     * Allocate a network_id for the given entity_id. Returns the new id.
     * Throws if the entity already has a network_id.
     *
     * @param {number} entity_id
     * @returns {number} network_id
     */
    allocate(entity_id) {
        assert.isNonNegativeInteger(entity_id, 'entity_id');
        if (this.#network_id_by_entity.has(entity_id)) {
            throw new Error(`ReplicationSlotTable.allocate: entity ${entity_id} already has network_id ${this.#network_id_by_entity.get(entity_id)}`);
        }

        let network_id;
        if (this.#free_list.length > 0) {
            network_id = this.#free_list.pop();
        } else {
            network_id = this.#highwater++;
            if (network_id >= this.#capacity) {
                this.#grow();
            }
        }

        this.#entity_id_by_network[network_id] = entity_id;
        this.#network_id_by_entity.set(entity_id, network_id);
        return network_id;
    }

    /**
     * Allocate using an explicit network_id (for the receiving peer that takes its
     * IDs from the wire). Bumps the slot's generation if it was reused. Throws
     * if the slot is currently in use.
     *
     * @param {number} network_id
     * @param {number} entity_id
     */
    allocate_at(network_id, entity_id) {
        assert.isNonNegativeInteger(network_id, 'network_id');
        assert.isNonNegativeInteger(entity_id, 'entity_id');
        if (network_id >= this.#capacity) {
            while (network_id >= this.#capacity) this.#grow();
        }
        if (this.#entity_id_by_network[network_id] !== -1) {
            throw new Error(`ReplicationSlotTable.allocate_at: network_id ${network_id} already in use`);
        }
        if (this.#network_id_by_entity.has(entity_id)) {
            throw new Error(`ReplicationSlotTable.allocate_at: entity ${entity_id} already has network_id ${this.#network_id_by_entity.get(entity_id)}`);
        }
        // Catch the "id is on the free list" case before silent clobbering: a
        // subsequent allocate() would pop the same id and overwrite this mapping.
        assert.arrayHasNo(this.#free_list, network_id, `ReplicationSlotTable.allocate_at: network_id ${network_id} is in the free list`);
        this.#entity_id_by_network[network_id] = entity_id;
        this.#network_id_by_entity.set(entity_id, network_id);
        if (network_id >= this.#highwater) this.#highwater = network_id + 1;
    }

    /**
     * Free the slot for `network_id`. Bumps the slot's generation. Slot becomes
     * available for re-allocation.
     *
     * @param {number} network_id
     */
    free(network_id) {
        assert.isNonNegativeInteger(network_id, 'network_id');
        const entity_id = this.#entity_id_by_network[network_id];
        if (entity_id === -1) {
            return; // already free
        }
        this.#entity_id_by_network[network_id] = -1;
        this.#network_id_by_entity.delete(entity_id);
        this.#generation_by_network[network_id] = (this.#generation_by_network[network_id] + 1) >>> 0;
        this.#free_list.push(network_id);
    }

    /**
     * Local entity for a network_id, or -1 if the slot is free.
     *
     * @param {number} network_id
     * @returns {number}
     */
    entity_for(network_id) {
        assert.isInteger(network_id, 'network_id');
        if (network_id < 0 || network_id >= this.#capacity) return -1;
        return this.#entity_id_by_network[network_id];
    }

    /**
     * Network_id for a local entity, or -1 if not registered.
     *
     * @param {number} entity_id
     * @returns {number}
     */
    network_for(entity_id) {
        assert.isNonNegativeInteger(entity_id, 'entity_id');
        const v = this.#network_id_by_entity.get(entity_id);
        return v === undefined ? -1 : v;
    }

    /**
     * Generation of the slot. Useful for stale-reference detection.
     *
     * @param {number} network_id
     * @returns {number}
     */
    generation_of(network_id) {
        assert.isInteger(network_id, 'network_id');
        if (network_id < 0 || network_id >= this.#capacity) return 0;
        return this.#generation_by_network[network_id];
    }

    /**
     * Number of currently-allocated slots.
     * @returns {number}
     */
    live_count() {
        return this.#network_id_by_entity.size;
    }

    /**
     * @private
     */
    #grow() {
        const new_capacity = Math.max(this.#capacity * 2, this.#capacity + 64);

        const new_entities = new Int32Array(new_capacity).fill(-1);
        new_entities.set(this.#entity_id_by_network);

        const new_generations = new Uint32Array(new_capacity);
        new_generations.set(this.#generation_by_network);

        this.#entity_id_by_network = new_entities;
        this.#generation_by_network = new_generations;
        this.#capacity = new_capacity;
    }
}
