let panicImpl;
let logImpl, log2Impl;
let canvasBeginImpl,
    canvasLineImpl,
    canvasRectangleImpl,
    canvasCircleImpl,
    canvasFillImpl,
    canvasStrokeImpl;

function allocCheck(p) {
    if (p == 0) throw new Error("out of memory");
    return p;
}

function makeLogFunction(level) {
    return (length, pMessage) => {
        logImpl(level, length, pMessage);
    };
}

function makeLogFunction2(level) {
    return (pScope, scopeLen, pMsg, len) => {
        log2Impl(level, pScope, scopeLen, pMsg, len);
    };
}

// NOTE: It may seem tempting to share memory between the two modules, but that's *impossible*.
// This is because Wasm binaries are not position-independent; addresses of the stack and the heap
// are hardcoded.
let [hakuWasm, haku2Wasm] = await Promise.all([
    WebAssembly.instantiateStreaming(fetch(HAKU_WASM_PATH), {
        env: {
            panic(length, pMessage) {
                panicImpl(length, pMessage);
            },
            trace: makeLogFunction("trace"),
            debug: makeLogFunction("debug"),
            info: makeLogFunction("info"),
            warn: makeLogFunction("warn"),
            error: makeLogFunction("error"),
        },
    }),
    WebAssembly.instantiateStreaming(fetch(HAKU2_WASM_PATH), {
        env: {
            __haku2_log_err: makeLogFunction2("error"),
            __haku2_log_warn: makeLogFunction2("warn"),
            __haku2_log_info: makeLogFunction2("info"),
            __haku2_log_debug: makeLogFunction2("debug"),

            __haku2_canvas_begin: (c) => canvasBeginImpl(c),
            __haku2_canvas_line: (c, x1, y1, x2, y2) => canvasLineImpl(c, x1, y1, x2, y2),
            __haku2_canvas_rectangle: (c, x, y, width, height) =>
                canvasRectangleImpl(c, x, y, width, height),
            __haku2_canvas_circle: (c, x, y, r) => canvasCircleImpl(c, x, y, r),
            __haku2_canvas_fill: (c, r, g, b, a) => canvasFillImpl(c, r, g, b, a),
            __haku2_canvas_stroke: (c, r, g, b, a, thickness) =>
                canvasStrokeImpl(c, r, g, b, a, thickness),
        },
    }),
]);

let memory = hakuWasm.instance.exports.memory;
let w = hakuWasm.instance.exports;
let memory2 = haku2Wasm.instance.exports.memory;
let w2 = haku2Wasm.instance.exports;

let textEncoder = new TextEncoder();
function allocString(string) {
    let size = string.length * 3;
    let align = 1;
    let pString = allocCheck(w.haku_alloc(size, align));

    let buffer = new Uint8Array(memory.buffer, pString, size);
    let result = textEncoder.encodeInto(string, buffer);

    return {
        ptr: pString,
        length: result.written,
        size,
        align,
    };
}

function freeString(alloc) {
    w.haku_free(alloc.ptr, alloc.size, alloc.align);
}

let textDecoder = new TextDecoder();
function readString(memory, size, pString) {
    let buffer = new Uint8Array(memory.buffer, pString, size);
    return textDecoder.decode(buffer);
}

function readCString(pCString) {
    let memoryBuffer = new Uint8Array(memory.buffer);

    let pCursor = pCString;
    while (memoryBuffer[pCursor] != 0 && memoryBuffer[pCursor] != null) {
        pCursor++;
    }

    let size = pCursor - pCString;
    return readString(memory, size, pCString);
}

function dup1to2(pHaku1, size, align) {
    if (size <= 0) {
        return {
            ptr: 0,
            length: 0,
            size: 0,
            align: 1,
        };
    }

    let pHaku2 = allocCheck(w2.haku2_alloc(size, align));

    let src = new Uint8Array(memory.buffer, pHaku1, size);
    let dst = new Uint8Array(memory2.buffer, pHaku2, size);
    dst.set(src);

    return {
        ptr: pHaku2,
        length: size,
        size,
        align,
    };
}

function freeString2(alloc) {
    if (alloc.ptr == 0) return;
    w2.haku2_free(alloc.ptr, alloc.size, alloc.align);
}

class Panic extends Error {
    name = "Panic";
}

panicImpl = (length, pMessage) => {
    throw new Panic(readString(memory, length, pMessage));
};

logImpl = (level, length, pMessage) => {
    console[level]("h1:", readString(memory, length, pMessage));
};

log2Impl = (level, pScope, scopeLen, pMsg, len) => {
    console[level](
        "h2:",
        `${readString(memory2, scopeLen, pScope)}: ${readString(memory2, len, pMsg)}`,
    );
};

canvasBeginImpl = w.haku_pixmap_begin;
canvasLineImpl = w.haku_pixmap_line;
canvasRectangleImpl = w.haku_pixmap_rectangle;
canvasCircleImpl = w.haku_pixmap_circle;
canvasFillImpl = w.haku_pixmap_fill;
canvasStrokeImpl = w.haku_pixmap_stroke;

