import { useEffect, useRef } from "react";
import { EditorView, keymap, lineNumbers, highlightActiveLine, Decoration, ViewPlugin } from "@codemirror/view";
import { EditorState, RangeSetBuilder } from "@codemirror/state";
import type { Extension } from "@codemirror/state";
import type { DecorationSet } from "@codemirror/view";
import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
import { searchKeymap } from "@codemirror/search";
import { oneDark } from "./oneDarkTheme";

function toRoman(n: number): string {
  const vals = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1];
  const syms = ["M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"];
  let result = "";
  for (let i = 0; i < vals.length; i++) {
    while (n >= vals[i]) {
      result += syms[i];
      n -= vals[i];
    }
  }
  return result;
}

const dotDeco = Decoration.mark({ class: "cm-dot-space" });

function buildSpaceDeco(view: EditorView): DecorationSet {
  const builder = new RangeSetBuilder<Decoration>();
  for (const { from, to } of view.visibleRanges) {
    const text = view.state.sliceDoc(from, to);
    for (let i = 0; i < text.length; i++) {
      if (text[i] === " ") {
        builder.add(from + i, from + i + 1, dotDeco);
      }
    }
  }
  return builder.finish();
}

const subtleWhitespace = ViewPlugin.fromClass(
  class {
    decorations: DecorationSet;
    constructor(view: EditorView) {
      this.decorations = buildSpaceDeco(view);
    }
    update(update: { docChanged: boolean; viewportChanged: boolean; view: EditorView }) {
      if (update.docChanged || update.viewportChanged) {
        this.decorations = buildSpaceDeco(update.view);
      }
    }
  },
  { decorations: (v) => v.decorations },
);

interface Props {
  value: string;
  onChange: (value: string) => void;
  extensions?: Extension[];
}

export default function EditorPane({ value, onChange, extensions = [] }: Props) {
  const containerRef = useRef<HTMLDivElement>(null);
  const viewRef = useRef<EditorView>(null);
  const onChangeRef = useRef(onChange);
  onChangeRef.current = onChange;

  useEffect(() => {
    if (!containerRef.current) return;

    const state = EditorState.create({
      doc: value,
      extensions: [
        history(),
        lineNumbers({ formatNumber: toRoman }),
        highlightActiveLine(),
        subtleWhitespace,
        keymap.of([...defaultKeymap, ...historyKeymap, ...searchKeymap]),
        oneDark,
        EditorView.updateListener.of((update) => {
          if (update.docChanged) {
            onChangeRef.current(update.state.doc.toString());
          }
          if (update.selectionSet || update.docChanged) {
            const pos = update.state.selection.main.head;
            const coords = update.view.coordsAtPos(pos);
            if (coords) {
              window.dispatchEvent(
                new CustomEvent("cm-cursor-move", { detail: { top: coords.top, left: coords.left } }),
              );
            }
          }
        }),
        EditorView.theme({
          "&": { height: "100%", fontSize: "14px" },
          ".cm-scroller": { overflow: "auto" },
          ".cm-content": { fontFamily: "monospace", padding: "16px" },
          ".cm-cursor, .cm-cursor-primary": {
            borderLeftColor: "var(--primary)",
            borderLeftWidth: "0.5em",
          },
          ".cm-dot-space": {
            color: "transparent",
            position: "relative",
          },
          ".cm-dot-space:before": {
            content: '"\\22C5"',
            position: "absolute",
            left: "0",
            right: "0",
            textAlign: "center",
            color: "var(--muted-foreground)",
            opacity: "0.5",
            pointerEvents: "none",
          },
        }),
        ...extensions,
      ],
    });

    const view = new EditorView({ state, parent: containerRef.current });
    viewRef.current = view;

    return () => view.destroy();
  }, []); // Only mount once

  // Sync external value changes (e.g. loading a page)
  useEffect(() => {
    const view = viewRef.current;
    if (view && view.state.doc.toString() !== value) {
      view.dispatch({
        changes: { from: 0, to: view.state.doc.length, insert: value },
      });
    }
  }, [value]);

  return <div ref={containerRef} style={{ height: "100%" }} />;
}
