Source: encoder.class.js

// encoder.class.js
import wasmBinaryEncoder from './encoder.mvp.wasm';

// Import constants from index.js (assuming index.js is in the same directory or adjust path)
// If published, users would import constants from the package root.
import * as CTE from './index.js';

// No module-scoped WASM instance/memory variables anymore

/**
 * @class CteEncoder
 * @classdesc Provides a fluent interface to encode data according to the CTE specification.
 * Each instance manages its own independent WASM encoder module instance and memory.
 * Must be instantiated asynchronously using the static `create` method.
 */
export class CteEncoder {
    #wasmInstance = null;
    #wasmMemory = null;
    #wasmExports = null;
    #encoderHandle = 0; // Pointer to C struct cte_encoder_t*

    /**
     * @private
     * @description Internal constructor. Use static `CteEncoder.create()` method instead.
     * @param {WebAssembly.Instance} wasmInstance - The instantiated encoder WASM module for this object.
     * @param {DataView} wasmMemory - A DataView for this instance's WASM memory.
     * @param {number} encoderHandle - The pointer (handle) to the C encoder context (`cte_encoder_t*`).
     */
    constructor(wasmInstance, wasmMemory, encoderHandle) {
        this.#wasmInstance = wasmInstance;
        this.#wasmMemory = wasmMemory;
        this.#wasmExports = wasmInstance.exports;
        this.#encoderHandle = encoderHandle;
    }

    /**
     * @static
     * @async
     * @description Asynchronously creates and initializes a new, independent CTE encoder instance.
     * Loads and instantiates a fresh copy of the encoder WASM module for this instance.
     * @param {number} capacity - The fixed buffer capacity in bytes for the encoder. Should be large enough for the expected transaction.
     * @returns {Promise<CteEncoder>} A promise that resolves to the initialized CteEncoder instance.
     * @throws {Error} If WASM binary is invalid, instantiation fails, exports are missing, or C-level encoder initialization (`cte_encoder_init`) fails.
     * @example
     * import { CteEncoder } from '@leachain/ctejs-core'; // Use package name
     *
     * async function main() {
     * try {
     * // Each call creates a new WASM instance
     * const encoder1 = await CteEncoder.create(1024);
     * const encoder2 = await CteEncoder.create(2048);
     * console.log('Encoders ready!');
     * } catch (err) {
     * console.error("Failed to create encoder:", err);
     * }
     * }
     * main();
     */
    static async create(capacity) {
        // --- WASM Instantiation moved inside create ---
        const importObject = {
            env: {
                abort: () => {
                    throw new Error(`WASM Encoder aborted`);
                },
            },
        };
        const requiredExports = [
            'memory',
            'get_public_key_size',
            'get_signature_item_size', //
            'cte_encoder_init',
            'cte_encoder_reset',
            'cte_encoder_get_data',
            'cte_encoder_get_size', //
            'cte_encoder_begin_public_key_list',
            'cte_encoder_begin_signature_list', //
            'cte_encoder_write_ixdata_index_reference',
            'cte_encoder_write_ixdata_uleb128', //
            'cte_encoder_write_ixdata_sleb128',
            'cte_encoder_write_ixdata_int8', //
            'cte_encoder_write_ixdata_uint8',
            'cte_encoder_write_ixdata_int16', //
            'cte_encoder_write_ixdata_uint16',
            'cte_encoder_write_ixdata_int32', //
            'cte_encoder_write_ixdata_uint32',
            'cte_encoder_write_ixdata_int64', //
            'cte_encoder_write_ixdata_uint64',
            'cte_encoder_write_ixdata_float32', //
            'cte_encoder_write_ixdata_float64',
            'cte_encoder_write_ixdata_boolean', //
            'cte_encoder_begin_command_data', //
        ];

        // Instantiate a new WASM module for *this* encoder object
        const { instance } = await WebAssembly.instantiate(wasmBinaryEncoder, importObject);
        const memory = new DataView(instance.exports.memory.buffer);

        // Check exports for this instance
        for (const exportName of requiredExports) {
            if (!(exportName in instance.exports)) {
                throw new Error(`WASM Encoder module instance is missing required export: ${exportName}`);
            }
        }
        // --- End WASM Instantiation ---

        const handle = instance.exports.cte_encoder_init(capacity); //
        if (!handle) {
            throw new Error('Failed to create CTE encoder handle in WASM');
        }

        // Create the JS object, passing the specific WASM instance/memory/handle
        return new CteEncoder(instance, memory, handle);
    }

