import { FunctionCompiler } from "../../../core/function/FunctionCompiler.js";

/**
 * Base class for synchronous, serializable, replicable game-state mutations.
 *
 * Distinct from `core/process/undo/Action.js` (async, designed for editor
 * undo/redo). `SimAction` is the netcode-tier counterpart: sync `apply`,
 * no `revert` (rewind is handled by the executor's prior-bytes capture),
 * and serializable to/from a `BinaryBuffer`.
 *
 * **Contract**: any mutation to replicated state goes through a `SimAction`
 * passed to {@link SimActionExecutor#execute}. Anything else must be a
 * deterministic consequence of the simulation, or rewind / replay /
 * replication will desync.
 *
 * Wire vs local: actions reference entities by `network_id` (peer-shared);
 * `executor.slot_table.entity_for(network_id)` translates to the local id.
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
export class SimAction {

    /**
     * Stable network identifier for this action class. Set by
     * {@link SimActionRegistry.register}. Do not assign manually.
     * @type {number}
     */
    static type_id = -1;

    /**
     * Apply this action's effect to the world. The executor has already captured
     * prior bytes of the components reported by {@link affected_components} before
     * this is called, so direct mutation of those components is safe.
     *
     * @param {EntityComponentDataset} world
     * @param {SimActionExecutor} executor context — exposes `slot_table` for
     *        network_id ↔ entity_id translation, and other framework state
     * @returns {void}
     */
    apply(world, executor) {
        throw new Error("SimAction subclasses must override apply(world, executor).");
    }

    /**
     * Report each (local_entity_id, component_class) pair this action will mutate.
     * The executor invokes `callback(local_entity_id, component_class)` for each
     * and uses the result to capture prior bytes for rewind.
     *
     * If your action stores a `network_id` (the canonical wire form), translate it:
     * ```
     *   affected_components(cb, executor) {
     *     const local = executor.slot_table.entity_for(this.network_id);
     *     cb(local, Transform);
     *   }
     * ```
     *
     * Default: no affected components (e.g. for a pure event-style action).
     *
     * @param {function(number, Function): void} callback
     * @param {SimActionExecutor} executor context
     */
    affected_components(callback, executor) {
        // default: nothing
    }

    /**
     * Serialize this action's forward parameters into the buffer. Do NOT include
     * prior state — the executor captures that via affected_components+adapters.
     *
     * @param {BinaryBuffer} buffer
     * @returns {void}
     */
    serialize(buffer) {
        throw new Error("SimAction subclasses must override serialize(buffer).");
    }

    /**
     * Deserialize forward parameters from the buffer into this instance.
     *
     * @param {BinaryBuffer} buffer
     * @returns {void}
     */
    deserialize(buffer) {
        throw new Error("SimAction subclasses must override deserialize(buffer).");
    }

    /**
     * Reset to a blank state for reuse from the registry's pool.
     * Override if your action holds non-trivial fields.
     */
    reset() {
        // default: nothing
    }
}

/**
 * Fast type check, no instanceof needed.
 * @readonly
 * @type {boolean}
 */
SimAction.prototype.isSimAction = true;

/**
 * Schema-type → code-gen recipe. Each entry supplies the source-string
 * fragments {@link SimAction.extend} concatenates into specialised
 * serialize / deserialize / reset bodies.
 *
 * Same pattern as `core/binary/data_view/buildAccessor.js`.
 */
const SCHEMA_CODEGEN = {
    uint8:   { default_literal: '0',     write_expr: (f) => `buffer.writeUint8(this.${f})`,   read_expr: () => `buffer.readUint8()` },
    uint16:  { default_literal: '0',     write_expr: (f) => `buffer.writeUint16(this.${f})`,  read_expr: () => `buffer.readUint16()` },
    uint32:  { default_literal: '0',     write_expr: (f) => `buffer.writeUint32(this.${f})`,  read_expr: () => `buffer.readUint32()` },
    int8:    { default_literal: '0',     write_expr: (f) => `buffer.writeInt8(this.${f})`,    read_expr: () => `buffer.readInt8()` },
    int16:   { default_literal: '0',     write_expr: (f) => `buffer.writeInt16(this.${f})`,   read_expr: () => `buffer.readInt16()` },
    int32:   { default_literal: '0',     write_expr: (f) => `buffer.writeInt32(this.${f})`,   read_expr: () => `buffer.readInt32()` },
    uintVar: { default_literal: '0',     write_expr: (f) => `buffer.writeUintVar(this.${f})`, read_expr: () => `buffer.readUintVar()` },
    float32: { default_literal: '0',     write_expr: (f) => `buffer.writeFloat32(this.${f})`, read_expr: () => `buffer.readFloat32()` },
    float64: { default_literal: '0',     write_expr: (f) => `buffer.writeFloat64(this.${f})`, read_expr: () => `buffer.readFloat64()` },
    bool:    { default_literal: 'false', write_expr: (f) => `buffer.writeUint8(this.${f} ? 1 : 0)`, read_expr: () => `(buffer.readUint8() !== 0)` },
};

/**
 * Field names rejected by {@link SimAction.extend} because they collide
 * with the generated function bodies (which use `buffer` as a parameter)
 * or with framework methods/properties on `SimAction.prototype`.
 */
