import { assert } from "../../../core/assert.js";
import { binarySearchHighIndex } from "../../../core/collection/array/binarySearchHighIndex.js";

/**
 * Comparator for {@link binarySearchHighIndex}: given a new frame number
 * and an existing queue entry, returns the standard `< 0 / 0 / > 0` ordering.
 * Hoisted to module scope so the binary-search helper doesn't allocate a
 * fresh closure on every push.
 * @param {number} frame
 * @param {{frame: number}} entry
 * @returns {number}
 */
function jitter_compare_frame(frame, entry) {
    return frame - entry.frame;
}

/**
 * Holds inbound payloads keyed by frame number, releasing them only after they
 * have aged enough that the consumer has high confidence ordering is settled.
 *
 * The delay (`delay_frames`) is the cost we pay to absorb arrival jitter. A
 * larger delay smooths over more jitter but adds visible latency; the canonical
 * choice is `~3 * snapshot_interval + small slack` (Glenn Fiedler).
 *
 * Generic over payload type — the buffer treats payloads as opaque blobs to
 * pass through. Common payload types: `Uint8Array` (raw packet), `BinaryBuffer`,
 * deserialized action records.
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
export class JitterBuffer {
    #queue;


    /**
     * @param {{ delay_frames?: number }} [options]
     */
    constructor({ delay_frames = 3 } = {}) {
        assert.isNonNegativeInteger(delay_frames, 'delay_frames');

        /** @readonly @type {number} */
        this.delay_frames = delay_frames;

        /**
         * Sorted by frame ascending.
         * @type {{frame: number, payload: any}[]}
         * @private
         */
        this.#queue = [];
    }

    /**
     * Insert a payload tagged with its source frame number. Inserts in
     * frame-sorted order so {@link drain_until} can scan from the front.
     *
     * @param {number} frame
     * @param {*} payload
     */
    push(frame, payload) {
        assert.isNonNegativeInteger(frame, 'frame');
        // The whole point of this class is to absorb jittered arrivals, so
        // out-of-order pushes are the common case. `binarySearchHighIndex`
        // gives O(log n) insertion-point lookup and matches the engine's
        // canonical sorted-insert pattern (see SortedListProjection).
        const q = this.#queue;
        const i = binarySearchHighIndex(q, frame, jitter_compare_frame);
        q.splice(i, 0, { frame, payload });
    }

    /**
     * Release every payload whose frame is `<= current_frame - delay_frames`.
     * Callback receives `(frame, payload)` in frame-ascending order.
     *
     * @param {number} current_frame
     * @param {function(number, *): void} callback
     * @returns {number} number of payloads released
     */
    drain_until(current_frame, callback) {
        assert.isNonNegativeInteger(current_frame, 'current_frame');
        assert.isFunction(callback, 'callback');

        const threshold = current_frame - this.delay_frames;
        let released = 0;
        const q = this.#queue;
        while (q.length > 0 && q[0].frame <= threshold) {
            const entry = q.shift();
            callback(entry.frame, entry.payload);
            released++;
        }
        return released;
    }

    /**
     * Discard everything in the buffer (e.g. on disconnect).
     */
    clear() {
        this.#queue.length = 0;
    }

    /**
     * Number of payloads currently held.
     * @returns {number}
     */
    size() {
        return this.#queue.length;
    }

    /**
     * Frame of the oldest payload, or `-1` if empty.
     * @returns {number}
     */
    earliest_frame() {
        return this.#queue.length === 0 ? -1 : this.#queue[0].frame;
    }
}