    /**
     * @private
     * @description Refreshes the internal DataView reference to this instance's WASM memory buffer.
     */
    #refreshMemoryView() {
        // Check if the memory buffer associated with *this* instance has changed
        if (this.#wasmMemory.buffer !== this.#wasmInstance.exports.memory.buffer) {
            this.#wasmMemory = new DataView(this.#wasmInstance.exports.memory.buffer);
        }
    }

    /**
     * @description Resets the encoder state for this instance, allowing reuse of its allocated buffer.
     * @returns {this} The encoder instance for chaining.
     * @throws {Error} If the encoder instance handle is invalid (e.g., after destroy).
     */
    reset() {
        if (!this.#encoderHandle) throw new Error('Encoder handle invalid (destroyed?).');
        this.#wasmExports.cte_encoder_reset(this.#encoderHandle); //
        this.#refreshMemoryView();
        return this;
    }

    /**
     * @description Retrieves the currently encoded data from this instance as a byte array.
     * Returns a copy. Terminates an encoding chain.
     * @returns {Uint8Array} A copy of the encoded data bytes.
     * @throws {Error} If the encoder instance handle is invalid or WASM memory access fails.
     */
    getEncodedData() {
        if (!this.#encoderHandle) throw new Error('Encoder handle invalid (destroyed?).');
        const dataPtr = this.#wasmExports.cte_encoder_get_data(this.#encoderHandle); //
        const size = this.#wasmExports.cte_encoder_get_size(this.#encoderHandle); //
        if (!dataPtr && size > 0) {
            throw new Error('WASM get_data returned null pointer but size > 0.');
        }
        if (size === 0) {
            return new Uint8Array(0);
        }
        this.#refreshMemoryView(); // Ensure memory view is current before reading
        if (dataPtr + size > this.#wasmMemory.buffer.byteLength) {
            throw new Error(`WASM memory access error reading encoded data`);
        }
        return new Uint8Array(this.#wasmMemory.buffer.slice(dataPtr, dataPtr + size));
    }

    // --- Size Helpers ---
    /**
     * Gets the expected size in bytes for a signature/hash item based on crypto type code,
     * using this instance's WASM module.
     * @param {number} typeCode - The crypto type code (e.g., `CTE.CTE_CRYPTO_TYPE_ED25519`).
     * @returns {number} The size in bytes.
     * @throws {Error} If the encoder instance handle is invalid or the WASM function returns an invalid size.
     */
    getSignatureItemSize(typeCode) {
        if (!this.#encoderHandle) throw new Error('Encoder not initialized or destroyed.');
        const size = this.#wasmExports.get_signature_item_size(typeCode); //
        if (size <= 0) throw new Error(`WASM get_signature_item_size invalid size (${size}) for type ${typeCode}`);
        return size;
    }
    /**
     * Gets the expected size in bytes for a public key item based on crypto type code,
     * using this instance's WASM module.
     * @param {number} typeCode - The crypto type code (e.g., `CTE.CTE_CRYPTO_TYPE_ED25519`).
     * @returns {number} The size in bytes.
     * @throws {Error} If the encoder instance handle is invalid or the WASM function returns an invalid size.
     */
    getPublicKeySize(typeCode) {
        if (!this.#encoderHandle) throw new Error('Encoder not initialized or destroyed.');
        const size = this.#wasmExports.get_public_key_size(typeCode); //
        if (size <= 0) throw new Error(`WASM get_public_key_size invalid size (${size}) for type ${typeCode}`);
        return size;
    }

    // --- Encoding Methods (Implementations use this.#wasmExports etc.) ---
    /** @private */
    #beginAndWriteListData(beginFuncName, items, typeCode, getWasmItemSizeFunc, listName) {
        if (!this.#encoderHandle) throw new Error('Encoder handle invalid.');
        if (!Array.isArray(items) || items.length < 1 || items.length > CTE.CTE_LIST_MAX_LEN) {
            throw new Error(`Invalid ${listName} list size`);
        } //
        const itemCount = items.length;
        const itemSize = this.#wasmExports[getWasmItemSizeFunc](typeCode); // Call on instance exports
        if (itemSize <= 0) {
            throw new Error(`Invalid ${listName} item size ${itemSize}`);
        }
        const expectedTotalItemSize = itemCount * itemSize;
        const writePtr = this.#wasmExports[beginFuncName](this.#encoderHandle, itemCount, typeCode); //
        if (!writePtr) {
            throw new Error(`Begin ${listName} list failed in WASM`);
        }
        this.#refreshMemoryView();
        if (writePtr + expectedTotalItemSize > this.#wasmMemory.buffer.byteLength) {
            throw new Error(`WASM overflow preparing ${listName}`);
        }
        const memoryBytesView = new Uint8Array(this.#wasmMemory.buffer);
        let currentOffset = writePtr;
        for (const item of items) {
            if (!(item instanceof Uint8Array) || item.length !== itemSize) {
                throw new Error(`Invalid ${listName} item: Expected Uint8Array size ${itemSize}.`);
            }
            memoryBytesView.set(item, currentOffset);
            currentOffset += itemSize;
        }
        return this; // Return this for chaining
    }

    /** @returns {this} */
    addPublicKeyList(keys, typeCode) {
        return this.#beginAndWriteListData(
            'cte_encoder_begin_public_key_list',
            keys,
            typeCode,
            'get_public_key_size',
            'PublicKey'
        );
    } //
    /** @returns {this} */
    addSignatureList(signatures, typeCode) {
        return this.#beginAndWriteListData(
            'cte_encoder_begin_signature_list',
            signatures,
            typeCode,
            'get_signature_item_size',
            'Signature'
        );
    } //

    /** @returns {this} */
    addIxDataIndexReference(index) {
        if (!this.#encoderHandle) throw new Error('Encoder handle invalid.');
        if (
            typeof index !== 'number' ||
            index < 0 ||
            index > CTE.CTE_LEGACY_INDEX_MAX_VALUE ||
            !Number.isInteger(index)
        ) {
            throw new Error(`Invalid legacy index`);
        } //
        this.#wasmExports.cte_encoder_write_ixdata_index_reference(this.#encoderHandle, index); //
        return this;
    }
    /** @private @returns {this} */
    #writeSimpleIxData(fn, v, t, c = null) {
        if (!this.#encoderHandle) throw new Error('Encoder handle invalid.');
        if (c && !c(v)) {
            throw new Error(`Invalid IxData ${t}: Val ${v}`);
        }
        this.#wasmExports[fn](this.#encoderHandle, v);
        return this;
    } //
    /** @returns {this} */
    addIxDataUleb128(v) {
        return this.#writeSimpleIxData(
            'cte_encoder_write_ixdata_uleb128',
            BigInt(v),
            'ULEB128',
            (x) => typeof x === 'bigint' && x >= 0n
        );
    } //
    /** @returns {this} */
    addIxDataSleb128(v) {
        return this.#writeSimpleIxData(
            'cte_encoder_write_ixdata_sleb128',
            BigInt(v),
            'SLEB128',
            (x) => typeof x === 'bigint'
        );
    } //
    /** @returns {this} */
    addIxDataInt8(v) {
        return this.#writeSimpleIxData(
            'cte_encoder_write_ixdata_int8',
            v,
            'Int8',
            (x) => Number.isInteger(x) && x >= -128 && x <= 127
        );
    } //
    /** @returns {this} */
    addIxDataUint8(v) {
        return this.#writeSimpleIxData(
            'cte_encoder_write_ixdata_uint8',
            v,
            'Uint8',
            (x) => Number.isInteger(x) && x >= 0 && x <= 255
        );
    } //
    /** @returns {this} */
    addIxDataInt16(v) {
        return this.#writeSimpleIxData(
            'cte_encoder_write_ixdata_int16',
            v,
            'Int16',
            (x) => Number.isInteger(x) && x >= -32768 && x <= 32767
        );
    } //
    /** @returns {this} */
    addIxDataUint16(v) {
        return this.#writeSimpleIxData(
            'cte_encoder_write_ixdata_uint16',
            v,
            'Uint16',
            (x) => Number.isInteger(x) && x >= 0 && x <= 65535
        );
    } //
    /** @returns {this} */
    addIxDataInt32(v) {
        return this.#writeSimpleIxData(
            'cte_encoder_write_ixdata_int32',
            v,
            'Int32',
            (x) => Number.isInteger(x) && x >= -(2 ** 31) && x <= 2 ** 31 - 1
        );
    } //
    /** @returns {this} */
    addIxDataUint32(v) {
        return this.#writeSimpleIxData(
            'cte_encoder_write_ixdata_uint32',
            v,
            'Uint32',
            (x) => Number.isInteger(x) && x >= 0 && x <= 2 ** 32 - 1
        );
    } //
    /** @returns {this} */
    addIxDataInt64(v) {
        return this.#writeSimpleIxData(
            'cte_encoder_write_ixdata_int64',
            BigInt(v),
            'Int64',
            (x) => typeof x === 'bigint'
        );
    } //
    /** @returns {this} */
    addIxDataUint64(v) {
        return this.#writeSimpleIxData(
            'cte_encoder_write_ixdata_uint64',
            BigInt(v),
            'Uint64',
            (x) => typeof x === 'bigint' && x >= 0n
        );
    } //
    /** @returns {this} */
    addIxDataFloat32(v) {
        return this.#writeSimpleIxData('cte_encoder_write_ixdata_float32', v, 'Float32', (x) => typeof x === 'number');
    } //
    /** @returns {this} */
    addIxDataFloat64(v) {
        return this.#writeSimpleIxData('cte_encoder_write_ixdata_float64', v, 'Float64', (x) => typeof x === 'number');
    } //
    /** @returns {this} */
    addIxDataBoolean(v) {
        return this.#writeSimpleIxData('cte_encoder_write_ixdata_boolean', !!v, 'Boolean');
    } //

    /**
     * Adds a Command Data field. Requires input as `Uint8Array`.
     * @param {Uint8Array} data - The command payload bytes. Length must not exceed `CTE.CTE_COMMAND_EXTENDED_MAX_LEN`.
     * @returns {this} The encoder instance for chaining.
     * @throws {Error} If data is not a `Uint8Array` or exceeds max length, or WASM fails.
     */
    addCommandData(data) {
        // Requires Uint8Array
        if (!this.#encoderHandle) throw new Error('Encoder handle invalid.');
        if (!(data instanceof Uint8Array)) {
            throw new Error('Command data must be a Uint8Array.');
        }
        const bytes = data;
        const len = bytes.length;
        if (len > CTE.CTE_COMMAND_EXTENDED_MAX_LEN) {
            throw new Error(`Cmd data too long: ${len} > ${CTE.CTE_COMMAND_EXTENDED_MAX_LEN}`);
        } //
        const ptr = this.#wasmExports.cte_encoder_begin_command_data(this.#encoderHandle, len); //
        if (!ptr) {
            throw new Error(`Begin command data failed`);
        }
        this.#refreshMemoryView();
        if (ptr + len > this.#wasmMemory.buffer.byteLength) {
            throw new Error(`WASM overflow for Command Data`);
        }
        new Uint8Array(this.#wasmMemory.buffer).set(bytes, ptr);
        return this;
    }

    /**
     * @description Cleans up Javascript references associated with this encoder instance.
     * Does not explicitly free WASM memory. Future calls to this instance will fail.
     */
    destroy() {
        this.#encoderHandle = 0;
        this.#wasmExports = null;
        this.#wasmInstance = null; // Release reference to instance
        this.#wasmMemory = null;
    }
}