const defaultSource = `import {
  ArcRotateCamera,
  ColorGrading,
  FXAA,
  Mesh,
  PBRMaterial,
  PointLight,
  Scene,
} from "@yawn/handles";

const scene = new Scene(canvas, { hdr: true, fps: 60 });
await scene.ready;

const material = new PBRMaterial(scene, {
  baseColor: [0.12, 0.58, 1, 1],
  metallic: 0.15,
  roughness: 0.35,
});
await material.ready;

const mesh = new Mesh(scene, {
  material,
  vertexData: {
    positions: [-0.72, -0.62, 0, 0.72, -0.62, 0, 0, 0.76, 0],
    normals: [0, 0, 1, 0, 0, 1, 0, 0, 1],
    indices: [0, 1, 2],
  },
});
await mesh.ready;

log("Scene ready — drag your pointer across the preview.");
const move = (event: PointerEvent) => {
  const bounds = canvas.getBoundingClientRect();
  mesh.position.x = ((event.clientX - bounds.left) / bounds.width - 0.5) * 1.2;
  mesh.position.y = (0.5 - (event.clientY - bounds.top) / bounds.height) * 0.8;
};
canvas.addEventListener("pointermove", move);

export default {
  scene,
  mesh,
  dispose() {
    canvas.removeEventListener("pointermove", move);
    scene.dispose();
  },
};`;

const snippets = {
  camera: `// Orbit camera — drag to orbit and use the wheel to zoom.
const camera = new ArcRotateCamera(scene, {
  target: mesh,
  alpha: 0,
  beta: Math.PI / 2,
  radius: 3,
  controls: { element: canvas, pointer: true },
});
await camera.ready;`,
  light: `// A warm point light backed by shared rows.
const light = new PointLight(scene, {
  position: [0, 0.7, 1],
  color: [1, 0.68, 0.4],
  intensity: 12,
  range: 8,
});
await light.ready;`,
  instances: `// Clones share geometry until one clone changes its vertex data.
for (let x = -2; x <= 2; x++) {
  const instance = mesh.clone({ position: [x * 0.35, 0, 0] });
  await instance.ready;
}`,
  rows: `// Application-owned hot data in the same shared arena.
const velocity = await scene.ensureRows("app.velocity", 1024, 16, "f32");
velocity.row(0).set([1, 0, 0, 0]);
log("velocity[0]", velocity.read(0));`,
  post: `// Batch graph changes so the loadout is rebuilt once.
await scene.batchGraphUpdates(async () => {
  const grade = new ColorGrading(scene, { toneMap: "aces", amount: 1 });
  const fxaa = new FXAA(scene);
  await Promise.all([grade.ready, fxaa.ready]);
});`,
};

const elements = Object.fromEntries(
  [
    "canvas",
    "clear-console",
    "console-count",
    "console-empty",
    "console-lines",
    "cursor-status",
    "dirty-indicator",
    "editor",
    "editor-diagnostics",
    "format-button",
    "fps-status",
    "fullscreen-button",
    "inspector",
    "inspector-button",
    "preview",
    "preview-message",
    "profile-adapter",
    "profile-empty",
    "profile-latency",
    "profile-passes",
    "profile-results",
    "profile-total",
    "project-title",
    "reset-button",
    "resize-handle",
    "resolution-status",
    "revision-label",
    "run-button",
    "run-status",
    "save-button",
    "share-button",
    "snippets-button",
    "snippets-dialog",
    "tab-dirty",
    "toast",
    "type-status",
    "workbench",
  ].map((id) => [id, document.getElementById(id)]),
);

const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
const packages = Promise.all([import("/pkg/handles.js"), import("/pkg/core.js")]).then(
  ([handles, core]) => ({ ...handles, ...core }),
);
let monaco;
let editor;
let model;
let current;
let currentID = "";
let currentRevision = 0;
let initialSource = defaultSource;
let generation = 0;
let dirty = false;
let activePanel = "console";
let stopProfile;
let profileCore;
let toastTimer;
let frameRequest;
let sampledFrame = 0;
let sampledAt = 0;

function savedLocation() {
  const match = location.pathname.match(/^\/playground\/([a-z0-9]{6,24})\/(\d+)\/?$/);
  return match ? { id: match[1], revision: Number(match[2]) } : undefined;
}