w.haku_init_logging();

export class Pixmap {
    #pPixmap = 0;

    constructor(width, height) {
        this.#pPixmap = allocCheck(w.haku_pixmap_new(width, height));
        this.width = width;
        this.height = height;
    }

    destroy() {
        w.haku_pixmap_destroy(this.#pPixmap);
    }

    clear(r, g, b, a) {
        w.haku_pixmap_clear(this.#pPixmap, r, g, b, a);
    }

    get ptr() {
        return this.#pPixmap;
    }

    getArrayBuffer() {
        return new Uint8ClampedArray(
            memory.buffer,
            w.haku_pixmap_data(this.#pPixmap),
            this.width * this.height * 4,
        );
    }

    getImageData() {
        return new ImageData(this.getArrayBuffer(), this.width, this.height);
    }
}

export const ContKind = {
    Scribble: 0,
    Dotter: 1,
};

export class Haku {
    #pInstance = 0;

    #pLimits2 = 0;
    #pScratch2 = 0;
    #pDefs2 = 0;
    #pVm2 = 0;

    #bytecode2 = null;
    #localCount = 0;

    #fuel = 0;
    #renderMaxDepth = 0;

    constructor(limits) {
        console.groupCollapsed("construct Haku");
        {
            let pLimits = allocCheck(w.haku_limits_new());
            for (let name of Object.keys(limits)) {
                w[`haku_limits_set_${name}`](pLimits, limits[name]);
            }

            this.#pInstance = allocCheck(w.haku_instance_new(pLimits));

            w.haku_limits_destroy(pLimits);
        }
        console.groupEnd();

        // NOTE(v2): VM is not constructed until there is a brush ready
        this.#pLimits2 = allocCheck(w2.haku2_limits_new());
        w2.haku2_limits_set_stack_capacity(this.#pLimits2, limits.stack_capacity);
        w2.haku2_limits_set_call_stack_capacity(this.#pLimits2, limits.call_stack_capacity);
        this.#fuel = limits.fuel;
        this.#renderMaxDepth = limits.render_max_depth;

        this.#pScratch2 = allocCheck(w2.haku2_scratch_new(limits.memory));
    }

    destroy() {
        w.haku_instance_destroy(this.#pInstance);

        w2.haku2_scratch_destroy(this.#pScratch2);
        w2.haku2_vm_destroy(this.#pVm2);
        w2.haku2_defs_destroy(this.#pDefs2);
        w2.haku2_limits_destroy(this.#pLimits2);
        freeString2(this.#bytecode2);
    }

    setBrush(code) {
        w.haku_reset(this.#pInstance);

        if (this.#pVm2 != 0) w2.haku2_vm_destroy(this.#pVm2);
        if (this.#pDefs2 != 0) w2.haku2_defs_destroy(this.#pDefs2);
        if (this.#bytecode2 != null) freeString2(this.#bytecode2);
        this.#pVm2 = 0;
        this.#pDefs2 = 0;
        this.#bytecode2 = null;

        let brushCode = allocString(code);
        let statusCode = w.haku_compile_brush(this.#pInstance, brushCode.length, brushCode.ptr);
        freeString(brushCode);
        if (!w.haku_is_ok(statusCode)) {
            if (w.haku_is_diagnostics_emitted(statusCode)) {
                let diagnostics = [];
                for (let i = 0; i < w.haku_num_diagnostics(this.#pInstance); ++i) {
                    diagnostics.push({
                        start: w.haku_diagnostic_start(this.#pInstance, i),
                        end: w.haku_diagnostic_end(this.#pInstance, i),
                        message: readString(
                            memory,
                            w.haku_diagnostic_message_len(this.#pInstance, i),
                            w.haku_diagnostic_message(this.#pInstance, i),
                        ),
                    });
                }
                return {
                    status: "error",
                    errorKind: "diagnostics",
                    diagnostics,
                };
            } else {
                return {
                    status: "error",
                    errorKind: "plain",
                    message: readCString(w.haku_status_string(statusCode)),
                };
            }
        }

        let pDefsString = dup1to2(
            w.haku_defs(this.#pInstance),
            w.haku_defs_len(this.#pInstance),
            1,
        );
        let pTagsString = dup1to2(
            w.haku_tags(this.#pInstance),
            w.haku_tags_len(this.#pInstance),
            1,
        );
        this.#pDefs2 = allocCheck(
            w2.haku2_defs_parse(
                pDefsString.ptr,
                w.haku_defs_len(this.#pInstance),
                pTagsString.ptr,
                w.haku_tags_len(this.#pInstance),
            ),
        );

        w2.haku2_scratch_reset(this.#pScratch2);
        this.#pVm2 = allocCheck(w2.haku2_vm_new());

        this.#bytecode2 = dup1to2(
            w.haku_bytecode(this.#pInstance),
            w.haku_bytecode_len(this.#pInstance),
            1,
        );
        this.#localCount = w.haku_local_count(this.#pInstance);

        freeString2(pDefsString);
        freeString2(pTagsString);

        return { status: "ok" };
    }

    #exceptionMessage() {
        let len = w2.haku2_vm_exception_len(this.#pVm2);
        let pExn = allocCheck(w2.haku2_alloc(len, 1));
        w2.haku2_vm_exception_render(this.#pVm2, pExn);
        let exn = readString(memory2, len, pExn);
        w2.haku2_free(pExn, len, 1);
        return exn;
    }

    #stackTrace() {
        let count = w2.haku2_vm_stackframe_count(this.#pVm2);
        let trace = [];
        for (let i = 0; i < count; ++i) {
            // Don't collect information about return marker stack frames.
            // They don't actually represent function calls, they're only used to signal the .ret
            // instruction at which the VM ought to halt.
            let isReturnMarker = w2.haku2_vm_stackframe_is_return_marker(this.#pVm2, i) != 0;
            if (isReturnMarker) continue;

            let isSystem = w2.haku2_vm_stackframe_is_system(this.#pVm2, i) != 0;
            let pc = w2.haku2_vm_stackframe_pc(this.#pVm2, i);

            let span = null;
            if (!isSystem && pc > 0) {
                // NOTE: We find the span at (pc - 1), because stack frames' program counters are
                // situated at where control flow _ought_ to return, and not where the function call
                // is located. Therefore, to pull the program counter back into the function call,
                // we subtract 1.
                let pSpan = w.haku_bytecode_find_span(this.#pInstance, Math.max(0, pc - 1));
                if (pSpan != 0) {
                    span = {
                        start: w.haku_span_start(pSpan),
                        end: w.haku_span_end(pSpan),
                    };
                }
            }

            trace.push({
                isSystem,
                pc,
                span,
                functionName: readString(
                    memory2,
                    w2.haku2_vm_stackframe_function_name_len(this.#pVm2, i),
                    w2.haku2_vm_stackframe_function_name(this.#pVm2, i),
                ),
            });
        }
        return trace;
    }

    #exceptionResult() {
        return {
            status: "error",
            errorKind: "exception",
            description: "Runtime error",
            message: this.#exceptionMessage(),
            stackTrace: this.#stackTrace(),
        };
    }

    get ok() {
        return this.#pVm2 != 0;
    }

    beginBrush() {
        if (!this.ok) {
            console.debug("VM instance is not available for drawing");
            return { status: "ok" };
        }

        w2.haku2_scratch_reset(this.#pScratch2);
        w2.haku2_vm_reset(this.#pVm2, this.#pScratch2, this.#pDefs2, this.#pLimits2, this.#fuel);
        let ok = w2.haku2_vm_run_main(
            this.#pVm2,
            this.#pScratch2,
            this.#bytecode2.ptr,
            this.#bytecode2.size,
            this.#localCount,
        );
        if (!ok) {
            return this.#exceptionResult();
        }

        return { status: "ok" };
    }

    expectedContKind() {
        if (w2.haku2_vm_is_dotter(this.#pVm2)) return ContKind.Dotter;
        else return ContKind.Scribble;
    }

    contScribble(pixmap, translationX, translationY) {
        w.haku_pixmap_set_translation(pixmap.ptr, translationX, translationY);
        let ok = w2.haku2_render(this.#pVm2, pixmap.ptr, this.#renderMaxDepth);
        if (!ok) {
            return this.#exceptionResult();
        } else {
            return { status: "ok" };
        }
    }

    contDotter({ fromX, fromY, toX, toY, num }) {
        let ok = w2.haku2_vm_run_dotter(this.#pVm2, this.#pScratch2, fromX, fromY, toX, toY, num);
        if (!ok) {
            return this.#exceptionResult();
        } else {
            return { status: "ok" };
        }
    }

    async evalBrush(options) {
        let { runDotter, runScribble } = options;

        let result;
        result = this.beginBrush();
        if (result.status != "ok") return result;

        while (true) {
            switch (this.expectedContKind()) {
                case ContKind.Scribble:
                    result = await runScribble((pixmap, translationX, translationY) => {
                        return this.contScribble(pixmap, translationX, translationY);
                    });
                    return result;

                case ContKind.Dotter:
                    let dotter = await runDotter();
                    result = this.contDotter(dotter);
                    if (result.status != "ok") return result;
                    break;
            }
        }
    }

    get astSize() {
        return w.haku_stat_ast_size(this.#pInstance);
    }

    get remainingFuel() {
        return this.ok ? w2.haku2_vm_fuel(this.#pVm2) : this.#fuel;
    }

    get usedMemory() {
        return this.ok ? w2.haku2_scratch_used(this.#pScratch2) : 0;
    }
}
