import { useEffect, useMemo, useRef, useState } from "react";
import hljs from "highlight.js/lib/core";
import cpp from "highlight.js/lib/languages/cpp";
import csharp from "highlight.js/lib/languages/csharp";
import python from "highlight.js/lib/languages/python";
import javascript from "highlight.js/lib/languages/javascript";
import typescript from "highlight.js/lib/languages/typescript";
import json from "highlight.js/lib/languages/json";
import xml from "highlight.js/lib/languages/xml";
import ini from "highlight.js/lib/languages/ini";
import glsl from "highlight.js/lib/languages/glsl";
import { p4, OpenedFile, splitPath } from "./p4";
import { t } from "./i18n";

hljs.registerLanguage("cpp", cpp);
hljs.registerLanguage("csharp", csharp);
hljs.registerLanguage("python", python);
hljs.registerLanguage("javascript", javascript);
hljs.registerLanguage("typescript", typescript);
hljs.registerLanguage("json", json);
hljs.registerLanguage("xml", xml);
hljs.registerLanguage("ini", ini);
hljs.registerLanguage("glsl", glsl);

// file extension -> highlight.js language (empty = no highlighting, plain text)
function langOf(name: string): string {
  const e = (name.split(".").pop() || "").toLowerCase();
  if (/^(cpp|cc|cxx|c\+\+|hpp|hxx|h|hh|inl|c|ino)$/.test(e)) return "cpp";
  if (e === "cs") return "csharp";
  if (e === "py") return "python";
  if (/^(js|jsx|mjs|cjs)$/.test(e)) return "javascript";
  if (/^(ts|tsx)$/.test(e)) return "typescript";
  if (/^(json|uproject|uplugin)$/.test(e)) return "json";
  if (/^(xml|html|htm|svg|xaml)$/.test(e)) return "xml";
  if (/^(ini|cfg|conf|config|toml|editorconfig)$/.test(e)) return "ini";
  if (/^(usf|ush|hlsl|glsl|shader|frag|vert)$/.test(e)) return "glsl";
  return "";
}
const LANG_LABEL: Record<string, string> = { cpp: "C++", csharp: "C#", python: "Python", javascript: "JavaScript", typescript: "TypeScript", json: "JSON", xml: "XML", ini: "INI", glsl: "GLSL" };

function esc(s: string): string {
  return s.replace(/[&<>]/g, (c) => (c === "&" ? "&amp;" : c === "<" ? "&lt;" : "&gt;"));
}

const MAX = 800_000; // don't try to render/highlight enormous files