const RESERVED_FIELD_NAMES = new Set([
    'buffer', 'constructor', 'prototype', 'this', 'super',
    'apply', 'affected_components', 'serialize', 'deserialize', 'reset',
    'isSimAction',
]);

/**
 * Generate a `SimAction` subclass declaratively. The helper writes the
 * constructor / serialize / deserialize / reset boilerplate via
 * `FunctionCompiler` so each action ends up with a specialised, straight-
 * line function instead of a generic schema-loop.
 *
 * ```js
 * const MoveAction = SimAction.extend({
 *     type: 'Move',
 *     schema: { network_id: 'uintVar', dx: 'float32' },
 *     affects(executor) {
 *         const entity = executor.slot_table.entity_for(this.network_id);
 *         return entity < 0 ? [] : [[entity, Transform]];
 *     },
 *     apply(world, executor) {
 *         const entity = executor.slot_table.entity_for(this.network_id);
 *         if (entity < 0) return;
 *         world.getComponent(entity, Transform).position[0] += this.dx;
 *     },
 * });
 *
 * new MoveAction(network_id, dx);   // positional, in schema-key order
 * ```
 *
 * Schema key insertion order is also wire-byte order and constructor
 * positional order. Schema types must be one of {@link SCHEMA_CODEGEN}'s
 * keys; for more complex shapes (vec3, quat, bitfields, variable length)
 * subclass `SimAction` directly. `type` is retained as
 * `action_type_name` for diagnostics; the wire `type_id` is assigned
 * by the registry at registration time.
 *
 * @param {{
 *     type: string,
 *     schema?: Object<string, string>,
 *     affects?: (executor: SimActionExecutor) => Array<[number, Function]>,
 *     apply: (world: EntityComponentDataset, executor: SimActionExecutor) => void,
 * }} spec
 * @returns {Function} a `SimAction` subclass ready to register
 */
SimAction.extend = function ({ type, schema = {}, affects, apply }) {
    if (typeof type !== 'string' || type.length === 0) {
        throw new Error("SimAction.extend: `type` must be a non-empty string");
    }
    if (typeof apply !== 'function') {
        throw new Error("SimAction.extend: `apply` must be a function");
    }
    if (schema === null || typeof schema !== 'object' || Array.isArray(schema)) {
        throw new Error("SimAction.extend: `schema` must be a plain object {field: 'type'}");
    }

    const field_names = Object.keys(schema);
    for (const name of field_names) {
        if (typeof name !== 'string' || name.length === 0) {
            throw new Error("SimAction.extend: schema field name must be a non-empty string");
        }
        if (RESERVED_FIELD_NAMES.has(name)) {
            throw new Error(`SimAction.extend: '${name}' is a reserved field name and would collide with framework methods/properties`);
        }
        if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name)) {
            throw new Error(`SimAction.extend: schema field name '${name}' is not a valid JS identifier`);
        }
        const type_name = schema[name];
        if (!SCHEMA_CODEGEN[type_name]) {
            throw new Error(`SimAction.extend: unknown schema type '${type_name}' for field '${name}' — supported: ${Object.keys(SCHEMA_CODEGEN).join(', ')}`);
        }
    }

    // Code-gen the four methods as straight-line sequences specialised
    // for this schema.
    const serialize_body = field_names
        .map((name) => `${SCHEMA_CODEGEN[schema[name]].write_expr(name)};`)
        .join('\n');
    const serialize_fn = FunctionCompiler.INSTANCE.compile({
        args: ['buffer'],
        code: serialize_body,
        name: `${type}__serialize`,
    });

    const deserialize_body = field_names
        .map((name) => `this.${name} = ${SCHEMA_CODEGEN[schema[name]].read_expr()};`)
        .join('\n');
    const deserialize_fn = FunctionCompiler.INSTANCE.compile({
        args: ['buffer'],
        code: deserialize_body,
        name: `${type}__deserialize`,
    });

    const reset_body = field_names
        .map((name) => `this.${name} = ${SCHEMA_CODEGEN[schema[name]].default_literal};`)
        .join('\n');
    const reset_fn = FunctionCompiler.INSTANCE.compile({
        args: [],
        code: reset_body,
        name: `${type}__reset`,
    });

    // Constructor body: positional args, undefined → default literal.
    const init_body = field_names
        .map((name) => {
            const lit = SCHEMA_CODEGEN[schema[name]].default_literal;
            return `this.${name} = ${name} !== undefined ? ${name} : ${lit};`;
        })
        .join('\n');
    const init_fn = field_names.length === 0
        ? null
        : FunctionCompiler.INSTANCE.compile({
            args: field_names,
            code: init_body,
            name: `${type}__init`,
        });

    class GeneratedSimAction extends SimAction {
        constructor(...args) {
            super();
            if (init_fn !== null) {
                init_fn.apply(this, args);
            }
        }
        apply(world, executor) {
            return apply.call(this, world, executor);
        }
        affected_components(callback, executor) {
            if (affects === undefined) return;
            const pairs = affects.call(this, executor);
            if (!pairs) return;
            for (let i = 0; i < pairs.length; i++) {
                callback(pairs[i][0], pairs[i][1]);
            }
        }
    }

    GeneratedSimAction.prototype.serialize = serialize_fn;
    GeneratedSimAction.prototype.deserialize = deserialize_fn;
    GeneratedSimAction.prototype.reset = reset_fn;

    GeneratedSimAction.action_type_name = type;

    return GeneratedSimAction;
};