async function loadInitialPlayground() {
  const saved = savedLocation();
  if (saved) {
    try {
      const response = await fetch(`/api/playgrounds/${saved.id}/${saved.revision}`);
      if (!response.ok) throw new Error("This saved playground could not be found.");
      const result = await response.json();
      currentID = result.id;
      currentRevision = result.revision;
      elements["project-title"].value = result.title;
      elements["revision-label"].textContent = `Revision ${result.revision}`;
      elements["share-button"].hidden = false;
      initialSource = result.code;
      document.title = `${result.title} — Yawn Playground`;
      return result.code;
    } catch (error) {
      history.replaceState(null, "", "/playground");
      setRunStatus(error.message, "failed");
      showToast(error.message);
    }
  }
  return localStorage.getItem("yawn:playground:draft") || defaultSource;
}

function loadMonaco() {
  return new Promise((resolve, reject) => {
    if (!window.require?.config) {
      reject(new Error("The TypeScript editor could not be loaded."));
      return;
    }
    const monacoBase = `${window.location.origin}/assets/monaco/`;
    const workerSource = `
self.MonacoEnvironment = { baseUrl: ${JSON.stringify(monacoBase)} };
importScripts(${JSON.stringify(`${monacoBase}vs/base/worker/workerMain.js`)});
`;
    const workerURL = URL.createObjectURL(
      new Blob([workerSource], { type: "text/javascript" }),
    );
    window.MonacoEnvironment = { getWorkerUrl: () => workerURL };
    window.require.config({ paths: { vs: `${monacoBase}vs` } });
    window.require(["vs/editor/editor.main"], () => resolve(window.monaco), reject);
  });
}

async function createEditor(source) {
  monaco = await loadMonaco();
  const types = await fetch("/assets/yawn.d.ts").then((response) => response.text());
  const defaults = monaco.languages.typescript.typescriptDefaults;
  defaults.setEagerModelSync(true);
  defaults.setCompilerOptions({
    allowNonTsExtensions: true,
    module: monaco.languages.typescript.ModuleKind.ESNext,
    moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
    noEmit: false,
    strict: true,
    target: monaco.languages.typescript.ScriptTarget.ESNext,
  });
  defaults.setDiagnosticsOptions({ noSemanticValidation: false, noSyntaxValidation: false });
  defaults.addExtraLib(types, "file:///types/yawn.d.ts");
  monaco.editor.defineTheme("yawn", {
    base: "vs-dark",
    inherit: true,
    rules: [
      { token: "comment", foreground: "658198", fontStyle: "italic" },
      { token: "keyword", foreground: "C792EA" },
      { token: "string", foreground: "8FD5B6" },
      { token: "number", foreground: "F6AD7B" },
      { token: "type.identifier", foreground: "69C7F4" },
    ],
    colors: {
      "editor.background": "#0B1927",
      "editor.foreground": "#C8D8E5",
      "editor.lineHighlightBackground": "#102235",
      "editor.selectionBackground": "#1D5B8066",
      "editorCursor.foreground": "#55BDF4",
      "editorGutter.background": "#0B1927",
      "editorLineNumber.foreground": "#405B70",
      "editorLineNumber.activeForeground": "#89A4B8",
      "editorIndentGuide.background1": "#1B3143",
      "editorWidget.background": "#102538",
      "editorSuggestWidget.background": "#102538",
    },
  });
  elements.editor.textContent = "";
  model = monaco.editor.createModel(source, "typescript", monaco.Uri.parse("file:///playground/scene.ts"));
  editor = monaco.editor.create(elements.editor, {
    model,
    theme: "yawn",
    automaticLayout: true,
    fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
    fontLigatures: true,
    fontSize: 12,
    lineHeight: 20,
    minimap: { enabled: false },
    padding: { top: 12, bottom: 12 },
    renderLineHighlight: "all",
    scrollBeyondLastLine: false,
    smoothScrolling: true,
    tabSize: 2,
    wordWrap: "off",
  });
  editor.addAction({
    id: "yawn.run",
    label: "Run playground",
    keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter],
    run,
  });
  editor.onDidChangeCursorPosition(({ position }) => {
    elements["cursor-status"].textContent = `Ln ${position.lineNumber}, Col ${position.column}`;
  });
  model.onDidChangeContent(() => {
    markDirty(true);
    if (!currentID) localStorage.setItem("yawn:playground:draft", model.getValue());
  });
  monaco.editor.onDidChangeMarkers(([resource]) => {
    if (resource.toString() === model.uri.toString()) updateDiagnostics();
  });
  elements["type-status"].textContent = "TypeScript language service ready";
  updateDiagnostics();
}

