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

/**
 * Compute a 32-bit fingerprint of every replicated component on every entity
 * in the world. Used by `SyncTest` and as a general "did the world state change?"
 * primitive.
 *
 * The hash is FNV-1a — fast, stable, no dependencies. Not cryptographic.
 *
 * Iteration order: entities in the order yielded by `world` (the
 * EntityComponentDataset's iterator), then components in `type_id` order.
 * As long as both sides iterate identically, the fingerprint is comparable.
 *
 * @param {EntityComponentDataset} world
 * @param {ReplicatedComponentRegistry} component_registry
 * @param {BinaryBuffer} [scratch] optional reusable scratch; resets to position 0 each call
 * @returns {number} 32-bit unsigned hash
 */
export function fingerprint_world(world, component_registry, scratch) {
    assert.ok(world, 'world required');
    assert.ok(component_registry, 'component_registry required');

    const buffer = scratch === undefined ? new BinaryBuffer() : scratch;
    let h = 0x811c9dc5; // FNV-1a 32-bit offset basis

    const type_count = component_registry.type_count();
    for (const entity of world) {
        for (let type_id = 0; type_id < type_count; type_id++) {
            const klass = component_registry.class_of(type_id);
            if (!world.hasComponent(entity, klass)) continue;

            const adapter = component_registry.adapter_for_id(type_id);
            buffer.position = 0;
            adapter.serialize(buffer, world.getComponent(entity, klass));

            // Mix entity_id and type_id into the hash so structurally-identical
            // components on different entities/types produce different prints.
            h = __fnv_byte(h, entity & 0xFF);
            h = __fnv_byte(h, (entity >> 8) & 0xFF);
            h = __fnv_byte(h, (entity >> 16) & 0xFF);
            h = __fnv_byte(h, (entity >> 24) & 0xFF);
            h = __fnv_byte(h, type_id);

            const bytes = buffer.raw_bytes;
            const len = buffer.position;
            for (let i = 0; i < len; i++) {
                h = __fnv_byte(h, bytes[i]);
            }
        }
    }
    return h >>> 0;
}

/**
 * @private
 */
function __fnv_byte(h, byte) {
    h ^= byte;
    return Math.imul(h, 0x01000193) >>> 0;
}

/**
 * Diagnostic harness for catching rewind / replay bugs.
 *
 * Usage:
 *   - `harness.checkpoint()` — records the current world fingerprint.
 *   - … run tick logic …
 *   - `harness.assert_recoverable_to_checkpoint(rewind_engine, current_frame, target_frame)`
 *     rewinds and asserts the world's fingerprint matches the checkpoint.
 *
 * For full nondeterminism detection (GGPO-style "save → advance → load →
 * advance → diff"), the application's tick logic must be re-runnable; that
 * coordination is left to the caller. This harness provides the fingerprint +
 * compare primitives and the assertion shape.
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
export class SyncTest {
    #scratch;
    #checkpoint_hash;
    #has_checkpoint;


    /**
     * @param {{
     *   world: EntityComponentDataset,
     *   component_registry: ReplicatedComponentRegistry,
     * }} options
     */
    constructor({ world, component_registry }) {
        assert.ok(world, 'world required');
        assert.ok(component_registry, 'component_registry required');

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

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

        /** Reused scratch for fingerprint serialization. */
        this.#scratch = new BinaryBuffer();

        /** @type {number} @private */
        this.#checkpoint_hash = 0;

        /** @type {boolean} @private */
        this.#has_checkpoint = false;
    }

    /**
     * Capture the current world state as the reference point for the next
     * `assert_recoverable_to_checkpoint` call.
     */
    checkpoint() {
        this.#checkpoint_hash = fingerprint_world(this.world, this.component_registry, this.#scratch);
        this.#has_checkpoint = true;
    }

    /**
     * @returns {number}
     */
    current_fingerprint() {
        return fingerprint_world(this.world, this.component_registry, this.#scratch);
    }

    /**
     * Rewind from `current_frame` back to `target_frame` (typically the frame
     * at which `checkpoint()` was called) and assert the resulting world state
     * matches the checkpoint.
     *
     * Throws on mismatch with a diagnostic message including both fingerprints.
     *
     * @param {RewindEngine} rewind_engine
     * @param {number} current_frame
     * @param {number} target_frame
     */
    assert_recoverable_to_checkpoint(rewind_engine, current_frame, target_frame) {
        assert.ok(this.#has_checkpoint, 'SyncTest: no checkpoint() taken before assertion');
        rewind_engine.rewind_to(current_frame, target_frame);
        const after = fingerprint_world(this.world, this.component_registry, this.#scratch);
        if (after !== this.#checkpoint_hash) {
            throw new Error(
                `SyncTest: world fingerprint after rewind (${after.toString(16)}) ` +
                `does not match checkpoint (${this.#checkpoint_hash.toString(16)}) ` +
                `— rewind is non-deterministic or did not fully restore state`
            );
        }
    }
}
