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

/**
 * Per-player ring of input bytes, one frame per slot.
 *
 * An "input" is a game-defined blob of bytes representing a single tick's
 * worth of player intent (button bits, mouse deltas, view angles, etc.). The
 * application owns the encoding; the ring just stores and indexes by frame.
 *
 * Used for:
 *   - **Input redundancy**: the most recent N unacked frames of input get
 *     packed into every outgoing packet from the client, so a single dropped
 *     packet doesn't cause a stutter.
 *   - **Reconciliation replay**: after the client rewinds to an authoritative
 *     state, it replays inputs from `[acked + 1, current]` through the
 *     simulation to catch back up to "now".
 *
 * Distinct from {@link ActionLog} despite a similar shape: inputs are raw
 * player intent (sender side), actions are state mutations (sim side). One
 * input may produce zero or many actions when the simulation processes it.
 *
 * Sized to `frame_capacity` frames; oldest is overwritten on wrap.
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
export class InputRing {
    #buffers;
    #frame_numbers;
    #write_ends;


    /**
     * @param {{ frame_capacity: number, initial_buffer_size?: number }} options
     */
    constructor({ frame_capacity, initial_buffer_size = 64 }) {
        if (!Number.isInteger(frame_capacity) || frame_capacity <= 0) {
            throw new Error(`InputRing: frame_capacity must be a positive integer, got ${frame_capacity}`);
        }

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

        /**
         * Per-frame input buffers. Index by `frame % frame_capacity`.
         * @type {BinaryBuffer[]}
         * @private
         */
        this.#buffers = new Array(frame_capacity);

        /**
         * Per-slot absolute frame number; -1 means slot is empty.
         * @type {Int32Array}
         * @private
         */
        this.#frame_numbers = new Int32Array(frame_capacity).fill(-1);

        /**
         * Per-slot byte length of valid input data.
         * @type {Int32Array}
         * @private
         */
        this.#write_ends = new Int32Array(frame_capacity);

        for (let i = 0; i < frame_capacity; i++) {
            this.#buffers[i] = new BinaryBuffer();
            this.#buffers[i].setCapacity(initial_buffer_size);
        }
    }

    /**
     * Write input bytes for a given frame. The callback receives the buffer
     * positioned at 0 and is expected to write input bytes via the buffer's
     * `writeXxx` methods. The buffer's `position` after the callback is the
     * recorded length; data beyond that is ignored on read.
     *
     * Overwrites any prior data for that frame slot.
     *
     * **Writer must not throw.** The slot's buffer has already been positioned
     * at 0 by the time the writer runs, and the prior frame's bytes are being
     * overwritten in-place. If the writer throws partway, the slot's metadata
     * (frame number, length) still reflects the previous occupant while the
     * leading bytes have been clobbered with the partial new write — a torn
     * state that downstream readers cannot detect. A try/catch wrapper here
     * would force a V8 deopt on the hot write path, which is a worse trade
     * than the "writer is trusted" contract; treat any throw in a writer
     * callback as a programming error.
     *
     * @param {number} frame
     * @param {function(BinaryBuffer): void} writer
     */
    write(frame, writer) {
        assert.isNonNegativeInteger(frame, 'frame');
        assert.isFunction(writer, 'writer');
        const slot = frame % this.frame_capacity;
        const buffer = this.#buffers[slot];
        buffer.position = 0;
        writer(buffer);
        this.#write_ends[slot] = buffer.position;
        this.#frame_numbers[slot] = frame;
    }

    /**
     * @param {number} frame
     * @returns {boolean}
     */
    has(frame) {
        assert.isNonNegativeInteger(frame, 'frame');
        const slot = frame % this.frame_capacity;
        return this.#frame_numbers[slot] === frame;
    }

    /**
     * Read-only access to a frame's buffer. Throws if not present.
     * Caller must respect the byte length reported by {@link write_end_for}.
     *
     * @param {number} frame
     * @returns {BinaryBuffer}
     */
    buffer_for(frame) {
        assert.isNonNegativeInteger(frame, 'frame');
        if (!this.has(frame)) {
            throw new Error(`InputRing.buffer_for: frame ${frame} is not present`);
        }
        const slot = frame % this.frame_capacity;
        const buffer = this.#buffers[slot];
        buffer.position = 0;
        return buffer;
    }

    /**
     * @param {number} frame
     * @returns {number}
     */
    write_end_for(frame) {
        assert.isNonNegativeInteger(frame, 'frame');
        if (!this.has(frame)) {
            throw new Error(`InputRing.write_end_for: frame ${frame} is not present`);
        }
        const slot = frame % this.frame_capacity;
        return this.#write_ends[slot];
    }

    /**
     * Iterate frames in `[start_frame, end_frame]` (inclusive) that are present
     * in the ring. Order is ascending by frame.
     *
     * @param {number} start_frame
     * @param {number} end_frame
     * @param {function(number, BinaryBuffer, number): void} fn callback receives (frame, buffer_at_pos_0, byte_length)
     */
    for_each_in_range(start_frame, end_frame, fn) {
        assert.isNonNegativeInteger(start_frame, 'start_frame');
        assert.isNonNegativeInteger(end_frame, 'end_frame');
        assert.isFunction(fn, 'fn');
        // Clamp `start_frame` so we never iterate further back than the ring
        // could possibly retain. A caller asking for `[0, current_frame]` after
        // a long disconnect would otherwise walk millions of frames just to
        // call `has()` on each — the ring only holds the last `frame_capacity`.
        const earliest_possible = end_frame - this.frame_capacity + 1;
        const lo = start_frame < earliest_possible ? earliest_possible : start_frame;
        for (let f = lo; f <= end_frame; f++) {
            if (this.has(f)) {
                const buffer = this.buffer_for(f);
                fn(f, buffer, this.write_end_for(f));
            }
        }
    }
}