function updateDiagnostics() {
  if (!monaco || !model) return;
  const markers = monaco.editor.getModelMarkers({ resource: model.uri });
  const errors = markers.filter((marker) => marker.severity === monaco.MarkerSeverity.Error).length;
  const warnings = markers.filter((marker) => marker.severity === monaco.MarkerSeverity.Warning).length;
  const total = errors + warnings;
  elements["editor-diagnostics"].classList.toggle("has-errors", errors > 0);
  elements["editor-diagnostics"].lastChild.textContent = total
    ? ` ${errors} error${errors === 1 ? "" : "s"}, ${warnings} warning${warnings === 1 ? "" : "s"}`
    : " No problems";
  elements["type-status"].textContent = errors
    ? `${errors} TypeScript error${errors === 1 ? "" : "s"}`
    : "TypeScript language service ready";
}

function markDirty(value) {
  dirty = value;
  elements["dirty-indicator"].classList.toggle("visible", value);
  elements["tab-dirty"].classList.toggle("visible", value);
}

async function transpile() {
  const worker = await monaco.languages.typescript.getTypeScriptWorker();
  const client = await worker(model.uri);
  const emitted = await client.getEmitOutput(model.uri.toString());
  const output = emitted.outputFiles.find((file) => file.name.endsWith(".js"));
  if (emitted.emitSkipped || !output) throw new Error("TypeScript could not emit this scene.");
  return output.text
    .replace(/^\s*import\s+(?:[\s\S]*?\s+from\s+)?["'][^"']+["'];?\s*$/gm, "")
    .replace(/^\s*export\s*\{\s*\};?\s*$/gm, "")
    .replace(/\bexport\s+default\s+/, "return ");
}

async function disposeCurrent() {
  stopProfile?.();
  stopProfile = undefined;
  if (profileCore) {
    try {
      await profileCore.setProfiler(false);
    } catch {
      // A disposed core cannot disable a profiler that is already gone.
    }
    profileCore = undefined;
  }
  const value = current;
  current = undefined;
  delete window.__yawnPlayground;
  if (!value) return;
  if (typeof value === "function") await value();
  else if (typeof value.dispose === "function") await value.dispose();
  else if (value.scene?.dispose) value.scene.dispose();
}

function replaceCanvas() {
  const previous = elements.canvas;
  const canvas = document.createElement("canvas");
  canvas.id = "canvas";
  canvas.setAttribute("aria-label", "Yawn WebGPU preview");
  const scale = Math.min(devicePixelRatio || 1, 2);
  canvas.width = Math.max(1, Math.round(elements.preview.clientWidth * scale));
  canvas.height = Math.max(1, Math.round(elements.preview.clientHeight * scale));
  previous.replaceWith(canvas);
  elements.canvas = canvas;
  elements["resolution-status"].textContent = `${canvas.width} × ${canvas.height}`;
  return canvas;
}

async function run() {
  if (!editor || elements["run-button"].disabled) return;
  const runID = ++generation;
  elements["run-button"].disabled = true;
  elements["run-button"].lastChild.textContent = " Running";
  clearConsole();
  setRunStatus("Compiling", "running");
  showPreviewMessage("Compiling TypeScript", "Checking types and preparing the render graph.");
  try {
    if (!crossOriginIsolated) throw new Error("Cross-origin isolation is disabled on this page.");
    if (!navigator.gpu) throw new Error("WebGPU is not available in this browser.");
    const [code, api] = await Promise.all([transpile(), packages]);
    await disposeCurrent();
    const canvas = replaceCanvas();
    const names = Object.keys(api);
    const started = performance.now();
    const result = await new AsyncFunction(...names, "canvas", "log", code)(
      ...names.map((name) => api[name]),
      canvas,
      (...values) => appendConsole(values.map(formatValue).join(" ")),
    );
    if (runID !== generation) {
      if (result?.dispose) await result.dispose();
      return;
    }
    current = result;
    window.__yawnPlayground = result;
    elements["preview-message"].hidden = true;
    setRunStatus(`Running · ${Math.round(performance.now() - started)} ms`, "ready");
    if (activePanel === "profile") await attachProfiler();
  } catch (error) {
    if (runID === generation) {
      const message = error instanceof Error ? error.message : String(error);
      appendConsole(message, true);
      setRunStatus("Run failed", "failed");
      showPreviewMessage("Scene could not start", message);
      openInspector("console");
    }
  } finally {
    if (runID === generation) {
      elements["run-button"].disabled = false;
      elements["run-button"].innerHTML = '<span aria-hidden="true">▶</span> Run <kbd>Ctrl ↵</kbd>';
    }
  }
}

function currentCore() {
  return current?.scene?.core ?? current?.core;
}

