"use strict";
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-require-imports */
/* eslint-disable @typescript-eslint/no-unused-vars */
/**
 * Standard WASM Loader
 *
 * Loads WASI P1 plugins (AssemblyScript and Rust library plugins)
 */
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.loadStandardPlugin = loadStandardPlugin;
const debug_1 = __importDefault(require("debug"));
const loader_1 = __importDefault(require("@assemblyscript/loader"));
const env_imports_1 = require("../bindings/env-imports");
const resource_provider_1 = require("../bindings/resource-provider");
const weather_provider_1 = require("../bindings/weather-provider");
const radar_provider_1 = require("../bindings/radar-provider");
const fetch_wrapper_1 = require("../utils/fetch-wrapper");
const debug = (0, debug_1.default)('signalk:wasm:loader:standard');
// Use Node.js built-in WASI (Node 20+ required)
const { WASI } = require('node:wasi');
/**
 * Load a standard WASI P1 plugin (AssemblyScript or Rust library)
 */
async function loadStandardPlugin(pluginId, wasmPath, wasmBuffer, vfsRoot, capabilities, app) {
    debug(`Loading standard WASM plugin: ${pluginId} from ${wasmPath}`);
    // Create WASI instance with VFS isolation
    debug(`Creating WASI instance for ${pluginId}`);
    const wasi = new WASI({
        version: 'preview1',
        env: { PLUGIN_ID: pluginId },
        args: [],
        preopens: { '/': vfsRoot }
    });
    debug(`WASI instance created`);
    // Compile WASM module
    debug(`Compiling WASM module for inspection...`);
    let module;
    try {
        module = await WebAssembly.compile(wasmBuffer);
        debug(`WASM module compiled successfully`);
    }
    catch (compileError) {
        debug(`WASM compilation failed: ${compileError}`);
        throw compileError;
    }
    // Inspect module to determine plugin type
    const imports = WebAssembly.Module.imports(module);
    const moduleExports = WebAssembly.Module.exports(module);
    debug(`Module has ${imports.length} imports, ${moduleExports.length} exports`);
    debug.enabled &&
        debug(`Module imports: ${JSON.stringify(imports.map((i) => `${i.module}.${i.name}`).slice(0, 20))}`);
    // Detect plugin type
    // Note: plugin_id is optional since ID can be derived from package.json name
    const hasPluginId = moduleExports.some((e) => e.name === 'plugin_id');
    const hasPluginName = moduleExports.some((e) => e.name === 'plugin_name');
    const hasPluginStart = moduleExports.some((e) => e.name === 'plugin_start');
    const hasAllocate = moduleExports.some((e) => e.name === 'allocate');
    const hasStart = moduleExports.some((e) => e.name === '_start');
    const isRustLibraryPlugin = hasPluginId && hasAllocate;
    const isRustPlugin = hasStart;
    // AssemblyScript plugins must have plugin_name and plugin_start (plugin_id is optional)
    const isAssemblyScriptPlugin = (hasPluginId || (hasPluginName && hasPluginStart)) &&
        !hasAllocate &&
        !hasStart;
    debug(`Plugin type detection: AS=${isAssemblyScriptPlugin}, RustLib=${isRustLibraryPlugin}, RustCmd=${isRustPlugin}`);
    // Get WASI imports
    const wasiImports = (wasi.getImportObject ? wasi.getImportObject() : wasi.getImports(module));
    debug(`Got WASI imports`);
    // Refs that will be populated after instantiation
    const memoryRef = { current: null };
    const rawExportsRef = { current: null };
    const asLoaderRef = { current: null };
    // Create environment imports
    const envImports = (0, env_imports_1.createEnvImports)({
        pluginId,
        capabilities,
        app,
        memoryRef,
        rawExports: rawExportsRef,
        asLoaderInstance: asLoaderRef
    });
    // Initialize as-fetch handler for network capability
    let fetchHandler = null;
    let fetchImports = {};
    if (capabilities.network) {
        debug(`Setting up as-fetch handler for network capability`);
        const nodeFetch = (0, fetch_wrapper_1.getNodeFetch)();
        // Create a wrapper that reads strings from WASM memory
        const fetchWrapper = async (urlPtr, init) => {
            let url;
            if (typeof urlPtr === 'number') {
                if (!memoryRef.current) {
                    throw new Error('WASM memory not available for string conversion');
                }
                // Read AssemblyScript string from memory (UTF-16LE)
                const SIZE_OFFSET = -4;
                const memView = new Uint32Array(memoryRef.current.buffer);
                const strLengthInBytes = memView[(urlPtr + SIZE_OFFSET) >>> 2];
                const strLengthInChars = strLengthInBytes >>> 1;
                const strView = new Uint16Array(memoryRef.current.buffer, urlPtr, strLengthInChars);
                url = String.fromCharCode(...Array.from(strView));
                debug(`Converted WASM string pointer ${urlPtr} to URL: ${url}`);
            }
            else {
                url = String(urlPtr);
            }
            return nodeFetch(url, init);
        };
        // Dynamic import for ESM-only as-fetch package
        const { FetchHandler } = await import('as-fetch/bindings.raw.esm.js');
        fetchHandler = new FetchHandler(fetchWrapper);
        fetchImports = fetchHandler.imports;
    }
    // Instantiate the module
    let instance;
    let asLoaderInstance = null;
    let rawExports;
    if (isAssemblyScriptPlugin) {
        debug(`Using AssemblyScript loader for ${pluginId}`);
        asLoaderInstance = await loader_1.default.instantiate(module, {
            wasi_snapshot_preview1: wasiImports.wasi_snapshot_preview1 || wasiImports,
            env: envImports,
            ...fetchImports
        });
        instance = asLoaderInstance.instance;
        rawExports = asLoaderInstance.exports;
        asLoaderRef.current = asLoaderInstance;
        debug(`AssemblyScript instance created with loader`);
    }
    else {
        // Standard WebAssembly instantiation for Rust plugins
        instance = await WebAssembly.instantiate(module, {
            wasi_snapshot_preview1: wasiImports.wasi_snapshot_preview1 || wasiImports,
            env: envImports,
            ...fetchImports
        });
        rawExports = instance.exports;
        debug(`Standard WASM instance created`);
    }
    // Set refs for use in callbacks
    rawExportsRef.current = rawExports;
    if (rawExports.memory) {
        memoryRef.current = rawExports.memory;
    }
    // Store reference for Asyncify resume
    let asyncifyResumeFunction = null;
    // NOTE: Do NOT initialize as-fetch handler here!
    // as-fetch uses global state that gets corrupted if multiple plugins are loaded in parallel.
    // The handler is initialized right before plugin_start() is called, protected by a mutex.
    // Initialize based on plugin type
    if (isRustPlugin) {
        debug(`Initializing Rust command plugin: ${pluginId}`);
        wasi.start(instance);
    }
    else if (isRustLibraryPlugin) {
        debug(`Initialized Rust library plugin: ${pluginId}`);
        // Initialize WASI runtime without calling _start (for library plugins)
        // This sets up fd_write and other syscalls properly
        if (typeof wasi.initialize === 'function') {
            debug(`Calling wasi.initialize() for Rust library plugin`);
            wasi.initialize(instance);
        }
        // Also call _initialize if present (Rust static constructors)
        if (rawExports._initialize) {
            debug(`Calling _initialize for Rust library plugin`);
            rawExports._initialize();
        }
    }
    else if (isAssemblyScriptPlugin) {
        debug(`Initialized AssemblyScript plugin: ${pluginId}`);
    }
    else {
        throw new Error(`Unknown WASM plugin format for ${pluginId}`);
    }
    // Create normalized export interface
    const exports = createPluginExports(isAssemblyScriptPlugin, isRustLibraryPlugin, asLoaderInstance, rawExports, () => asyncifyResumeFunction, (fn) => {
        asyncifyResumeFunction = fn;
    }, fetchHandler, capabilities);
    // Create setter for asyncify resume that can be used by external callers
    const setAsyncifyResume = (fn) => {
        asyncifyResumeFunction = fn;
    };
    const pluginInstance = {
        pluginId,
        wasmPath,
        vfsRoot,
        capabilities,
        format: 'wasi-p1',
        wasi,
        module,
        instance,
        exports,
        asLoader: asLoaderInstance,
        setAsyncifyResume
    };
    // Update provider references
    (0, resource_provider_1.updateResourceProviderInstance)(pluginId, pluginInstance);
    (0, weather_provider_1.updateWeatherProviderInstance)(pluginId, pluginInstance);
    (0, radar_provider_1.updateRadarProviderInstance)(pluginId, pluginInstance);
    debug(`Successfully loaded WASM plugin: ${pluginId}`);
    return pluginInstance;
}
/**
 * Create normalized plugin exports based on plugin type
 */
