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

/**
 * Initial-sync state stream generator.
 *
 * When a new peer joins, it needs the current state of the world before any
 * incremental {@link ActionLog} updates make sense. Snapshotter walks every
 * entity in scope, asks each replicated component for its serialized state,
 * and writes a flat record stream into a `BinaryBuffer`. The receiver reads
 * the stream, creates local entities, attaches components, and registers each
 * in the local {@link ReplicationSlotTable} at the matching network_id.
 *
 * This is a one-shot operation, separate from the per-tick action machinery:
 *   - Uses the same {@link BinaryClassSerializationAdapter}s as the action log.
 *   - Does NOT go through the executor — initial sync isn't a reversible mutation.
 *   - Wire format is minimal; no action_type tags, no prior state.
 *
 * Wire format:
 * ```
 *   varint:    entity_count
 *   loop entity_count times:
 *     varint:  network_id
 *     uint8:   component_count
 *     loop component_count times:
 *       uint8:   component_type_id
 *       uint32:  payload_len
 *       bytes:   payload   (adapter.serialize)
 * ```
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */

/**
 * Walk every entity in the scope and write a snapshot stream into `buffer`.
 *
 * @param {{
 *   buffer: BinaryBuffer,
 *   world: EntityComponentDataset,
 *   slot_table: ReplicationSlotTable,
 *   component_registry: ReplicatedComponentRegistry,
 *   entity_iter: function(function(number): void): void,
 * }} options where `entity_iter(cb)` invokes `cb(entity_id)` for each in-scope entity
 */
export function snapshotter_emit({ buffer, world, slot_table, component_registry, entity_iter }) {
    assert.isFunction(entity_iter, 'entity_iter');
    // Two passes: first count entities (so we can write the count up front),
    // then write each. Single-pass with back-patch would also work; counting
    // is cheap and keeps the wire format simpler to read.
    const entity_ids = [];
    entity_iter(id => entity_ids.push(id));

    buffer.writeUintVar(entity_ids.length);

    for (let i = 0; i < entity_ids.length; i++) {
        const entity_id = entity_ids[i];
        const network_id = slot_table.network_for(entity_id);
        if (network_id < 0) {
            throw new Error(`snapshotter_emit: entity ${entity_id} is not registered in the slot table`);
        }

        // Determine how many replicated components this entity has.
        const type_count = component_registry.type_count();
        const present_type_ids = [];
        for (let type_id = 0; type_id < type_count; type_id++) {
            const klass = component_registry.class_of(type_id);
            if (world.hasComponent(entity_id, klass)) {
                present_type_ids.push(type_id);
            }
        }

        buffer.writeUintVar(network_id);
        buffer.writeUint8(present_type_ids.length);

        for (let j = 0; j < present_type_ids.length; j++) {
            const type_id = present_type_ids[j];
            const klass = component_registry.class_of(type_id);
            const adapter = component_registry.adapter_for_id(type_id);
            const component = world.getComponent(entity_id, klass);

            buffer.writeUint8(type_id);

            const len_position = buffer.position;
            buffer.writeUint32(0);
            const payload_start = buffer.position;
            adapter.serialize(buffer, component);
            const payload_end = buffer.position;
            buffer.position = len_position;
            buffer.writeUint32(payload_end - payload_start);
            buffer.position = payload_end;
        }
    }
}

/**
 * Apply a snapshot stream to a (typically empty) world, creating each entity
 * and attaching components.
 *
 * @param {{
 *   buffer: BinaryBuffer,
 *   buffer_end: number,
 *   world: EntityComponentDataset,
 *   slot_table: ReplicationSlotTable,
 *   component_registry: ReplicatedComponentRegistry,
 * }} options buffer is read starting from its current position; reading stops at `buffer_end`
 */
export function snapshotter_apply({ buffer, buffer_end, world, slot_table, component_registry }) {
    assert.isNonNegativeInteger(buffer_end, 'buffer_end');
    const entity_count = buffer.readUintVar();

    for (let i = 0; i < entity_count; i++) {
        const network_id = buffer.readUintVar();
        const component_count = buffer.readUint8();

        const local_entity_id = world.createEntity();
        slot_table.allocate_at(network_id, local_entity_id);

        for (let j = 0; j < component_count; j++) {
            const type_id = buffer.readUint8();
            const payload_len = buffer.readUint32();

            const klass = component_registry.class_of(type_id);
            const adapter = component_registry.adapter_for_id(type_id);

            if (klass === undefined || adapter === undefined) {
                // Unknown component type — skip this payload to stay aligned.
                buffer.position += payload_len;
                continue;
            }

            // Allocate a fresh component instance via the class's no-arg constructor.
            // This relies on the engine convention that components are constructible without args.
            const component = new klass();
            adapter.deserialize(buffer, component);
            world.addComponentToEntity(local_entity_id, component);
        }

        if (buffer.position > buffer_end) {
            throw new Error(`snapshotter_apply: read past buffer_end at network_id ${network_id}`);
        }
    }
}

/**
 * Apply a snapshot stream to existing entities, updating their components in
 * place via `adapter.deserialize`. Wire format is identical to {@link snapshotter_emit};
 * only the apply semantics differ.
 *
 * Used by the recovery flow: server emits the current state of a list of
 * mutated entities; client applies those bytes to its already-existing local
 * counterparts. Entities the receiver doesn't know about are skipped silently
 * (their payload bytes are still consumed so the parser stays aligned).
 *
 * Skipped also: unknown `component_type_id` (forward-compat with newer servers
 * that replicate components the receiver doesn't recognise), and components
 * the local entity doesn't currently have (the receiver hasn't been told to
 * attach this component — recovery doesn't change entity composition, only state).
 *
 * @param {{
 *   buffer: BinaryBuffer,
 *   buffer_end: number,
 *   world: EntityComponentDataset,
 *   slot_table: ReplicationSlotTable,
 *   component_registry: ReplicatedComponentRegistry,
 * }} options
 */
export function snapshotter_apply_to_existing({ buffer, buffer_end, world, slot_table, component_registry }) {
    assert.isNonNegativeInteger(buffer_end, 'buffer_end');
    const entity_count = buffer.readUintVar();

    for (let i = 0; i < entity_count; i++) {
        const network_id = buffer.readUintVar();
        const component_count = buffer.readUint8();

        const local_entity_id = slot_table.entity_for(network_id);

        for (let j = 0; j < component_count; j++) {
            const type_id = buffer.readUint8();
            const payload_len = buffer.readUint32();

            const klass = component_registry.class_of(type_id);
            const adapter = component_registry.adapter_for_id(type_id);

            // Three skip cases — all consume the payload to keep the parser aligned.
            if (klass === undefined || adapter === undefined) {
                buffer.position += payload_len;
                continue;
            }
            if (local_entity_id < 0) {
                buffer.position += payload_len;
                continue;
            }
            const component = world.getComponent(local_entity_id, klass);
            if (component === undefined) {
                buffer.position += payload_len;
                continue;
            }

            adapter.deserialize(buffer, component);
        }

        if (buffer.position > buffer_end) {
            throw new Error(`snapshotter_apply_to_existing: read past buffer_end at network_id ${network_id}`);
        }
    }
}