async function attachProfiler() {
  stopProfile?.();
  stopProfile = undefined;
  profileCore = currentCore();
  if (!profileCore?.onProfile || !profileCore?.setProfiler) {
    setProfileMessage("Run a scene to begin profiling.");
    return;
  }
  stopProfile = profileCore.onProfile(renderProfile);
  const supported = await profileCore.setProfiler(true);
  if (!supported) setProfileMessage("Timestamp queries are not supported by this GPU adapter.");
}

function setProfileMessage(message) {
  elements["profile-empty"].querySelector("strong").textContent = message;
  elements["profile-empty"].hidden = false;
  elements["profile-results"].hidden = true;
}

function renderProfile(profile) {
  elements["profile-empty"].hidden = true;
  elements["profile-results"].hidden = false;
  elements["profile-total"].textContent = `${profile.milliseconds.toFixed(2)} ms`;
  elements["profile-latency"].textContent = `${profile.readbackMilliseconds.toFixed(2)} ms`;
  elements["profile-adapter"].textContent = profile.adapter || "WebGPU adapter";
  elements["profile-adapter"].title = profile.adapter || "WebGPU adapter";
  elements["profile-passes"].replaceChildren(
    ...profile.passes.map((pass) => {
      const row = document.createElement("tr");
      const name = document.createElement("td");
      const time = document.createElement("td");
      name.textContent = pass.name;
      time.textContent = `${pass.milliseconds.toFixed(2)} ms`;
      row.append(name, time);
      return row;
    }),
  );
}

function formatValue(value) {
  if (typeof value === "string") return value;
  if (value instanceof Error) return value.message;
  try {
    return JSON.stringify(value);
  } catch {
    return String(value);
  }
}

function appendConsole(message, error = false) {
  elements["console-empty"].hidden = true;
  const line = document.createElement("li");
  line.textContent = message;
  line.classList.toggle("error", error);
  elements["console-lines"].append(line);
  elements["console-count"].textContent = elements["console-lines"].children.length;
  line.scrollIntoView({ block: "nearest" });
}

function clearConsole() {
  elements["console-lines"].replaceChildren();
  elements["console-empty"].hidden = false;
  elements["console-count"].textContent = "0";
}

function setRunStatus(message, state) {
  elements["run-status"].className = state === "running" ? "running" : state === "failed" ? "failed" : "";
  elements["run-status"].lastChild.textContent = ` ${message}`;
}

function showPreviewMessage(title, detail) {
  elements["preview-message"].hidden = false;
  elements["preview-message"].querySelector("strong").textContent = title;
  elements["preview-message"].querySelector("small").textContent = detail;
}

function openInspector(panel) {
  activePanel = panel;
  elements.inspector.classList.remove("collapsed");
  elements["inspector-button"].setAttribute("aria-pressed", "true");
  for (const button of document.querySelectorAll(".inspector-tabs [data-panel]")) {
    const active = button.dataset.panel === panel;
    button.classList.toggle("active", active);
    button.setAttribute("aria-selected", String(active));
  }
  for (const target of document.querySelectorAll(".inspector-panel")) {
    target.classList.toggle("active", target.id === `${panel}-panel`);
  }
  if (panel === "profile") void attachProfiler();
  else if (profileCore) {
    stopProfile?.();
    stopProfile = undefined;
    void profileCore.setProfiler(false).catch(() => undefined);
    profileCore = undefined;
  }
}

async function save() {
  if (!editor || elements["save-button"].disabled) return;
  elements["save-button"].disabled = true;
  try {
    const response = await fetch("/api/playgrounds", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        id: currentID,
        title: elements["project-title"].value.trim() || "Untitled playground",
        code: model.getValue(),
      }),
    });
    const result = await response.json();
    if (!response.ok) throw new Error(result.error || "The playground could not be saved.");
    currentID = result.id;
    currentRevision = result.revision;
    initialSource = result.code;
    history.pushState(null, "", `/playground/${result.id}/${result.revision}`);
    elements["revision-label"].textContent = `Revision ${result.revision}`;
    elements["share-button"].hidden = false;
    localStorage.removeItem("yawn:playground:draft");
    markDirty(false);
    showToast(`Saved revision ${result.revision}`);
  } catch (error) {
    showToast(error instanceof Error ? error.message : String(error));
  } finally {
    elements["save-button"].disabled = false;
  }
}

function showToast(message) {
  clearTimeout(toastTimer);
  elements.toast.textContent = message;
  elements.toast.classList.add("visible");
  toastTimer = setTimeout(() => elements.toast.classList.remove("visible"), 2400);
}

