const DEBUG = false;
const log = (msg) => DEBUG && console.log(msg);

const wasmCache = new Map();
let wasiInstance;

export function setWasiInstance(instance) {
    wasiInstance = instance;
}

export default async function(baseURL, path, opts = {}) {
    const url = new URL(path, baseURL);

    let wasm;
    if (wasmCache.has(url.pathname)) wasm = wasmCache.get(url.pathname);
    else {
        wasm = await WebAssembly.instantiateStreaming(
            fetch(url), {
                wasi_snapshot_preview1: {
                    ...wasi,
                },
                env: {
                    ...wasi,
                    ...syscalls,
                    ...javascripts,
                },
            },
        );
        wasmCache.set(url.pathname, wasm);
    }
    setWasiInstance(wasm.instance);
    return wasm;
}

const FS = {};
let nextFd = 0;
writeFS(new Uint8Array(0), "/dev/stdin");
writeFS(new Uint8Array(1024*8), "/dev/stdout");
writeFS(new Uint8Array(1024*8), "/dev/stderr");
if (nextFd !== 3) throw new Error("Unexpected next fd");

export function writeFS(buffer, path = "") {
    if (!(buffer instanceof Uint8Array)) throw new Error("can only write Uint8Array");
    FS[nextFd] = {
        buffer,
        position: 0,
        path,
    };
    nextFd += 1;
    return nextFd - 1;
}

export function readFS(fd) {
    const file = FS[fd];
    if (!file) throw new Error("file does not exist");

    let end = file.buffer.length;
    while (end > 0 && file.buffer[end - 1] === 0) end--;
    return file.buffer.subarray(0, end);
}

export function clearFS() {
    Object.keys(FS).forEach((key) => {
        if (key > 2) delete FS[key];
    });
    nextFd = 3;
}

function getFile(path) {
    const allFds = Object.keys(FS);
    for (let i=allFds.length - 1; i>0; i--) {
        if (FS[allFds[i]].path === path) {
            log(`  fileopen fd=${i} path=${path}`);
            return FS[allFds[i]];
        }
    }
    throw new Error(`cannot get file "${path}"`);
}

export const syscalls = {
    __syscall_fcntl64: (fd, cmd, varargs) => {
        console.log(`Stubbed __syscall_fcntl64 called with fd=${fd}, cmd=${cmd}, varargs=${varargs}`);
        return -1;
    },
    __syscall_ioctl: (fd, op, varargs) => {
        switch (op) {
        case 21523:
            break;
        default:
            console.log(`Stubbed __syscall_ioctl called with fd=${fd}, request=${op}, varargs=${varargs}`);
        }
        return 0;
    },
    __syscall_unlinkat: (fd) => {
        console.log(`Stubbed __syscall_unlinkat called with fd=${fd}`);
        return -1;
    },
    __syscall_rmdir: (fd) => {
        console.log(`Stubbed __syscall_rmdir called with fd=${fd}`);
        return -1;
    },
    __syscall_newfstatat: (pathPtr, bufPtr) => {
        console.log(`Stubbed __syscall_stat64 called with pathPtr=${pathPtr}, bufPtr=${bufPtr}`);
        return 0; // Return 0 for a successful call
    },
    __syscall_lstat64: () => {
        console.log(`Stubbed __syscall_lstat64 called`);
        return -1;
    },
    __assert_fail: () => {
        console.log(`Stubbed __assert_fail called`);
        return -1;
    },
    __syscall_ftruncate64: () => {
        console.log(`Stubbed __syscall_ftruncate64`);
        return -1;
    },
    __syscall_renameat: () => {
        console.log(`Stubbed __syscall_renameat`);
        return -1;
    },
};

