const kWasmModule = Symbol('kWasmModule');
const kWasmModuleRef = Symbol('kWasmModuleRef');
const kWasmExports = Symbol('kWasmExports');
const kWasmInstance = Symbol('kWasmInstance');
const kWasmInstances = Symbol('kWasmInstances');
const kWasiLinked = Symbol('kWasiLinked');
const kWasiStarted = Symbol('kWasiStarted');
const kWasiOptions = Symbol('kWasiOptions');


class CompileError extends Error {
    constructor(...args) {
        super(...args);
        this.name = 'CompileError';
    }
}

class LinkError extends Error {
    constructor(...args) {
        super(...args);
        this.name = 'LinkError';
    }
}

class RuntimeError extends Error {
    constructor(...args) {
        super(...args);
        this.name = 'RuntimeError';
    }
}


function getWasmError(e) {
    switch (e.wasmError) {
        case 'CompileError':
            return new CompileError(e.message);
        case 'LinkError':
            return new LinkError(e.message);
        case 'RuntimeError':
            return new RuntimeError(e.message);
        default:
            return new TypeError(`Invalid WASM error: ${e.wasmError}`);
    }
}

function callWasmFunction(name, ...args) {
    const instance = this;

    try {
        return instance.callFunction(name, ...args);
    } catch (e) {
        if (e.wasmError) {
            throw getWasmError(e);
        } else {
            throw e;
        }
    }
}

function buildInstance(mod) {
    try {
        return wasm.buildInstance(mod);
    } catch (e) {
        if (e.wasmError) {
            throw getWasmError(e);
        } else {
            throw e;
        }
    }
}

function linkWasi(instance) {
    try {
        instance.linkWasi();
    } catch (e) {
        if (e.wasmError) {
            throw getWasmError(e);
        } else {
            throw e;
        }
    }
}

function parseModule(buf) {
    try {
        return wasm.parseModule(buf);
    } catch (e) {
        if (e.wasmError) {
            throw getWasmError(e);
        } else {
            throw e;
        }
    }
}

class Module {
    constructor(buf) {
        this[kWasmModule] = parseModule(buf);
    }

    static exports(module) {
        return wasm.moduleExports(module[kWasmModule]);
    }

    // eslint-disable-next-line no-unused-vars
    static imports(module) {
        // TODO.
        return {};
    }
}

class Instance {
    constructor(module, importObject = {}) {
        const instance = buildInstance(module[kWasmModule]);

        if (importObject.wasi_unstable) {
            linkWasi(instance);
            this[kWasiLinked] = true;
        }

        const _exports = Module.exports(module);
        const exports = Object.create(null);

        for (const item of _exports) {
            if (item.kind === 'function') {
                exports[item.name] = callWasmFunction.bind(instance, item.name);
            }
        }

        this[kWasmInstance] = instance;
        this[kWasmExports] = Object.freeze(exports);
        this[kWasmModuleRef] = module;
        globalThis.WebAssembly[kWasmInstances].push(this);
    }

    get exports() {
        return this[kWasmExports];
    }
}

class WASI {
    wasiImport = 'w4s1';  // Doesn't matter right now.

    constructor(options = {args: [], env: {}, preopens: {}}) {
        this[kWasiStarted] = false;

        if (options === null || typeof options !== 'object') {
            throw new TypeError('options must be an object');
        }

        this[kWasiOptions] = JSON.parse(JSON.stringify(options));
    }

    start(instance) {
        if (this[kWasiStarted]) {
            throw new Error('WASI instance has already started');
        }

        if (!instance[kWasiLinked]) {
            throw new Error('WASM instance doesn\'t have WASI linked');
        }

        if (!instance.exports._start) {
            throw new TypeError('WASI entrypoint not found');
        }

        this[kWasiStarted] = true;
        instance.exports._start(...(this[kWasiOptions].args ?? []));
    }
}

class WebAssembly {
    Module = Module;
    Instance = Instance;
    CompileError = CompileError;
    LinkError = LinkError;
    RuntimeError = RuntimeError;
    WASI = WASI;

    constructor() {
        this[kWasmInstances] = [];
    }

    async compile(src) {
        return new Module(src);
    }

    async instantiate(src, importObject) {
        const module = await this.compile(src);
        const instance = new Instance(module, importObject);

        return {module, instance};
    }
}


Object.defineProperty(globalThis, 'WebAssembly', {
    enumerable: true, configurable: true, writable: true, value: new WebAssembly()
});
