import { EditorState, Compartment } from "@codemirror/state";
import { EditorView, keymap, lineNumbers, highlightActiveLine } from "@codemirror/view";
import { defaultKeymap, history, historyKeymap, indentWithTab } from "@codemirror/commands";
import { StreamLanguage, indentOnInput, bracketMatching, defaultHighlightStyle, syntaxHighlighting } from "@codemirror/language";
import { closeBrackets, closeBracketsKeymap, completionKeymap, acceptCompletion } from "@codemirror/autocomplete";
import { shell as shellLegacy } from "@codemirror/legacy-modes/mode/shell";

import { srccfgLanguage } from "./srccfg-mode.js";
import { editorLightTheme, editorDarkTheme, editorHighlighting } from "./themes.js";
import { autocompleteExtension } from "./autocomplete.js";

const bashLanguage = StreamLanguage.define(shellLegacy);

function pickLanguage(name) {
  if (name === "srccfg") return srccfgLanguage;
  if (name === "bash")   return bashLanguage;
  return null;  // "plain" / unknown → no language extension
}

function pickThemeForMatchMedia(mm) {
  return mm.matches ? editorDarkTheme : editorLightTheme;
}

function mount(textarea, { language = "plain", vocab = null, rows = 0 } = {}) {
  const langCompartment = new Compartment();
  const themeCompartment = new Compartment();
  const autocompleteCompartment = new Compartment();

  const lang = pickLanguage(language);
  const mm = window.matchMedia("(prefers-color-scheme: dark)");

  // Fix the editor's rendered height to match the textarea's `rows`
  // attribute via a small EditorView.theme — keeps the wrapper's
  // pre-reserved space (editor.css `.editor-mount`) and the actual
  // cm6 height in lockstep, eliminating the mount-time layout shift.
  // Empirical per-row metric on this build: 1.84rem content + 1.125rem
  // chrome (8px top + 8px bottom padding + 1px×2 border).
  const heightThemes = rows > 0 ? [
    EditorView.theme({
      "&": { height: `calc(${rows} * 1.84rem + 1.125rem)` },
      ".cm-scroller": { overflow: "auto" },
    }),
  ] : [];

  const extensions = [
    ...heightThemes,
    history(),
    lineNumbers(),
    highlightActiveLine(),
    bracketMatching(),
    closeBrackets(),
    indentOnInput(),
    syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
    editorHighlighting,
    themeCompartment.of(pickThemeForMatchMedia(mm)),
    langCompartment.of(lang ? [lang] : []),
    autocompleteCompartment.of(vocab ? [autocompleteExtension(vocab)] : []),
    keymap.of([
      // Tab → acceptCompletion when the popup is open; falls through
      // to indentWithTab when no popup. `run` returning false means
      // cm6 keeps walking the keymap list, so indentWithTab still
      // works as a fallback indent on Tab when typing normally.
      { key: "Tab", run: acceptCompletion },
      ...closeBracketsKeymap,
      ...defaultKeymap,
      ...historyKeymap,
      ...completionKeymap,
      indentWithTab,
    ]),
  ];

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

  // Insert the editor right before the textarea, then hide the textarea.
  textarea.parentElement.insertBefore(view.dom, textarea);
  textarea.style.display = "none";

  // OS-level theme swap
  const onThemeChange = () => view.dispatch({
    effects: themeCompartment.reconfigure(pickThemeForMatchMedia(mm)),
  });
  mm.addEventListener("change", onThemeChange);

  const controller = {
    getValue: () => view.state.doc.toString(),
    setContent: (text) => {
      view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: text } });
    },
    setLanguage: (name) => {
      const next = pickLanguage(name);
      view.dispatch({ effects: langCompartment.reconfigure(next ? [next] : []) });
    },
    destroy: () => {
      mm.removeEventListener("change", onThemeChange);
      view.destroy();
      textarea.style.display = "";
    },
  };
  return controller;
}

window.__editor = { mount };