function createPluginExports(isAssemblyScriptPlugin, isRustLibraryPlugin, asLoaderInstance, rawExports, getAsyncifyResume, setAsyncifyResume, fetchHandler, capabilities) {
    let idFunc;
    let nameFunc;
    let schemaFunc;
    let startFunc;
    let stopFunc;
    if (isAssemblyScriptPlugin && asLoaderInstance) {
        idFunc = () => {
            const ptr = asLoaderInstance.exports.plugin_id();
            return asLoaderInstance.exports.__getString(ptr);
        };
        nameFunc = () => {
            const ptr = asLoaderInstance.exports.plugin_name();
            return asLoaderInstance.exports.__getString(ptr);
        };
        schemaFunc = () => {
            const ptr = asLoaderInstance.exports.plugin_schema();
            return asLoaderInstance.exports.__getString(ptr);
        };
        startFunc = async (config) => {
            debug(`Calling plugin_start with config: ${config.substring(0, 100)}...`);
            // Re-initialize as-fetch handler to refresh ASYNCIFY_MEM view
            // This is needed because memory may have grown since init(), detaching the old buffer view
            if (fetchHandler && capabilities.network) {
                debug(`Re-initializing as-fetch handler before plugin_start`);
                fetchHandler.init(rawExports, () => {
                    debug(`FetchHandler calling main function to resume execution`);
                    const resumeFn = getAsyncifyResume();
                    if (resumeFn) {
                        resumeFn();
                    }
                });
            }
            const encoder = new TextEncoder();
            const configBytes = encoder.encode(config);
            const configLen = configBytes.length;
            const configPtr = asLoaderInstance.exports.__new(configLen, 0);
            const memory = asLoaderInstance.exports.memory.buffer;
            const memoryView = new Uint8Array(memory);
            memoryView.set(configBytes, configPtr);
            let resumePromiseResolve = null;
            const resumePromise = new Promise((resolve) => {
                resumePromiseResolve = resolve;
            });
            setAsyncifyResume(() => {
                debug(`Re-calling plugin_start to resume from rewind state`);
                // Check Asyncify state - as-fetch calls asyncify_start_rewind() before calling us
                // State 0 = normal (rewind already completed), skip to avoid double-rewind
                // State 1 = unwound (shouldn't happen, as-fetch would have started rewind)
                // State 2 = rewinding (expected, proceed with resume)
                if (typeof asLoaderInstance.exports.asyncify_get_state === 'function') {
                    const currentState = asLoaderInstance.exports.asyncify_get_state();
                    if (currentState === 0) {
                        debug(`Plugin in normal state (state=0), rewind already completed, skipping`);
                        return;
                    }
                    debug(`Asyncify state before resume: ${currentState}`);
                }
                try {
                    // Re-read memory buffer in case it was detached during async operation
                    const currentMemory = asLoaderInstance.exports.memory.buffer;
                    debug(`Memory buffer size: ${currentMemory.byteLength}, configPtr: ${configPtr}, configLen: ${configLen}`);
                    const resumeResult = asLoaderInstance.exports.plugin_start(configPtr, configLen);
                    if (resumePromiseResolve) {
                        resumePromiseResolve();
                    }
                    return resumeResult;
                }
                catch (err) {
                    debug(`Error during Asyncify rewind: ${err.message}`);
                    if (resumePromiseResolve) {
                        resumePromiseResolve();
                    }
                    throw err;
                }
            });
            const result = asLoaderInstance.exports.plugin_start(configPtr, configLen);
            if (typeof asLoaderInstance.exports.asyncify_get_state === 'function') {
                const state = asLoaderInstance.exports.asyncify_get_state();
                debug(`Asyncify state after plugin_start: ${state}`);
                if (state === 1) {
                    debug(`Plugin is in unwound state - waiting for async operation to complete`);
                    await resumePromise;
                    debug(`Async operation completed, plugin execution resumed`);
                }
                else {
                    setAsyncifyResume(null);
                }
            }
            if (typeof asLoaderInstance.exports.__free === 'function') {
                asLoaderInstance.exports.__free(configPtr);
            }
            return result;
        };
        stopFunc = () => asLoaderInstance.exports.plugin_stop();
    }
    else if (isRustLibraryPlugin) {
        debug(`Setting up Rust library plugin exports with buffer-based strings`);
        const callRustStringFunc = (funcName) => {
            const func = rawExports[funcName];
            if (typeof func !== 'function') {
                debug(`Warning: ${funcName} not found in exports`);
                return '';
            }
            const maxLen = 8192;
            const allocate = rawExports.allocate;
            if (typeof allocate !== 'function') {
                throw new Error('Rust plugin missing allocate export');
            }
            const outPtr = allocate(maxLen);
            if (!outPtr) {
                throw new Error(`Failed to allocate ${maxLen} bytes for ${funcName}`);
            }
            try {
                const writtenLen = func(outPtr, maxLen);
                if (writtenLen <= 0) {
                    debug(`${funcName} returned ${writtenLen}`);
                    return '';
                }
                const memory = rawExports.memory;
                const bytes = new Uint8Array(memory.buffer, outPtr, writtenLen);
                const decoder = new TextDecoder('utf-8');
                const result = decoder.decode(bytes);
                debug(`${funcName} returned: ${result.substring(0, 100)}...`);
                return result;
            }
            finally {
                const deallocate = rawExports.deallocate;
                if (typeof deallocate === 'function') {
                    deallocate(outPtr, maxLen);
                }
            }
        };
        idFunc = () => callRustStringFunc('plugin_id');
        nameFunc = () => callRustStringFunc('plugin_name');
        schemaFunc = () => callRustStringFunc('plugin_schema');
        startFunc = (config) => {
            debug(`Calling Rust plugin_start with config: ${config.substring(0, 100)}...`);
            const encoder = new TextEncoder();
            const configBytes = encoder.encode(config);
            const configLen = configBytes.length;
            const allocate = rawExports.allocate;
            const configPtr = allocate(configLen);
            const memory = rawExports.memory;
            const memoryView = new Uint8Array(memory.buffer);
            memoryView.set(configBytes, configPtr);
            try {
                const result = rawExports.plugin_start(configPtr, configLen);
                debug(`plugin_start returned: ${result}`);
                return result;
            }
            finally {
                const deallocate = rawExports.deallocate;
                if (typeof deallocate === 'function') {
                    deallocate(configPtr, configLen);
                }
            }
        };
        stopFunc = () => {
            const result = rawExports.plugin_stop();
            debug(`plugin_stop returned: ${result}`);
            return result;
        };
    }
    else {
        // Rust command plugins or unknown
        idFunc = rawExports.id;
        nameFunc = rawExports.name;
        schemaFunc = rawExports.schema;
        startFunc = rawExports.start;
        stopFunc = rawExports.stop;
    }
    // Wrap http_endpoints if it exists
    const httpEndpointsFunc = rawExports.http_endpoints
        ? isAssemblyScriptPlugin && asLoaderInstance
            ? () => {
                const ptr = asLoaderInstance.exports.http_endpoints();
                return asLoaderInstance.exports.__getString(ptr);
            }
            : rawExports.http_endpoints
        : undefined;
    // Wrap poll if it exists (for plugins that need periodic execution)
    const pollFunc = rawExports.poll
        ? isAssemblyScriptPlugin && asLoaderInstance
            ? () => asLoaderInstance.exports.poll()
            : rawExports.poll
        : undefined;
    // Wrap delta_handler if it exists (for plugins that subscribe to deltas)
    let deltaHandlerFunc = undefined;
    if (rawExports.delta_handler) {
        if (isAssemblyScriptPlugin && asLoaderInstance) {
            deltaHandlerFunc = (deltaJson) => {
                // Pass delta JSON string to the WASM delta_handler
                const ptr = asLoaderInstance.exports.__newString(deltaJson);
                asLoaderInstance.exports.delta_handler(ptr);
            };
        }
        else if (isRustLibraryPlugin) {
            // Rust library plugin: buffer-based string passing
            deltaHandlerFunc = (deltaJson) => {
                const encoder = new TextEncoder();
                const deltaBytes = encoder.encode(deltaJson);
                const deltaLen = deltaBytes.length;
                const allocate = rawExports.allocate;
                if (typeof allocate !== 'function') {
                    debug('Rust plugin missing allocate export for delta_handler');
                    return;
                }
                const deltaPtr = allocate(deltaLen);
                const memory = rawExports.memory;
                const memoryView = new Uint8Array(memory.buffer);
                memoryView.set(deltaBytes, deltaPtr);
                try {
                    rawExports.delta_handler(deltaPtr, deltaLen);
                }
                finally {
                    const deallocate = rawExports.deallocate;
                    if (typeof deallocate === 'function') {
                        deallocate(deltaPtr, deltaLen);
                    }
                }
            };
        }
        else {
            deltaHandlerFunc = rawExports.delta_handler;
        }
    }
    return {
        id: idFunc,
        name: nameFunc,
        schema: schemaFunc,
        start: startFunc,
        stop: stopFunc,
        memory: rawExports.memory,
        ...(httpEndpointsFunc && { http_endpoints: httpEndpointsFunc }),
        ...(pollFunc && { poll: pollFunc }),
        ...(deltaHandlerFunc && { delta_handler: deltaHandlerFunc })
    };
}
//# sourceMappingURL=standard-loader.js.map