const textEncoder = new TextEncoder();

class Wasi {
    #encodedStdin;
    #envEncodedStrings;
    #argEncodedStrings;
    instance;

    constructor({ env, stdin, args }) {
        this.#encodedStdin = textEncoder.encode(stdin);
        const envStrings = Object.entries(env).map(([k, v]) => `${k}=${v}`);
        this.#envEncodedStrings = envStrings.map(s => textEncoder.encode(s + "\0"))
        this.#argEncodedStrings = args.map(s => textEncoder.encode(s + "\0"));
        this.bind();
    }

    bind(){
        this.args_get= this.args_get.bind(this);
        this.args_sizes_get = this.args_sizes_get.bind(this);
        this.environ_get = this.environ_get.bind(this);
        this.environ_sizes_get = this.environ_sizes_get.bind(this);
        this.fd_read = this.fd_read.bind(this);
        this.fd_write = this.fd_write.bind(this);
    }

    args_sizes_get(argCountPtr, argBufferSizePtr) {
        const argByteLength = this.#argEncodedStrings.reduce((sum, val) => sum + val.byteLength, 0);
        const countPointerBuffer = new Uint32Array(this.instance.exports.memory.buffer, argCountPtr, 1);
        const sizePointerBuffer = new Uint32Array(this.instance.exports.memory.buffer, argBufferSizePtr, 1);
        countPointerBuffer[0] = this.#argEncodedStrings.length;
        sizePointerBuffer[0] = argByteLength;
        return 0;
    }
    args_get(argsPtr, argBufferPtr) {
        const argsByteLength = this.#argEncodedStrings.reduce((sum, val) => sum + val.byteLength, 0);
        const argsPointerBuffer = new Uint32Array(this.instance.exports.memory.buffer, argsPtr, this.#argEncodedStrings.length);
        const argsBuffer = new Uint8Array(this.instance.exports.memory.buffer, argBufferPtr, argsByteLength)


        let pointerOffset = 0;
        for (let i = 0; i < this.#argEncodedStrings.length; i++) {
            const currentPointer = argBufferPtr + pointerOffset;
            argsPointerBuffer[i] = currentPointer;
            argsBuffer.set(this.#argEncodedStrings[i], pointerOffset)
            pointerOffset += this.#argEncodedStrings[i].byteLength;
        }

        return 0;
    }
    fd_write(fd, iovsPtr, iovsLength, bytesWrittenPtr) {
        const iovs = new Uint32Array(this.instance.exports.memory.buffer, iovsPtr, iovsLength * 2);
        if (fd === 1) { //stdout
            let text = "";
            let totalBytesWritten = 0;

            const decoder = new TextDecoder();
            for (let i = 0; i < iovsLength * 2; i += 2) {
                const offset = iovs[i];
                const length = iovs[i + 1];
                const textChunk = decoder.decode(new Int8Array(this.instance.exports.memory.buffer, offset, length));
                text += textChunk;
                totalBytesWritten += length;
            }

            const dataView = new DataView(this.instance.exports.memory.buffer);
            dataView.setInt32(bytesWrittenPtr, totalBytesWritten, true);
            console.log(text);
        }
        return 0;
    }
    fd_read(fd, iovsPtr, iovsLength, bytesReadPtr) {
        const memory = new Uint8Array(this.instance.exports.memory.buffer);
        const iovs = new Uint32Array(this.instance.exports.memory.buffer, iovsPtr, iovsLength * 2);
        let totalBytesRead = 0;
        if (fd === 0) {//stdin
            for (let i = 0; i < iovsLength * 2; i += 2) {
                const offset = iovs[i];
                const length = iovs[i + 1];
                const chunk = this.#encodedStdin.slice(0, length);
                this.#encodedStdin = this.#encodedStdin.slice(length);

                memory.set(chunk, offset);
                totalBytesRead += chunk.byteLength;

                if (this.#encodedStdin.length === 0) break;
            }

            const dataView = new DataView(this.instance.exports.memory.buffer);
            dataView.setInt32(bytesReadPtr, totalBytesRead, true);
        }
        return 0;
    }
    fd_advise() {return 0;}
    fd_close() { return 0; }
    fd_fdstat_get() { return 0; }
    fd_prestat_get() { return 0; }
    fd_prestat_dir_name() { return 0; }
    clock_res_get() {return 0;}
    clock_time_get() {return 0;}
    fd_seek() { return 0; }
    fd_allocate() {return 0;}
    fd_datasync() {return 0;}
    fd_fdstat_set_flags() {return 0;}
    fd_fdstat_set_rights() {return 0;}
    fd_filestat_get() {return 0;}
    fd_filestat_set_size() {return 0;}
    fd_filestat_set_times() {return 0;}
    fd_pread() {return 0;}
    fd_pwrite(){return 0;}
    fd_readdir() {return 0;}
    fd_renumber() {return 0;}
    fd_sync() {return 0;}
    fd_tell() {return 0;}
    path_create_directory() {return 0;}
    path_filestat_get() {return 0;}
    path_filestat_set_times() {return 0;}
    path_link() {return 0;}
    path_open() {return 0;}
    path_readlink() {return 0;}
    path_remove_directory() {return 0;}
    path_rename() {return 0;}
    path_symlink() {return 0;}
    path_unlink_file() {return 0;}
    poll_oneoff() {return 0;}
    sched_yield() {return 0;}
    random_get() {return 0;}
    sock_accept() {return 0;}
    sock_recv() {return 0;}
    sock_send() {return 0;}
    sock_shutdown() {return 0;}
    environ_get(environPtr, environBufferPtr) {
        const envByteLength = this.#envEncodedStrings.map(s => s.byteLength).reduce((sum, val) => sum + val, 0);
        const environsPointerBuffer = new Uint32Array(this.instance.exports.memory.buffer, environPtr, this.#envEncodedStrings.length);
        const environsBuffer = new Uint8Array(this.instance.exports.memory.buffer, environBufferPtr, envByteLength)

        let pointerOffset = 0;
        for (let i = 0; i < this.#envEncodedStrings.length; i++) {
            const currentPointer = environBufferPtr + pointerOffset;
            environsPointerBuffer[i] = currentPointer;
            environsBuffer.set(this.#envEncodedStrings[i], pointerOffset)
            pointerOffset += this.#envEncodedStrings[i].byteLength;
        }

        return 0;
    }
    environ_sizes_get(environCountPtr, environBufferSizePtr) {
        const envByteLength = this.#envEncodedStrings.map(s => s.byteLength).reduce((sum, val) => sum + val, 0);
        const countPointerBuffer = new Uint32Array(this.instance.exports.memory.buffer, environCountPtr, 1);
        const sizePointerBuffer = new Uint32Array(this.instance.exports.memory.buffer, environBufferSizePtr, 1);
        countPointerBuffer[0] = this.#envEncodedStrings.length;
        sizePointerBuffer[0] = envByteLength;
        return 0;
    }
    proc_exit() {
        console.log("EXIT");
    }
}

const wasi = new Wasi({
    stdin: "stdio",
    env: {},
    args: []
});

if(!window.crafter_webbuild_env){
    window.crafter_webbuild_env = {};
}

window.crafter_webbuild_env.table = new WebAssembly.Table({ initial: 4, element: 'anyfunc' });

const { instance } = await WebAssembly.instantiateStreaming(fetch("/main.wasm"), {
    "wasi_snapshot_preview1": wasi,
    env: window.crafter_webbuild_env
});
wasi.instance = instance;
window.crafter_webbuild_wasi = wasi;

instance.exports._start();