// Preview source files like GitHub: syntax-highlighted content with line numbers
// for new files, or a colored unified diff for edits. Working-copy code files can
// be edited in place (built-in editor) and saved before committing.
export default function CodeView({ file, onFlash }: { file: OpenedFile; onFlash?: (text: string, err?: boolean) => void }) {
  const dp = file.depotFile || "";
  const spec = file._spec || ""; // set → read this exact submitted revision
  const { name } = splitPath(dp);
  const action = (file.action || "").toLowerCase();
  const [diff, setDiff] = useState<string | null>(null);
  const [code, setCode] = useState<string | null>(null);
  const [err, setErr] = useState("");
  const [tooBig, setTooBig] = useState(false);
  const [editing, setEditing] = useState(false);
  const [draft, setDraft] = useState("");
  const [saving, setSaving] = useState(false);
  const [reload, setReload] = useState(0);

  useEffect(() => {
    let live = true;
    setDiff(null); setCode(null); setErr(""); setTooBig(false); setEditing(false);
    // working-copy edits show a diff against the depot; historical revisions just
    // show their full content (no meaningful working diff to fetch).
    if (!spec) p4.diff(dp).then((d) => live && setDiff(d)).catch(() => live && setDiff(""));
    const load = spec ? p4.printDepot(spec) : p4.readDepot(dp);
    load.then((buf) => {
      if (!live) return;
      if (buf.byteLength > MAX) { setTooBig(true); setCode(""); return; }
      const bytes = new Uint8Array(buf);
      // crude binary sniff: a NUL in the first chunk
      if (bytes.subarray(0, 8000).includes(0)) { setCode(null); setErr("binary"); return; }
      setCode(new TextDecoder("utf-8", { fatal: false }).decode(bytes));
    }).catch((e) => live && setErr(String(e)));
    return () => { live = false; };
  }, [dp, spec, reload]);

  // built-in editor: editable for a code file in the working copy (not history/binary/huge)
  const canEdit = !spec && !tooBig && err !== "binary" && code != null;
  function startEdit() { setDraft(code || ""); setEditing(true); }
  function cancelEdit() { setEditing(false); }
  async function save() {
    setSaving(true);
    try {
      await p4.writeDepot(dp, draft);
      setCode(draft);
      setEditing(false);
      onFlash?.(t("Saved {name}", { name }));
      setReload((r) => r + 1); // refresh diff/status after the on-disk change
    } catch (e) { onFlash?.(String(e), true); }
    finally { setSaving(false); }
  }
  const pencil = <svg viewBox="0 0 24 24" width="13" height="13" fill="none"><path d="m14.5 5.5 4 4M4 20l1-4L16.5 4.5a2 2 0 0 1 3 3L8 19l-4 1Z" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" /></svg>;

  // NB: all hooks must run before any early return (Rules of Hooks) — compute
  // the highlighted body here, above the editing/diff/error returns below.
  const lang = langOf(name);
  const highlighted = useMemo(() => {
    if (code == null) return "";
    try { return lang ? hljs.highlight(code, { language: lang, ignoreIllegals: true }).value : esc(code); }
    catch { return esc(code); }
  }, [code, lang]);
  // live-highlighted draft for the editor underlay (same highlighter as the preview)
  const draftHi = useMemo(() => {
    try { return lang ? hljs.highlight(draft, { language: lang, ignoreIllegals: true }).value : esc(draft); }
    catch { return esc(draft); }
  }, [draft, lang]);
  const preRef = useRef<HTMLPreElement>(null);
  const gutRef = useRef<HTMLDivElement>(null);
  const draftRows = draft.split("\n").length;

  // editing mode — a real code editor: transparent textarea over a syntax-
  // highlighted underlay, with a line-number gutter (looks like the preview).
  if (editing) {
    const syncScroll = (e: { currentTarget: HTMLTextAreaElement }) => {
      const ta = e.currentTarget;
      if (preRef.current) { preRef.current.scrollTop = ta.scrollTop; preRef.current.scrollLeft = ta.scrollLeft; }
      if (gutRef.current) gutRef.current.scrollTop = ta.scrollTop;
    };
    return (
      <div className="right-body">
        <div className="codebar">
          <span className="cb-badge editing">{t("Editing")}</span>
          <span className="cb-name">{name}</span>
          {lang ? <span className="cb-lines">{LANG_LABEL[lang]}</span> : null}
          <span className="ce-actions">
            <button className="ce-btn" onClick={cancelEdit} disabled={saving}>{t("Cancel")}</button>
            <button className="ce-btn save" onClick={save} disabled={saving || draft === code}>{saving ? <span className="ldr sm" /> : null}{t("Save")}</button>
          </span>
        </div>
        <div className="code editing">
          <div className="code-gutter" ref={gutRef}>{Array.from({ length: draftRows }, (_, i) => <span key={i}>{i + 1}</span>)}</div>
          <div className="ce-wrap">
            <pre className="ce-under" aria-hidden="true" ref={preRef}><code className="hljs" dangerouslySetInnerHTML={{ __html: draftHi + (draft.endsWith("\n") ? " " : "") }} /></pre>
            <textarea className="ce-ta" value={draft} spellCheck={false} autoFocus onScroll={syncScroll} onChange={(e) => setDraft(e.target.value)}
              onKeyDown={(e) => {
                if (e.key === "Escape") cancelEdit();
                if (e.key === "s" && (e.ctrlKey || e.metaKey)) { e.preventDefault(); if (draft !== code) save(); }
                // Tab inserts a tab instead of leaving the field
                if (e.key === "Tab") {
                  e.preventDefault();
                  const ta = e.currentTarget; const s = ta.selectionStart, en = ta.selectionEnd;
                  const nv = draft.slice(0, s) + "\t" + draft.slice(en);
                  setDraft(nv);
                  requestAnimationFrame(() => { ta.selectionStart = ta.selectionEnd = s + 1; });
                }
              }} />
          </div>
        </div>
      </div>
    );
  }

  if (err === "binary") return <div className="nofile">{t("Binary file — no code preview.")}</div>;
  if (err) return <div className="nofile" style={{ color: "var(--del)" }}>{err}</div>;
  if (tooBig) return <div className="nofile">{t("File is too large to preview.")}</div>;

  // an edited file with a real diff → show the diff
  if (diff && diff.trim() && action.includes("edit")) {
    const lines = diff.split("\n");
    return (
      <div className="right-body">
        <div className="codebar"><span className="cb-badge diff">{t("Diff")}</span><span className="cb-name">{name}</span>
          {canEdit && <button className="ce-btn edit" style={{ marginLeft: "auto" }} onClick={startEdit}>{pencil}{t("Edit")}</button>}
        </div>
        <div className="diff">
          {lines.map((ln, i) => {
            let cls = "";
            if (ln.startsWith("@@")) cls = "hunk";
            else if (ln.startsWith("+") && !ln.startsWith("+++")) cls = "add";
            else if (ln.startsWith("-") && !ln.startsWith("---")) cls = "del";
            return <div key={i} className={"dl " + cls}><span className="g" /><span className="c">{ln || " "}</span></div>;
          })}
        </div>
      </div>
    );
  }

  // otherwise (new file, or edit with no text diff) → full content with line numbers
  if (code == null) return <div className="nofile">{t("Loading…")}</div>;
  const total = code.length ? code.replace(/\n$/, "").split("\n").length : 0;
  return (
    <div className="right-body">
      <div className="codebar">
        <span className={"cb-badge" + (action.includes("add") ? " add" : "")}>{action.includes("add") ? t("New file") : lang ? (LANG_LABEL[lang] || "") : t("Text")}</span>
        <span className="cb-name">{name}</span>
        <span className="cb-lines">{t("{n} lines", { n: total })}</span>
        {canEdit && <button className="ce-btn edit" onClick={startEdit}>{pencil}{t("Edit")}</button>}
      </div>
      <div className={"code" + (action.includes("add") ? " added" : "")}>
        <div className="code-gutter">{Array.from({ length: total }, (_, i) => <span key={i}>{i + 1}</span>)}</div>
        <pre className="code-body"><code className="hljs" dangerouslySetInnerHTML={{ __html: highlighted }} /></pre>
      </div>
    </div>
  );
}
