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

/**
 * Per-action-type priority accumulator for bandwidth-aware packet packing.
 *
 * Each action class is assigned a static priority at registration; per
 * tick, each type's accumulator grows by `priority * dt`. When the
 * outgoing packet's byte budget is tight, the orchestrator queries
 * {@link sorted_descending} to get types in priority-ranked order and
 * packs from highest down until the packet is full. Calling
 * {@link consume} after a type has been packed reduces its accumulator,
 * letting other types win the next tick — this is the starvation-
 * resistant scheduling pattern Glenn Fiedler's *Networked Physics*
 * series describes.
 *
 * Memory layout: two Float32Arrays indexed by `type_id` (assumed dense
 * starting at 0, matching the engine's `SimActionRegistry` numbering).
 * `tick` is a single pass over both arrays — cache-friendly and
 * allocation-free in steady state.
 *
 * Scope: this module is the data-structure half. Wiring it into
 * `Replicator.pack_for_peer` is the orchestrator's concern — the
 * prototype's tiny action volumes make integration unnecessary, but
 * a real production game with hundreds of replicated entities will
 * need the Replicator to consult {@link sorted_descending} when its
 * outgoing buffer approaches the MTU.
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
export class PriorityAccumulator {
    #priority;
    #accum;
    #sort_indices;


    /**
     * @param {{ type_count: number }} options
     *   `type_count` is the dense range `[0, type_count)` of action type_ids
     *   the accumulator will track. Should be at least one more than the
     *   highest registered type_id.
     */
    constructor({ type_count }) {
        assert.isPositiveInteger(type_count, 'type_count');

        /** @readonly */
        this.type_count = type_count;

        /**
         * Static per-type priority. Set once at registration via
         * {@link set_priority}.
         * @type {Float32Array}
         * @private
         */
        this.#priority = new Float32Array(type_count);

        /**
         * Per-type accumulator, grows linearly with `priority * dt` each
         * tick. Reduced by {@link consume} when an action of that type is
         * packed into an outgoing packet.
         * @type {Float32Array}
         * @private
         */
        this.#accum = new Float32Array(type_count);

        /**
         * Reusable indirection array for {@link sorted_descending}. Typed
         * arrays don't accept comparator-based sort; this is a regular
         * `Array<number>` for that purpose. Pre-allocated to avoid GC
         * pressure on the per-tick selection path.
         * @type {number[]}
         * @private
         */
        this.#sort_indices = new Array(type_count);
    }

    /**
     * Set the static priority for a type. Typically called at action
     * registration (per type, once). Larger = more important.
     *
     * @param {number} type_id
     * @param {number} priority  units are arbitrary; only relative magnitudes matter
     */
    set_priority(type_id, priority) {
        assert.isNonNegativeInteger(type_id, 'type_id');
        assert.ok(type_id < this.type_count, `type_id ${type_id} out of range [0, ${this.type_count})`);
        assert.isNumber(priority, 'priority');
        this.#priority[type_id] = priority;
    }

    /**
     * @param {number} type_id
     * @returns {number} the configured static priority for this type
     */
    priority(type_id) {
        return this.#priority[type_id];
    }

    /**
     * Advance every accumulator by `priority * dt`. Call once per tick
     * before consulting {@link sorted_descending} / {@link consume}.
     *
     * @param {number} dt  elapsed seconds (or any time unit consistent with
     *                     the priority units you configured)
     */
    tick(dt) {
        const N = this.type_count;
        const p = this.#priority;
        const a = this.#accum;
        for (let i = 0; i < N; i++) {
            a[i] += p[i] * dt;
        }
    }

    /**
     * Current accumulator value for a type.
     * @param {number} type_id
     * @returns {number}
     */
    value(type_id) {
        return this.#accum[type_id];
    }

    /**
     * Reduce a type's accumulator by `amount`. Clamped at 0 — a type's
     * accumulator never goes negative, so missing a single tick doesn't
     * permanently penalize it. Call after packing an action of this type
     * into an outgoing packet.
     *
     * @param {number} type_id
     * @param {number} amount
     */
    consume(type_id, amount) {
        const next = this.#accum[type_id] - amount;
        this.#accum[type_id] = next < 0 ? 0 : next;
    }

    /**
     * Clear a type's accumulator to 0. Useful when an entire backlog has
     * been packed (e.g. all pending actions of a type fit in one packet).
     *
     * @param {number} type_id
     */
    reset(type_id) {
        this.#accum[type_id] = 0;
    }

    /**
     * Write type_ids to `out_array` in descending accumulator order. The
     * orchestrator then iterates `out_array` to know which types to pack
     * first.
     *
     * `out_array` must have capacity for `type_count` entries. Stable
     * within ties (insertion order = type_id ascending), so the result
     * is deterministic given the same accumulator state.
     *
     * @param {Int32Array|number[]} out_array
     */
    sorted_descending(out_array) {
        assert.ok(out_array.length >= this.type_count,
            `out_array must hold at least ${this.type_count} entries`);
        const N = this.type_count;
        const indices = this.#sort_indices;
        for (let i = 0; i < N; i++) indices[i] = i;
        const accum = this.#accum;
        // Stable sort (Array.prototype.sort is stable in ES2019+); tie-break
        // by insertion order = type_id ascending, which matches the natural
        // registration order.
        indices.sort((a, b) => accum[b] - accum[a]);
        for (let i = 0; i < N; i++) out_array[i] = indices[i];
    }

    /**
     * Reset every accumulator to 0. Priorities are preserved. Use when
     * re-establishing fresh state (e.g. after a disconnect/reconnect).
     */
    clear_accumulators() {
        for (let i = 0; i < this.type_count; i++) this.#accum[i] = 0;
    }
}
