// wasm/app/app.js
// Single-page app shell — CodeMirror 6 editor + Web-Worker-backed tier
// runner. The main thread stays responsive: the live ms counter ticks
// every animation frame, and Cancel terminates the worker mid-eval.

import { EditorState } from "@codemirror/state";
import { EditorView, keymap, lineNumbers, drawSelection } from "@codemirror/view";
import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
import { syntaxHighlighting, defaultHighlightStyle, StreamLanguage } from "@codemirror/language";
import { scheme } from "@codemirror/legacy-modes/mode/scheme";
import { oneDark } from "@codemirror/theme-one-dark";

const TIERS = { python: "python (pyodide)", c: "c (emcc)", asm: "asm (wat)" };

const demoSources = {};

async function loadDemoSource(name) {
    if (!demoSources[name]) {
        const resp = await fetch(`demos/${name}.lsp`);
        demoSources[name] = await resp.text();
    }
    return demoSources[name];
}

const editorParent = document.getElementById("editor");
const outputEl = document.getElementById("output");
const statusEl = document.getElementById("status");
const runBtn = document.getElementById("run");
const cancelBtn = document.getElementById("cancel");

const editorView = new EditorView({
    state: EditorState.create({
        doc: "",
        extensions: [
            lineNumbers(),
            history(),
            drawSelection(),
            syntaxHighlighting(defaultHighlightStyle),
            StreamLanguage.define(scheme),
            keymap.of([...defaultKeymap, ...historyKeymap]),
            oneDark,
            EditorView.theme({ "&": { height: "100%" } }),
        ],
    }),
    parent: editorParent,
});

function setEditorText(text) {
    editorView.dispatch({
        changes: { from: 0, to: editorView.state.doc.length, insert: text },
    });
}

function getEditorText() {
    return editorView.state.doc.toString();
}

async function loadCurrentDemo() {
    const sel = document.querySelector('input[name="program"]:checked').value;
    const src = await loadDemoSource(sel);
    setEditorText(src);
}

function selectedTiers() {
    const sel = document.querySelector('input[name="tier"]:checked').value;
    return sel === "all" ? ["python", "c", "asm"] : [sel];
}

function setStatus(text, cls) {
    statusEl.textContent = text || "";
    statusEl.className = "status" + (cls ? " " + cls : "");
}

// ─── Worker plumbing ───────────────────────────────────────────────
// One worker hosts all tiers. Cancel terminates it; the next eval
// creates a fresh one. Cached Pyodide / Emscripten state is lost on
// cancel, which is the price of true cancellation.

const workerState = { worker: null, runId: 0, pending: null };

function spawnWorker() {
    return new Worker(new URL("./worker.mjs", import.meta.url), { type: "module" });
}

function ensureWorker() {
    if (!workerState.worker) workerState.worker = spawnWorker();
    return workerState.worker;
}

function runOnTierInWorker(tier, src, onLoading) {
    return new Promise((resolve, reject) => {
        const w = ensureWorker();
        const myRunId = ++workerState.runId;
        workerState.pending = { runId: myRunId, resolve, reject };
        const handler = (e) => {
            if (e.data.runId !== myRunId) return;
            if (e.data.kind === "loading") {
                onLoading && onLoading(e.data.tier);
            } else if (e.data.kind === "done") {
                w.removeEventListener("message", handler);
                workerState.pending = null;
                resolve(e.data.output);
            } else if (e.data.kind === "error") {
                w.removeEventListener("message", handler);
                workerState.pending = null;
                reject(new Error(e.data.message));
            }
        };
        w.addEventListener("message", handler);
        w.postMessage({ kind: "eval", runId: myRunId, tier, src });
    });
}

function cancelCurrentRun() {
    if (workerState.worker) {
        workerState.worker.terminate();
        workerState.worker = null;
    }
    if (workerState.pending) {
        workerState.pending.reject(new Error("cancelled"));
        workerState.pending = null;
    }
}

// ─── Per-tier output block with live ms counter ────────────────────

function makeLiveBlock(tierName) {
    const block = document.createElement("div");
    block.className = "tier-block";
    const h = document.createElement("h3");
    h.textContent = TIERS[tierName] || tierName;
    const t = document.createElement("span");
    t.className = "time";
    t.textContent = "  loading…";
    h.appendChild(t);
    block.appendChild(h);
    const pre = document.createElement("pre");
    pre.textContent = "";
    block.appendChild(pre);
    outputEl.appendChild(block);

    let raf = 0;
    let start = 0;
    function tickerLoop() {
        const ms = (performance.now() - start) | 0;
        t.textContent = `  ${ms} ms…`;
        raf = requestAnimationFrame(tickerLoop);
    }
    return {
        startTimer() { start = performance.now(); tickerLoop(); return start; },
        stop(elapsedMs) {
            if (raf) cancelAnimationFrame(raf);
            t.textContent = `  (${elapsedMs.toFixed(0)} ms)`;
        },
        cancelled(elapsedMs) {
            if (raf) cancelAnimationFrame(raf);
            t.textContent = `  (cancelled @ ${elapsedMs.toFixed(0)} ms)`;
        },
        setOutput(text) { pre.textContent = text; },
        setError(msg) { pre.className = "err"; pre.textContent = msg; },
    };
}

// ─── Run / cancel ──────────────────────────────────────────────────

let inFlight = false;

async function runAll() {
    if (inFlight) return;
    inFlight = true;
    runBtn.disabled = true;
    cancelBtn.disabled = false;
    setStatus("running…", "busy");
    outputEl.innerHTML = "";
    let anyErr = false;
    let cancelled = false;
    try {
        const tiers = selectedTiers();
        const src = getEditorText();
        for (const t of tiers) {
            const live = makeLiveBlock(t);
            const startMark = live.startTimer();
            try {
                const output = await runOnTierInWorker(t, src, (loadingTier) => {
                    setStatus(`loading ${loadingTier} tier…`, "busy");
                });
                setStatus(`running ${t}…`, "busy");
                const elapsed = performance.now() - startMark;
                live.stop(elapsed);
                live.setOutput(output);
            } catch (e) {
                const elapsed = performance.now() - startMark;
                if (e.message === "cancelled") {
                    live.cancelled(elapsed);
                    cancelled = true;
                    break;
                }
                live.stop(elapsed);
                live.setError(e.message || String(e));
                anyErr = true;
            }
        }
        if (cancelled) setStatus("cancelled", "warn");
        else setStatus(anyErr ? "completed with errors" : "ok", anyErr ? "err" : "ok");
    } catch (e) {
        setStatus(`fatal: ${e.message}`, "err");
        outputEl.textContent = e.stack || e.message;
    } finally {
        inFlight = false;
        runBtn.disabled = false;
        cancelBtn.disabled = true;
    }
}

function onCancel() {
    if (!inFlight) return;
    setStatus("cancelling…", "warn");
    cancelCurrentRun();
}

document.querySelectorAll('input[name="program"]').forEach((el) => {
    el.addEventListener("change", loadCurrentDemo);
});
runBtn.addEventListener("click", runAll);
cancelBtn.addEventListener("click", onCancel);
cancelBtn.disabled = true;
loadCurrentDemo();
