import { useRef, useCallback, useEffect, memo } from "react";
import {
  EditorView,
  keymap,
  lineNumbers,
  highlightActiveLine,
  highlightActiveLineGutter,
} from "@codemirror/view";
import { EditorState } from "@codemirror/state";
import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
import { bracketMatching, foldGutter, indentOnInput } from "@codemirror/language";
import { closeBrackets, closeBracketsKeymap } from "@codemirror/autocomplete";
import { highlightSelectionMatches, searchKeymap } from "@codemirror/search";
import { oneDark } from "@codemirror/theme-one-dark";
import { html } from "@codemirror/lang-html";
import { css } from "@codemirror/lang-css";
import { javascript } from "@codemirror/lang-javascript";

function getLanguageExtension(language: string) {
  switch (language) {
    case "html":
      return html();
    case "css":
      return css();
    case "javascript":
    case "js":
    case "typescript":
    case "ts":
      return javascript({
        typescript: language === "typescript" || language === "ts",
      });
    default:
      return html();
  }
}

function detectLanguage(filePath: string): string {
  const ext = filePath.split(".").pop()?.toLowerCase() ?? "";
  const map: Record<string, string> = {
    html: "html",
    htm: "html",
    css: "css",
    js: "javascript",
    ts: "typescript",
    jsx: "javascript",
    tsx: "typescript",
    json: "javascript",
  };
  return map[ext] ?? "html";
}

interface SourceEditorProps {
  content: string;
  filePath?: string;
  language?: string;
  onChange?: (content: string) => void;
  readOnly?: boolean;
  revealOffset?: number | null;
}

export const SourceEditor = memo(function SourceEditor({
  content,
  filePath,
  language,
  onChange,
  readOnly = false,
  revealOffset,
}: SourceEditorProps) {
  const editorRef = useRef<EditorView | null>(null);
  const containerRef = useRef<HTMLDivElement | null>(null);
  const onChangeRef = useRef(onChange);
  onChangeRef.current = onChange;

  const contentRef = useRef(content);
  contentRef.current = content;

  const mountEditor = useCallback(
    (node: HTMLDivElement | null) => {
      if (editorRef.current) {
        editorRef.current.destroy();
        editorRef.current = null;
      }
      if (!node) return;
      containerRef.current = node;

      const lang = language ?? (filePath ? detectLanguage(filePath) : "html");

      const updateListener = EditorView.updateListener.of((update) => {
        if (update.docChanged && onChangeRef.current) {
          onChangeRef.current(update.state.doc.toString());
        }
      });

      const state = EditorState.create({
        doc: contentRef.current,
        extensions: [
          lineNumbers(),
          highlightActiveLine(),
          highlightActiveLineGutter(),
          history(),
          foldGutter(),
          indentOnInput(),
          bracketMatching(),
          closeBrackets(),
          highlightSelectionMatches(),
          keymap.of([...closeBracketsKeymap, ...defaultKeymap, ...searchKeymap, ...historyKeymap]),
          getLanguageExtension(lang),
          oneDark,
          updateListener,
          EditorState.readOnly.of(readOnly),
          EditorView.theme({
            "&": { height: "100%" },
            ".cm-scroller": { overflow: "auto" },
          }),
        ],
      });

      editorRef.current = new EditorView({ state, parent: node });
    },
    [filePath, language, readOnly],
  );

  // Sync external content changes into the editor without recreating it.
  // Only applies when the new content differs from the current document
  // (e.g. file switch or server refresh), not on every keystroke.
  useEffect(() => {
    const view = editorRef.current;
    if (!view) return;
    const current = view.state.doc.toString();
    if (current !== content) {
      view.dispatch({
        changes: { from: 0, to: current.length, insert: content },
      });
    }
  }, [content]);

  useEffect(() => {
    const view = editorRef.current;
    if (!view || revealOffset == null || revealOffset < 0) return;
    const docLen = view.state.doc.length;
    const pos = Math.min(revealOffset, docLen);
    view.dispatch({
      selection: { anchor: pos },
      effects: EditorView.scrollIntoView(pos, { y: "center" }),
    });
    view.focus();
  }, [revealOffset]);

  return <div ref={mountEditor} className="h-full w-full overflow-hidden" />;
});
