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

/**
 * Per-frame ring of action records.
 *
 * Each frame's `BinaryBuffer` holds a sequence of action records. One record per
 * `SimActionExecutor.execute` call:
 *
 * ```
 *   varint:    prior_state_count
 *   loop:
 *     varint:  entity_id
 *     uint8:   component_type_id   (assigned by the ReplicatedComponentRegistry)
 *     uint32:  prior_payload_len
 *     bytes:   prior_payload       (adapter.serialize of the component's prior state)
 *   uint8:     action_type_id
 *   uint8:     sender_id           (peer that originated the action; local-only,
 *                                   STRIPPED by Replicator before send)
 *   uint32:    action_payload_len
 *   bytes:     action_payload      (the action's own serialize output)
 * ```
 *
 * Self-describing: records can be skipped without consulting the action registry,
 * which means rewind code can walk records forward to find boundaries, then
 * iterate backward to apply prior states — no need to instantiate any actions.
 *
 * Note that `sender_id` is recorded in-buffer for local rollback orchestrators
 * (stable-sort tie-breaking by sender on replay) but never crosses the wire —
 * {@link Replicator#pack_for_peer} strips it and the receiver derives sender
 * from the inbound packet's peer_id, so a hostile peer cannot impersonate.
 *
 * Ring depth is set at construction. When the ring fills, the oldest frame's
 * buffer is recycled for the new frame.
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
export class ActionLog {
    #buffers;
    #frame_numbers;
    #write_ends;
    #current_frame;


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

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

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

        /**
         * Per-frame absolute frame numbers (so we can detect "this slot is stale").
         * `-1` means the slot has not yet been used.
         * @type {Int32Array}
         * @private
         */
        this.#frame_numbers = new Int32Array(frame_capacity).fill(-1);

        /**
         * Per-frame "write end" position. The buffer's own `position` is reused
         * when actively writing; this is captured at frame_end so readers know
         * where the meaningful data ends.
         * @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);
        }

        /**
         * Frame number currently being written to, or -1 if no frame is open.
         * @type {number}
         * @private
         */
        this.#current_frame = -1;
    }

    /**
     * Open a new frame for writing. Recycles the oldest slot if the ring is full.
     *
     * @param {number} frame absolute frame number
     */
    begin_frame(frame) {
        assert.isNonNegativeInteger(frame, 'frame');

        if (this.#current_frame !== -1) {
            throw new Error(`ActionLog.begin_frame: previous frame ${this.#current_frame} still open; call end_frame first`);
        }

        const slot = frame % this.frame_capacity;
        const buffer = this.#buffers[slot];

        buffer.position = 0;
        this.#frame_numbers[slot] = frame;
        this.#current_frame = frame;
    }

    /**
     * Close the currently-open frame.
     */
    end_frame() {
        if (this.#current_frame === -1) {
            throw new Error(`ActionLog.end_frame: no frame is open`);
        }
        const slot = this.#current_frame % this.frame_capacity;
        this.#write_ends[slot] = this.#buffers[slot].position;
        this.#current_frame = -1;
    }

    /**
     * Get the buffer for the currently-open frame for direct writing.
     *
     * @returns {BinaryBuffer}
     */
    current_buffer() {
        if (this.#current_frame === -1) {
            throw new Error(`ActionLog.current_buffer: no frame is open`);
        }
        return this.#buffers[this.#current_frame % this.frame_capacity];
    }

    /**
     * Has the given frame number ever been written, and is it still in the ring?
     *
     * @param {number} frame
     * @returns {boolean}
     */
    has_frame(frame) {
        assert.isNonNegativeInteger(frame, 'frame');
        const slot = frame % this.frame_capacity;
        return this.#frame_numbers[slot] === frame;
    }

    /**
     * Read-only access to a closed frame's buffer. Throws if the frame is not in the ring.
     * The buffer's `position` will be set to 0 and its valid byte length is `write_end_for(frame)`.
     *
     * **Only valid for frames that have been closed via {@link end_frame}.** While a
     * frame is open, `__write_ends[slot]` still holds the previous occupant's
     * length — `buffer_for`/`write_end_for` would return mismatched
     * buffer-contents-vs-length and any reader would walk garbage.
     *
     * @param {number} frame
     * @returns {BinaryBuffer}
     */
    buffer_for(frame) {
        assert.isNonNegativeInteger(frame, 'frame');
        if (!this.has_frame(frame)) {
            throw new Error(`ActionLog.buffer_for: frame ${frame} is not present in the ring`);
        }
        const slot = frame % this.frame_capacity;
        const buffer = this.#buffers[slot];
        buffer.position = 0;
        return buffer;
    }

    /**
     * Number of valid bytes written to the given frame's buffer.
     * @param {number} frame
     * @returns {number}
     */
    write_end_for(frame) {
        assert.isNonNegativeInteger(frame, 'frame');
        if (!this.has_frame(frame)) {
            throw new Error(`ActionLog.write_end_for: frame ${frame} is not present in the ring`);
        }
        const slot = frame % this.frame_capacity;
        return this.#write_ends[slot];
    }
}
