// In-browser file editing (CodeMirror 6).
//
// **Enhancement only.** The page ships a working `<textarea>` inside an
// ordinary form. This script replaces it with an editor and copies the content
// back on submit. If the bundle fails to load, is blocked, or throws, the
// textarea is what remains and saving still works — which is the same
// progressive-enhancement rule the rest of the product follows (spec §7).
//
// The bundle is served from our own origin and executed under a per-request
// nonce, because the CSP has no `unsafe-inline` and no external hosts.

import { EditorState, Compartment } from "@codemirror/state";
import { EditorView, keymap, highlightActiveLine, lineNumbers } from "@codemirror/view";
import { indentWithTab } from "@codemirror/commands";
import { basicSetup } from "codemirror";
import { StreamLanguage } from "@codemirror/language";

import { rust } from "@codemirror/lang-rust";
import { python } from "@codemirror/lang-python";
import { javascript } from "@codemirror/lang-javascript";
import { json } from "@codemirror/lang-json";
import { markdown } from "@codemirror/lang-markdown";
import { html } from "@codemirror/lang-html";
import { css } from "@codemirror/lang-css";
import { java } from "@codemirror/lang-java";
import { cpp } from "@codemirror/lang-cpp";
import { php } from "@codemirror/lang-php";
import { sql } from "@codemirror/lang-sql";
import { xml } from "@codemirror/lang-xml";
import { yaml } from "@codemirror/lang-yaml";
import { shell } from "@codemirror/legacy-modes/mode/shell";
import { toml } from "@codemirror/legacy-modes/mode/toml";
import { go } from "@codemirror/legacy-modes/mode/go";
import { ruby } from "@codemirror/legacy-modes/mode/ruby";

// Deliberately the same set the server-side highlighter covers, so a file that
// is coloured when you read it is coloured when you edit it. Keys match the
// grammar names df-render uses.
const LANGUAGES = {
  rust: () => rust(),
  python: () => python(),
  javascript: () => javascript(),
  typescript: () => javascript({ typescript: true }),
  tsx: () => javascript({ typescript: true, jsx: true }),
  json: () => json(),
  markdown: () => markdown(),
  html: () => html(),
  css: () => css(),
  java: () => java(),
  c: () => cpp(),
  cpp: () => cpp(),
  php: () => php(),
  sql: () => sql(),
  xml: () => xml(),
  yaml: () => yaml(),
  bash: () => StreamLanguage.define(shell),
  toml: () => StreamLanguage.define(toml),
  go: () => StreamLanguage.define(go),
  ruby: () => StreamLanguage.define(ruby),
};

// The editor takes its colours from the page's CSS custom properties rather
// than shipping a theme of its own. That way it matches whichever theme the
// reader is in, including one added later, with no toggle to keep in sync.
const theme = EditorView.theme({
  "&": {
    color: "var(--text)",
    backgroundColor: "var(--bg)",
    border: "1px solid var(--border)",
    borderRadius: "var(--radius)",
    fontSize: "13px",
  },
  ".cm-content": {
    fontFamily: "var(--font-mono)",
    caretColor: "var(--identity)",
  },
  ".cm-cursor, .cm-dropCursor": { borderLeftColor: "var(--identity)" },
  "&.cm-focused": { outline: "2px solid var(--action)", outlineOffset: "-1px" },
  "&.cm-focused .cm-selectionBackground, .cm-selectionBackground, ::selection": {
    backgroundColor: "color-mix(in srgb, var(--action) 30%, transparent)",
  },
  ".cm-gutters": {
    backgroundColor: "var(--surface)",
    color: "var(--text-faint)",
    borderRight: "1px solid var(--border)",
  },
  ".cm-activeLine": {
    backgroundColor: "color-mix(in srgb, var(--identity) 8%, transparent)",
  },
  ".cm-activeLineGutter": { backgroundColor: "transparent", color: "var(--text-dim)" },
  ".cm-scroller": { overflow: "auto", maxHeight: "70vh" },
});

function enhance(form) {
  const textarea = form.querySelector("textarea[data-editor]");
  if (!textarea) return;

  const language = textarea.dataset.language || "";
  const factory = LANGUAGES[language];

  const extensions = [
    basicSetup,
    theme,
    // Tab indents rather than moving focus. Bound explicitly because taking
    // Tab away from keyboard navigation is only acceptable when it is doing
    // something — Escape then Tab still leaves the editor.
    keymap.of([indentWithTab]),
    EditorView.lineWrapping,
  ];
  if (factory) {
    try {
      extensions.push(factory());
    } catch (e) {
      // A grammar that fails to construct must not cost the user their editor.
      console.warn("dogfood: language mode failed to load", language, e);
    }
  }

  const view = new EditorView({
    state: EditorState.create({ doc: textarea.value, extensions }),
    parent: textarea.parentElement,
  });

  // The textarea remains the thing that is submitted; it is hidden rather than
  // removed so the form keeps working exactly as it did.
  textarea.style.display = "none";
  textarea.setAttribute("aria-hidden", "true");
  textarea.tabIndex = -1;

  // CodeMirror's editable area is not a labelled control, so name it from the
  // same label the textarea used.
  const content = view.contentDOM;
  content.setAttribute("role", "textbox");
  content.setAttribute("aria-multiline", "true");
  const label = form.querySelector(`label[for="${textarea.id}"]`);
  if (label) content.setAttribute("aria-label", label.textContent.trim());

  form.addEventListener("submit", () => {
    textarea.value = view.state.doc.toString();
  });

  // Warn before navigating away from unsaved work. Registered only once the
  // editor is actually up, so the no-JavaScript path never gets a stray prompt.
  const initial = textarea.value;
  let saving = false;
  form.addEventListener("submit", () => {
    saving = true;
  });
  window.addEventListener("beforeunload", (e) => {
    if (saving) return;
    if (view.state.doc.toString() !== initial) {
      e.preventDefault();
      e.returnValue = "";
    }
  });
}

for (const form of document.querySelectorAll("form[data-editor-form]")) {
  try {
    enhance(form);
  } catch (e) {
    // Leaves the plain textarea in place, which is a working editor.
    console.error("dogfood: could not start the editor", e);
  }
}