const javascripts = {
    _tzset_js: () => {
        console.log("Initializing time zone settings (stub)");
    },
    _abort_js: () => {
        console.error("WebAssembly module called _abort_js!");
        throw new Error("_abort_js was called");
    },
    _mktime_js: () => {
        console.error("WebAssembly module called _abort_js!");
        throw new Error("_abort_js was called");
    },
    _localtime_js: () => {
        console.error("WebAssembly module called _localtime_js!");
        throw new Error("_localtime_js was called");
    },
    emscripten_date_now: () => {
        console.error("WebAssembly module called emscripten_date_now!");
        throw new Error("_localtime_js was called");
    },
    emscripten_get_now: () => {
        console.error("WebAssembly module called emscripten_get_now!");
        throw new Error("_localtime_js was called");
    },
    emscripten_errn: () => {
        console.error("WebAssembly module called emscripten_errn!");
        throw new Error("_errn was called");
    },
    HaveOffsetConverter: () => {
        console.error("WebAssembly module called HaveOffsetConverter!");
        throw new Error("HaveOffsetConverter was called");
    },
    emscripten_pc_get_function: () => {
        console.error("WebAssembly module called emscripten_pc_get_function!");
        throw new Error("emscripten_pc_get_function was called");
    },
    emscripten_asm_const_int: () => {
        console.error("WebAssembly module called emscripten_asm_const_int!");
        throw new Error("emscripten_asm_const_int was called");
    },
    emscripten_stack_snapshot: () => {
        console.error("WebAssembly module called emscripten_stack_snapshot!");
        throw new Error("emscripten_stack_snapshot was called");
    },
    emscripten_stack_unwind_buffer: () => {
        console.error("WebAssembly module called emscripten_stack_unwind_buffer!");
        throw new Error("emscripten_stack_unwind_buffer was called");
    },
    emscripten_get_heap_max: () => {
        console.error("WebAssembly module called emscripten_get_heap_max!");
        throw new Error("emscripten_get_heap_max was called");
    },
    _timegm_js: () => {
        return 0;
    },
    _gmtime_js: () => {
        return null;
    },
    _munmap_js: () => {
        console.error("WebAssembly module called _munmap_js!");
        throw new Error("_munmap_js was called");
    },
    _mmap_js: () => {
        console.error("WebAssembly module called _mmap_js!");
        throw new Error("_mmap_js was called");
    }
};

