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

/**
 * Maps replicated component classes to compact `uint8` type IDs and to their
 * replication adapters. Sits on top of the engine's existing
 * {@link BinarySerializationRegistry}, which keys adapters by class name (string).
 *
 * Two reasons this exists separately:
 *   1. The action-log wire format wants 1-byte type tags, not strings.
 *   2. The "set of components that get replicated" is a strict subset of "all
 *      classes that have a binary adapter" (e.g. save-game components that we
 *      don't replicate). Registering for replication is an explicit opt-in.
 *
 * Registration is order-sensitive: `type_id` is assigned in registration order
 * starting from 0. To keep `type_id` stable across versions, register in a
 * fixed order at engine init.
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
export class ReplicatedComponentRegistry {
    #binary_registry;
    #classes_by_id;
    #id_by_class;
    #adapters_by_id;


    /**
     * @param {BinarySerializationRegistry} binary_registry
     */
    constructor(binary_registry) {
        /**
         * @type {BinarySerializationRegistry}
         * @private
         */
        this.#binary_registry = binary_registry;

        /**
         * Component classes indexed by assigned type_id.
         * @type {Function[]}
         * @private
         */
        this.#classes_by_id = [];

        /**
         * `class -> type_id` lookup. Class identity is the key.
         * @type {Map<Function, number>}
         * @private
         */
        this.#id_by_class = new Map();

        /**
         * `type_id -> adapter` cache so we don't re-hash the className every lookup.
         * @type {BinaryClassSerializationAdapter[]}
         * @private
         */
        this.#adapters_by_id = [];
    }

    /**
     * Register a component class for replication. Looks up the adapter from the
     * underlying binary registry by class typeName and assigns a stable type_id.
     *
     * @param {Function} component_class must have `typeName` static and be registered
     *                                   in the binary serialization registry
     * @returns {number} assigned type_id
     */
    register(component_class) {
        assert.isFunction(component_class, 'component_class');
        if (this.#id_by_class.has(component_class)) {
            throw new Error(`ReplicatedComponentRegistry.register: ${component_class.typeName ?? component_class.name} already registered`);
        }

        const type_name = component_class.typeName;
        if (typeof type_name !== "string") {
            throw new Error(`ReplicatedComponentRegistry.register: ${component_class.name} has no static .typeName`);
        }

        const adapter = this.#binary_registry.getAdapter(type_name);
        if (adapter === undefined) {
            throw new Error(`ReplicatedComponentRegistry.register: no adapter registered for '${type_name}' in BinarySerializationRegistry`);
        }

        const type_id = this.#classes_by_id.length;
        if (type_id > 0xFF) {
            throw new Error(`ReplicatedComponentRegistry: at most 256 component types are supported, tried to register ${type_id + 1}`);
        }

        this.#classes_by_id[type_id] = component_class;
        this.#adapters_by_id[type_id] = adapter;
        this.#id_by_class.set(component_class, type_id);

        return type_id;
    }

    /**
     * @param {Function} component_class
     * @returns {number} type_id, or -1 if unregistered
     */
    type_id_of(component_class) {
        assert.isFunction(component_class, 'component_class');
        const id = this.#id_by_class.get(component_class);
        return id === undefined ? -1 : id;
    }

    /**
     * @param {number} type_id
     * @returns {Function|undefined}
     */
    class_of(type_id) {
        assert.isNonNegativeInteger(type_id, 'type_id');
        return this.#classes_by_id[type_id];
    }

    /**
     * @param {number} type_id
     * @returns {BinaryClassSerializationAdapter|undefined}
     */
    adapter_for_id(type_id) {
        assert.isNonNegativeInteger(type_id, 'type_id');
        return this.#adapters_by_id[type_id];
    }

    /**
     * @param {Function} component_class
     * @returns {BinaryClassSerializationAdapter|undefined}
     */
    adapter_for_class(component_class) {
        assert.isFunction(component_class, 'component_class');
        const id = this.#id_by_class.get(component_class);
        return id === undefined ? undefined : this.#adapters_by_id[id];
    }

    /**
     * @returns {number}
     */
    type_count() {
        return this.#classes_by_id.length;
    }
}