function insertSnippet(name) {
  const selection = editor.getSelection();
  const prefix = model.getValueInRange(selection).endsWith("\n") ? "" : "\n\n";
  editor.executeEdits("snippet", [{ range: selection, text: `${prefix}${snippets[name]}\n`, forceMoveMarkers: true }]);
  elements["snippets-dialog"].close();
  editor.focus();
}

function sampleFPS(now = performance.now()) {
  try {
    const core = currentCore();
    if (!core) throw new Error();
    const frame = Number(core.array("signals").row(0)[1]);
    if (!sampledAt || frame < sampledFrame) {
      sampledAt = now;
      sampledFrame = frame;
    } else if (now - sampledAt >= 500) {
      const fps = ((frame - sampledFrame) * 1000) / (now - sampledAt);
      elements["fps-status"].textContent = `${fps < 10 ? fps.toFixed(1) : Math.round(fps)} FPS`;
      sampledAt = now;
      sampledFrame = frame;
    }
  } catch {
    sampledAt = now;
    sampledFrame = 0;
    elements["fps-status"].textContent = "0 FPS";
  }
  frameRequest = requestAnimationFrame(sampleFPS);
}

elements["run-button"].addEventListener("click", run);
elements["save-button"].addEventListener("click", save);
elements["share-button"].addEventListener("click", async () => {
  await navigator.clipboard.writeText(location.href);
  showToast("Playground URL copied");
});
elements["project-title"].addEventListener("input", () => markDirty(true));
elements["reset-button"].addEventListener("click", () => {
  model?.setValue(initialSource);
  void run();
});
elements["format-button"].addEventListener("click", () => editor?.getAction("editor.action.formatDocument").run());
elements["snippets-button"].addEventListener("click", () => elements["snippets-dialog"].showModal());
elements["snippets-dialog"].querySelector("[data-close-dialog]").addEventListener("click", () => elements["snippets-dialog"].close());
elements["snippets-dialog"].addEventListener("click", (event) => {
  if (event.target === elements["snippets-dialog"]) elements["snippets-dialog"].close();
});
for (const button of document.querySelectorAll("[data-snippet]")) {
  button.addEventListener("click", () => insertSnippet(button.dataset.snippet));
}
elements["snippets-dialog"].querySelector("input").addEventListener("input", (event) => {
  const query = event.target.value.trim().toLowerCase();
  for (const button of document.querySelectorAll("[data-snippet]")) {
    button.hidden = Boolean(query) && !button.textContent.toLowerCase().includes(query);
  }
});
for (const button of document.querySelectorAll(".inspector-tabs [data-panel]")) {
  button.addEventListener("click", () => openInspector(button.dataset.panel));
}
elements["clear-console"].addEventListener("click", clearConsole);
elements["inspector-button"].addEventListener("click", () => {
  const collapsed = elements.inspector.classList.toggle("collapsed");
  elements["inspector-button"].setAttribute("aria-pressed", String(!collapsed));
});
elements["fullscreen-button"].addEventListener("click", () => {
  if (document.fullscreenElement) void document.exitFullscreen();
  else void document.documentElement.requestFullscreen();
});

elements["resize-handle"].addEventListener("pointerdown", (event) => {
  if (innerWidth <= 800) return;
  elements["resize-handle"].setPointerCapture(event.pointerId);
  elements["resize-handle"].classList.add("dragging");
});
elements["resize-handle"].addEventListener("pointermove", (event) => {
  if (!elements["resize-handle"].hasPointerCapture(event.pointerId)) return;
  const bounds = elements.workbench.getBoundingClientRect();
  const percentage = Math.min(70, Math.max(30, ((event.clientX - bounds.left) / bounds.width) * 100));
  elements.workbench.style.setProperty("--editor-width", `${percentage}%`);
});
elements["resize-handle"].addEventListener("pointerup", (event) => {
  elements["resize-handle"].releasePointerCapture(event.pointerId);
  elements["resize-handle"].classList.remove("dragging");
});
addEventListener("popstate", () => location.reload());
addEventListener("beforeunload", () => {
  cancelAnimationFrame(frameRequest);
  void disposeCurrent();
});

try {
  const source = await loadInitialPlayground();
  await createEditor(source);
  markDirty(false);
  sampleFPS();
  await run();
} catch (error) {
  const message = error instanceof Error ? error.message : String(error);
  elements.editor.textContent = message;
  setRunStatus("Editor failed", "failed");
  showPreviewMessage("Editor could not start", message);
}