export const wasi = {

    fd_write(fd, iovs, iovs_len, nwritten) {
        if (!FS[fd]) throw new Error(`File descriptor ${fd} does not exist.`);

        const ioVecArray = new Uint32Array(wasiInstance.exports.memory.buffer, iovs, iovs_len * 2);
        const memory = new Uint8Array(wasiInstance.exports.memory.buffer);
        let totalBytesWritten = 0;

        for (let i = 0; i < iovs_len * 2; i += 2) {
            const offset = ioVecArray[i];
            const length = ioVecArray[i + 1];
            while (FS[fd].buffer.byteLength - FS[fd].position < length) {
                const newBuffer = new Uint8Array(FS[fd].buffer.byteLength + 1024 * 1024 * 5);
                newBuffer.set(FS[fd].buffer, 0);
                FS[fd].buffer = newBuffer;
            }
            FS[fd].buffer.set(
                memory.subarray(offset, offset + length),
                FS[fd].position
            );
            FS[fd].position += length;
            totalBytesWritten += length;
        }
        new DataView(wasiInstance.exports.memory.buffer).setUint32(
            nwritten,
            totalBytesWritten,
            true,
        );
        if (fd === 1 || fd === 2) {
            FS[fd] = {
                buffer: new Uint8Array(readFS(fd)),
                position: 0,
                path: FS[fd].path || "",
            };
        } else {
            log(`wasi::fd_write fd=${fd}`);
        }
        return 0;
    },

    fd_read(fd, iovs, iovs_len, nread) {
        const file = FS[fd];
        if (!file) {
            console.error(`Invalid fd: ${fd}`);
            return -1;
        }

        const ioVecArray = new Uint32Array(wasiInstance.exports.memory.buffer, iovs, iovs_len * 2);
        const memory = new Uint8Array(wasiInstance.exports.memory.buffer);
        let totalBytesRead = 0;
        for (let i = 0; i < iovs_len * 2; i += 2) {
            const offset = ioVecArray[i];
            const length = ioVecArray[i + 1] || 0;
            const bytesToRead = Math.min(
                length,
                file.buffer.length - file.position,
            );
            if (bytesToRead < 0) {
                break;
            }
            memory.set(
                file.buffer.subarray(file.position, file.position + bytesToRead),
                offset,
            );
            file.position += bytesToRead;
            totalBytesRead += bytesToRead;
        }
        log(`wasi::fd_read fd=${fd} iovs_len=${iovs_len} totalBytesRead=${totalBytesRead}`);
        new DataView(wasiInstance.exports.memory.buffer).setUint32(
            nread,
            totalBytesRead,
            true,
        );
        return 0;
    },

    fd_pread(fd, iovs, iovs_len, offset_lo, offset_hi, nread) {
        const file = FS[fd];
        if (!file) {
            console.error(`Invalid fd: ${fd}`);
            return -1;
        }

        const start = (offset_hi >>> 0) * 0x100000000 + (offset_lo >>> 0);
        const ioVec = new Uint32Array(wasiInstance.exports.memory.buffer, iovs, iovs_len * 2);
        const mem = new Uint8Array(wasiInstance.exports.memory.buffer);

        let total = 0;
        for (let i = 0; i < iovs_len * 2; i += 2) {
            const dst = ioVec[i];
            const len = ioVec[i + 1] || 0;
            const avail = Math.max(
                0,
                Math.min(len, file.buffer.length - (start + total)),
            );
            if (avail === 0) {
                console.log(`len=[${len}] buffLength=[${file.buffer.length}] start=[${start}] total=[${total}]`);
                break;
            }
            mem.set(
                file.buffer.subarray(start + total, start + total + avail),
                dst
            );
            total += avail;
        }

        new DataView(wasiInstance.exports.memory.buffer).setUint32(nread, total, true);
        return 0;
    },

    fd_seek(fd, offsetBigInt, _, whence) {
        log(`wasi::fd_seek fd=${fd} offset=${offsetBigInt} whence=${whence}`);
        const offset = Number(offsetBigInt);
        const file = FS[fd];
        if (!file) {
            console.error(`Invalid FD: ${fd}`);
            return -1;
        }
        switch (whence) {
        case 0: // SEEK_SET
            file.position = offset;
            break;
        case 1: // SEEK_CUR
            file.position += offset;
            break;
        case 2: // SEEK_END
            file.position = file.buffer.length + offset;
            break;
        default:
            console.log(`fd_seek called with fd=${fd}, offset=${offset}, position=${file.position} whence=${whence}`);
            const error = new Error("fd_seek trace");
            console.log("Invalid whence", whence, error.stack);
            return -1;
        }
        return 0;
    },

    fd_close(fd) {
        if (!FS[fd]) {
            console.error(`Invalid FD: ${fd}`);
            return -1;
        }
        return 0;
    },

    _emscripten_memcpy_js(dest, src, num) {
        const memory = new Uint8Array(wasiInstance.exports.memory.buffer);
        memory.set(memory.subarray(src, src + num), dest);
        return dest;
    },

    emscripten_resize_heap(requested) {
        console.log("Stubbed emscripten_resize_heap called");
        throw new Error("Heap resize not supported");
    },

    environ_sizes_get() {
        console.log(`Stubbed environ_sizes_get called`);
        return 0;
    },

    environ_get() {
        console.log(`Stubbed environ_get called`);
        return 0;
    },

    clock_time_get() {
        console.log(`Stubbed clock_time_get called`);
        return -1;
    },

    __syscall_openat(dirFd, pathPtr, flags, mode) {
        const memory = new Uint8Array(wasiInstance.exports.memory.buffer);
        let path = "";
        for (let i = pathPtr; memory[i] !== 0; i++) {
            path += String.fromCharCode(memory[i]);
        }
        const allFds = Object.keys(FS);
        for (let i=allFds.length - 1; i>0; i--) {
            if (FS[allFds[i]].path === path) {
                log(`  syscall::openat::result fd=${i} path=${path}`);
                return i;
            }
        }
        throw new Error("Unknown file for __syscall_openat");
    },

    __syscall_stat64(pathPtr, buf) {
        log(`  syscall::stat64 pathPtr=${pathPtr}, bufPtr=${buf}`);
        const memory = new Uint8Array(wasiInstance.exports.memory.buffer);
        let path = "";
        for (let i = pathPtr; memory[i] !== 0; i++) {
            path += String.fromCharCode(memory[i]);
        }
        const file = getFile(path);
        const HEAP32 = new Int32Array(wasiInstance.exports.memory.buffer);
        const HEAPU32 = new Uint32Array(wasiInstance.exports.memory.buffer);
        const stat = {
            dev: 1,
            ino: 42,
            mode: 0o100644,
            nlink: 1,
            uid: 1000,
            gid: 1000,
            rdev: 0,
            size: file.buffer.byteLength,
            blksize: 4096,
            blocks: 256,
            atime: new Date(),
            mtime: new Date(),
            ctime: new Date(),
        };
        HEAP32[(buf >> 2)] = stat.dev;
        HEAP32[((buf + 4) >> 2)] = stat.mode;
        HEAPU32[((buf + 8) >> 2)] = stat.nlink;
        HEAP32[((buf + 12) >> 2)] = stat.uid;
        HEAP32[((buf + 16) >> 2)] = stat.gid;
        HEAP32[((buf + 20) >> 2)] = stat.rdev;
        HEAP32[((buf + 24) >> 2)] = stat.size & 0xFFFFFFFF;
        HEAP32[((buf + 28) >> 2)] = Math.floor(stat.size / 4294967296);
        HEAP32[((buf + 32) >> 2)] = stat.blksize;
        HEAP32[((buf + 36) >> 2)] = stat.blocks;
        HEAP32[((buf + 40) >> 2)] = Math.floor(stat.atime.getTime() / 1000);
        HEAP32[((buf + 44) >> 2)] = 0;
        HEAP32[((buf + 48) >> 2)] = (stat.atime.getTime() % 1000) * 1e6;
        HEAP32[((buf + 56) >> 2)] = Math.floor(stat.mtime.getTime() / 1000);
        HEAP32[((buf + 60) >> 2)] = 0;
        HEAP32[((buf + 64) >> 2)] = (stat.mtime.getTime() % 1000) * 1e6;
        HEAP32[((buf + 72) >> 2)] = Math.floor(stat.ctime.getTime() / 1000);
        HEAP32[((buf + 76) >> 2)] = 0;
        HEAP32[((buf + 80) >> 2)] = (stat.ctime.getTime() % 1000) * 1e6;
        HEAP32[((buf + 88) >> 2)] = stat.ino & 0xFFFFFFFF;
        HEAP32[((buf + 92) >> 2)] = Math.floor(stat.ino / 4294967296);
        return 0;
    },

    __cxa_throw(ptr, type, destructor) {
        console.error(`  syscall::cxa_throw ptr=${ptr}, type=${type}, destructor=${destructor}`);
        throw new Error("WebAssembly exception");
    },

    random_get() {
        console.log(`Stubbed random_get called`);
        return -1;
    },

    proc_exit() {
        console.log(`Stubbed proc_exit called`);
        return -1;
    },

    __syscall_fstat64(fd, buf) {
        log(`  syscall::fstat64 fd=${fd}, buf=${buf}`);
        const file = FS[fd];
        if (!file) return -1; // EBADF

        const size = file.buffer.byteLength >>> 0; // ≤ 4 GB
        const nowSec = (Date.now() / 1000) | 0;
        const H32 = new Int32Array(wasiInstance.exports.memory.buffer);

        /* basic fields */
        H32[buf >> 2] = 1; /* st_dev   */
        H32[(buf+4) >> 2] = 0o100644; /* st_mode  */
        H32[(buf+8) >> 2] = 1; /* st_nlink */
        H32[(buf+12) >> 2] = 1000; /* st_uid   */
        H32[(buf+16) >> 2] = 1000; /* st_gid   */
        H32[(buf+20) >> 2] = 0; /* st_rdev  */

        H32[((buf + 24) >> 2)] = size & 0xFFFFFFFF;
        H32[((buf + 28) >> 2)] = Math.floor(size / 4294967296);

        H32[(buf+32) >> 2] = 4096;
        H32[(buf+36) >> 2] = (size + 511) >> 9;

        /* st_size lives at byte 40 in Emscripten’s 32-bit stat64 */
        H32[(buf+40) >> 2] = size; /* low 32 bits (high word = 0) */
        H32[(buf+44) >> 2] = 0;

        H32[(buf+32) >> 2] = 4096; /* st_blksize */
        H32[(buf+36) >> 2] = (size + 511) >> 9; /* st_blocks */

        /* atime / mtime / ctime: seconds, nsec = 0 */
        for (const off of [48, 56, 64]) {
            H32[((buf+off) >> 2)] = nowSec;
            H32[((buf+off+4) >> 2)] = 0;
        }

        H32[(buf+72) >> 2] = fd; /* st_ino (low) */
        H32[(buf+76) >> 2] = 0; /* st_ino (high) */
        return 0;
    }
};
