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

/**
 * Walks the {@link ActionLog} backwards to restore prior component state.
 *
 * For each frame to undo, the engine:
 *   1. Parses the frame's records forward to find record start positions
 *      (a record's full length isn't fixed; it's discovered by reading headers).
 *   2. Iterates record positions in reverse.
 *   3. For each record, reads its prior_state entries and writes them back into
 *      live components via the replication adapter for that component class.
 *
 * Reverse iteration is necessary because multiple actions in the same frame may
 * have mutated the same component; the most recent mutation must be undone
 * first, then the next-most-recent, and so on, ending at the start-of-frame state.
 *
 * Forward replay is just running the existing simulation tick again — that's the
 * orchestrator's job. RewindEngine only handles the backward direction.
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
export class RewindEngine {
    #record_starts;


    /**
     * @param {{
     *   action_log: ActionLog,
     *   world: EntityComponentDataset,
     *   component_registry: ReplicatedComponentRegistry,
     * }} options
     */
    constructor({ action_log, world, component_registry }) {
        /**
         * @type {ActionLog}
         */
        this.action_log = action_log;

        /**
         * @type {EntityComponentDataset}
         */
        this.world = world;

        /**
         * @type {ReplicatedComponentRegistry}
         */
        this.component_registry = component_registry;

        /**
         * Pre-allocated scratch for record-start positions. Grown on demand.
         * @type {Int32Array}
         * @private
         */
        this.#record_starts = new Int32Array(64);
    }

    /**
     * Undo all action records in a single frame, in reverse order.
     * Throws if the frame is not present in the log.
     *
     * @param {number} frame
     */
    undo_frame(frame) {
        assert.isNonNegativeInteger(frame, 'frame');
        if (!this.action_log.has_frame(frame)) {
            throw new Error(`RewindEngine.undo_frame: frame ${frame} is not present in the action log`);
        }

        const buffer = this.action_log.buffer_for(frame);
        const end = this.action_log.write_end_for(frame);

        // 1. First pass: walk forward to find record boundaries.
        let record_count = 0;
        while (buffer.position < end) {
            if (record_count >= this.#record_starts.length) {
                this.#grow_record_starts();
            }
            this.#record_starts[record_count++] = buffer.position;
            this.#skip_record(buffer);
        }

        // 2. Second pass: iterate records in reverse, restoring prior state.
        for (let i = record_count - 1; i >= 0; i--) {
            buffer.position = this.#record_starts[i];
            this.#restore_prior_state(buffer);
            // No need to do anything with the action portion — it stays unapplied.
        }
    }

    /**
     * Undo every frame in `(target_frame, current_frame]` so that the world is
     * left in start-of-`target_frame + 1` state. Equivalently: the world ends
     * up as it was at the END of `target_frame`.
     *
     * Throws if any frame in the range is missing from the log.
     *
     * @param {number} current_frame the most recent frame whose actions are applied
     * @param {number} target_frame the frame whose end-state we want to restore to
     */
    rewind_to(current_frame, target_frame) {
        assert.isNonNegativeInteger(current_frame, 'current_frame');
        assert.isInteger(target_frame, 'target_frame'); // -1 is valid: "rewind everything"
        if (target_frame > current_frame) {
            throw new Error(`RewindEngine.rewind_to: target_frame (${target_frame}) > current_frame (${current_frame})`);
        }
        for (let f = current_frame; f > target_frame; f--) {
            this.undo_frame(f);
        }
    }

    /**
     * Walk past a single record without restoring anything. Buffer position
     * advances to the start of the next record (or to `end` if this was the last).
     *
     * @param {BinaryBuffer} buffer
     * @private
     */
    #skip_record(buffer) {
        // Skip prior_state entries.
        const prior_count = buffer.readUintVar();
        for (let i = 0; i < prior_count; i++) {
            buffer.readUintVar();           // entity_id
            buffer.readUint8();             // component_type_id
            const len = buffer.readUint32();
            buffer.position += len;         // skip prior payload
        }
        // Skip action.
        buffer.readUint8();                 // action_type_id
        buffer.readUint8();                 // sender_id
        const action_len = buffer.readUint32();
        buffer.position += action_len;      // skip action payload
    }

    /**
     * Read the prior_state entries of the record at the buffer's current position
     * and write them back into the live components. Buffer position advances past
     * the prior_state section (the action portion is left unread).
     *
     * @param {BinaryBuffer} buffer
     * @private
     */
    #restore_prior_state(buffer) {
        const prior_count = buffer.readUintVar();
        for (let i = 0; i < prior_count; i++) {
            const entity_id = buffer.readUintVar();
            const component_type_id = buffer.readUint8();
            const payload_len = buffer.readUint32();

            const klass = this.component_registry.class_of(component_type_id);
            const adapter = this.component_registry.adapter_for_id(component_type_id);

            if (klass === undefined || adapter === undefined) {
                // Unknown component type — skip the payload but warn loudly.
                buffer.position += payload_len;
                console.warn(`RewindEngine: unknown component_type_id ${component_type_id} in action log`);
                continue;
            }

            const component = this.world.getComponent(entity_id, klass);
            if (component === undefined) {
                // Component was removed since the action was logged. Skip the payload.
                // (A future "lifecycle" extension would re-attach it; current model
                //  doesn't track structural changes.)
                buffer.position += payload_len;
                continue;
            }

            adapter.deserialize(buffer, component);
        }
    }

    /**
     * @private
     */
    #grow_record_starts() {
        const old = this.#record_starts;
        const next = new Int32Array(old.length * 2);
        next.set(old);
        this.#record_starts = next;
    }
}
