// @ts-nocheck
import React, { useMemo, useRef, useState } from "react";

/**
 * EN↔ZH Syntax Reorderer — Native HTML5 Drag & Drop + Roles & Patterns (v2.2.3)
 *
 * The component helps students understand structural and syntactic differences
 * between English and Chinese by segmenting, labeling roles, and reordering.
 * (Imported from prototype; minor adjustments for integration only.)
 */

// ------- Utilities -------
const containsHan = (s = "") => /\p{Script=Han}/u.test(s);
const isPunc = (t = "") => /[.,!?;:·…—\-，。？！；：、《》〈〉()“”"'\[\]{}]/.test(t);

function hash6(str) {
  let h = 0;
  for (let i = 0; i < str.length; i++) h = (h * 31 + str.charCodeAt(i)) >>> 0;
  return (h >>> 0).toString(36).slice(0, 6);
}

function tokenize(text) {
  if (!text || !String(text).trim()) return [];
  const rough = String(text).replace(/\s+/g, " ").trim().split(" ").filter(Boolean);
  const tokens = [];
  const re = /(\p{Script=Han}+)|([A-Za-z0-9]+(?:[-'][A-Za-z0-9]+)*)|([^\p{Script=Han}A-Za-z0-9\s])/gu;
  for (const chunk of (rough.length ? rough : [String(text).trim()])) {
    let m;
    while ((m = re.exec(chunk))) {
      const [tok] = m;
      if (tok.trim() !== "") tokens.push(tok);
    }
  }
  return tokens;
}

// Expand any Han token into individual characters (keeps punctuation intact)
function explodeHanChars(tokens = []) {
  const out = [];
  for (const t of tokens) {
    if (containsHan(t) && !isPunc(t) && t.length > 1) {
      for (const ch of t) out.push(ch);
    } else {
      out.push(t);
    }
  }
  return out;
}

const boundaryCount = (tokens) => Math.max(0, (tokens?.length || 0) - 1);

function groupsFrom(tokens = [], boundaries = []) {
  if (!Array.isArray(tokens) || tokens.length === 0) return [];
  const groups = [];
  let cur = [];
  tokens.forEach((t, i) => {
    cur.push(t);
    const atEnd = i === tokens.length - 1;
    if (atEnd || !!boundaries[i]) {
      groups.push(cur.join(" "));
      cur = [];
    }
  });
  return groups;
}

function boundariesFromPunctuation(tokens = []) {
  const count = boundaryCount(tokens);
  const out = new Array(count).fill(false);
  for (let i = 0; i < count; i++) out[i] = isPunc(tokens[i]);
  return out;
}

function splitLiteralChunks(literalText, groups) {
  if (!literalText || !literalText.trim()) return new Array(Math.max(0, groups.length)).fill("");
  let chunks;
  if (literalText.includes("|")) {
    chunks = literalText.split("|").map((s) => s.trim()).filter((s) => s !== "");
  } else if (literalText.includes("\n")) {
    chunks = literalText.split(/\n+/).map((s) => s.trim()).filter((s) => s !== "");
  } else {
    const words = literalText.trim().split(/\s+/).filter(Boolean);
    const sizes = groups.map((g) => g.split(/\s+/).filter(Boolean).length || 1);
    const total = sizes.reduce((a, b) => a + b, 0) || Math.max(1, groups.length);
    chunks = [];
    let cursor = 0;
    for (let i = 0; i < groups.length; i++) {
      const share = Math.round((sizes[i] / total) * words.length);
      const end = i === groups.length - 1 ? words.length : Math.min(words.length, cursor + Math.max(1, share));
      chunks.push(words.slice(cursor, end).join(" "));
      cursor = end;
    }
    chunks = chunks.map((s) => (s && s.trim()) || "⋯");
  }
  return chunks;
}

// Small helper to move item in an array
function moveItem(arr, from, to) {
  const a = arr.slice();
  const [item] = a.splice(from, 1);
  a.splice(to, 0, item);
  return a;
}

// ---------- Roles & Colours ----------
const ROLES = [
  "TIME", "PLACE", "TOPIC", "SUBJECT", "AUX", "VERB", "OBJECT", "MANNER", "DEGREE", "CAUSE", "RESULT", "REL", "EXIST", "BA", "BEI", "AGENT", "OTHER"
];
const ROLE_LABEL = {
  TIME: "Time", PLACE: "Place", TOPIC: "Topic/Stance", SUBJECT: "Subject", AUX: "Auxiliary",
  VERB: "Verb", OBJECT: "Object", MANNER: "Manner", DEGREE: "Degree",
  CAUSE: "Cause", RESULT: "Result", REL: "的‑clause", EXIST: "Existential(有)", BA: "把", BEI: "被", AGENT: "Agent", OTHER: "Other"
};
const ROLE_COLOUR = {
  TIME: "bg-blue-50 border-blue-200 text-blue-700",
  PLACE: "bg-teal-50 border-teal-200 text-teal-700",
  TOPIC: "bg-amber-50 border-amber-200 text-amber-800",
  SUBJECT: "bg-sky-50 border-sky-200 text-sky-800",
  VERB: "bg-indigo-50 border-indigo-200 text-indigo-700",
  OBJECT: "bg-rose-50 border-rose-200 text-rose-700",
  MANNER: "bg-violet-50 border-violet-200 text-violet-700",
  DEGREE: "bg-fuchsia-50 border-fuchsia-200 text-fuchsia-700",
  CAUSE: "bg-orange-50 border-orange-200 text-orange-700",
  RESULT: "bg-lime-50 border-lime-200 text-lime-700",
  REL: "bg-emerald-50 border-emerald-200 text-emerald-700",
  EXIST: "bg-cyan-50 border-cyan-200 text-cyan-700",
  BA: "bg-yellow-50 border-yellow-200 text-yellow-700",
  BEI: "bg-stone-50 border-stone-200 text-stone-700",
  AGENT: "bg-stone-50 border-stone-200 text-stone-800",
  OTHER: "bg-zinc-50 border-zinc-200 text-zinc-700",
};

// Prevent drag initiation from form controls inside draggable cards
const NO_DRAG = {
  draggable: false,
  onDragStart: (e) => e.preventDefault(),
  onMouseDown: (e) => e.stopPropagation(),
};

function RolePill({ role }) {
  const cls = ROLE_COLOUR[role] || ROLE_COLOUR.OTHER;
  return <span className={`inline-flex items-center px-2 py-0.5 rounded-lg border text-[11px] ${cls}`}>{ROLE_LABEL[role] || role}</span>;
}

function RoleSelector({ value, onChange }) {
  return (
    <select {...NO_DRAG} className="rounded-lg border border-zinc-300 px-2 py-1 text-xs" value={value} onChange={(e) => onChange(e.target.value)}>
      {ROLES.map((r) => (
        <option key={r} value={r}>{ROLE_LABEL[r]}</option>
      ))}
    </select>
  );
}

function SortableCard({ id, idx, src, lit, role, onEdit, onDragStart, onDragEnter, onDragOver, onDrop, onDragEnd, onKeyMove, isDragOver, highlightRoles }) {
  const isHL = highlightRoles?.includes(role);
  return (
    <div
      data-id={id}
      draggable
      onDragStart={(e) => onDragStart(e, id)
      }
      onDragEnter={(e) => onDragEnter(e, id)}
      onDragOver={(e) => onDragOver(e, id)}
      onDrop={(e) => onDrop(e, id)}
      onDragEnd={onDragEnd}
      className={`bg-white rounded-2xl shadow-sm border p-4 mb-3 hover:shadow-md transition outline-none ${isDragOver ? "ring-2 ring-indigo-400" : "border-zinc-200"} ${isHL ? "ring-2 ring-amber-400" : ""} cursor-grab active:cursor-grabbing`}
      tabIndex={0}
      onKeyDown={(e) => {
        if (e.key === "ArrowUp") { e.preventDefault(); onKeyMove(id, -1); }
        if (e.key === "ArrowDown") { e.preventDefault(); onKeyMove(id, +1); }
      }}
    >
      <div className="flex items-center justify-between mb-2">
        <div className="flex items-center gap-2">
          <div className="text-xs uppercase tracking-wider text-zinc-500">Chunk {idx + 1}</div>
          <RolePill role={role} />
        </div>
        <div className="flex items-center gap-2">
          <RoleSelector value={role} onChange={(r) => onEdit(id, { role: r })} />
          <div className="flex gap-1">
            <button className="text-[11px] px-2 py-1 rounded bg-zinc-100 hover:bg-zinc-200" onClick={() => onKeyMove(id, -1)} aria-label="Move up">▲</button>
            <button className="text-[11px] px-2 py-1 rounded bg-zinc-100 hover:bg-zinc-200" onClick={() => onKeyMove(id, +1)} aria-label="Move down">▼</button>
          </div>
        </div>
      </div>

      {/* Grab bar / affordance */}
      <div className="w-full mb-3 rounded-xl border border-indigo-200 bg-indigo-50 text-indigo-700 text-xs px-3 py-2 select-none">
        ⠿ Drag anywhere on the card background • or use ▲▼ keys
      </div>

      <div className="grid md:grid-cols-2 gap-3">
        <label className="text-sm text-zinc-600">
          <span className="block text-xs text-zinc-500 mb-1">Source phrase</span>
          <input
            {...NO_DRAG}
            className="w-full rounded-xl border border-zinc-300 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500"
            value={src}
            onChange={(e) => onEdit(id, { src: e.target.value })}
          />
        </label>
        <label className="text-sm text-zinc-600">
          <span className="block text-xs text-zinc-500 mb-1">Literal translation</span>
          <input
            {...NO_DRAG}
            className="w-full rounded-xl border border-zinc-300 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500"
            value={lit}
            onChange={(e) => onEdit(id, { lit: e.target.value })}
          />
        </label>
      </div>
    </div>
  );
}

// ------------------------
// Self-tests (expanded)
// ------------------------
function runTokenizerTests() {
  const cases = [
    { name: "empty string", input: "", expect: [] },
    { name: "spaces only", input: "   \t\n  ", expect: [] },
    { name: "simple EN", input: "I like apples.", expect: ["I", "like", "apples", "."] },
    { name: "simple ZH", input: "我喜欢中文。", expect: ["我", "喜欢", "中文", "。"] },
    { name: "ZH+quotes", input: "“你好”，世界！", expect: ["“", "你", "好", "”", "，", "世", "界", "！"] },
    { name: "EN hyphen+apostrophe", input: "state-of-the-art don't fail.", expect: ["state-of-the-art", "don't", "fail", "."] },
    { name: "Emoji and symbols", input: "Hi🙂!", expect: ["Hi", "🙂", "!"] },
    { name: "ZH with spaces", input: "我 爱 你。", expect: ["我", "爱", "你", "。"] },
  ];
  return cases.map((c) => {
    const got = tokenize(c.input);
    const pass = Array.isArray(got) && got.join("|") === c.expect.join("|");
    return { name: c.name, pass, got, expect: c.expect };
  });
}

function runBoundaryTests() {
  const toks = tokenize("我 喜欢 中文 .");
  const expectedCount = Math.max(0, toks.length - 1);
  const b1 = boundariesFromPunctuation(toks);
  const safeNewTrue = new Array(Math.max(0, toks.length - 1)).fill(true);
  const safeNewFalse = new Array(Math.max(0, toks.length - 1)).fill(false);
  return [
    { name: "boundary count safe", pass: b1.length === expectedCount, got: b1.length, expect: expectedCount },
    { name: "new true safe", pass: safeNewTrue.length === expectedCount },
    { name: "new false safe", pass: safeNewFalse.length === expectedCount },
  ];
}

function runLiteralSplitTests() {
  const groups = ["It is", "difficult", "for me", "to learn Chinese"];
  const t1 = splitLiteralChunks("很难 | 对我来说 | 学习中文", groups);
  const t2 = splitLiteralChunks("很难\n对我来说\n学习中文", groups);
  const t3 = splitLiteralChunks("很 难 对 我 来 说 学 习 中 文", groups);
  return [
    { name: "literal with pipes", pass: Array.isArray(t1) && t1.length === groups.length },
    { name: "literal with newlines", pass: Array.isArray(t2) && t2.length === groups.length },
    { name: "literal with words", pass: Array.isArray(t3) && t3.length === groups.length },
  ];
}

function runCharExplodeTests() {
  const base = ["我喜欢中文", "。"];
  const exploded = explodeHanChars(base);
  const pass = exploded.join("") === "我喜欢中文。" && exploded.length === 6; // 我 喜 欢 中 文 。
  return [{ name: "explode Han into characters", pass, got: exploded }];
}

function runNormalizeBoundaryTests() {
  const toks = ["我","喜","欢","中","文","。"];
  const b = [true, false, true];
  const n = Math.max(0, toks.length - 1);
  const out = new Array(n).fill(false);
  for (let i = 0; i < Math.min(n, b.length); i++) out[i] = !!b[i];
  return [{ name: "normalize boundaries len", pass: out.length === toks.length - 1 }];
}

function SelfTests() {
  const tokResults = runTokenizerTests();
  const bResults = runBoundaryTests();
  const lResults = runLiteralSplitTests();
  const xResults = runCharExplodeTests();
  const nbResults = runNormalizeBoundaryTests();
  const all = [...tokResults, ...bResults, ...lResults, ...xResults, ...nbResults];
  const passed = all.filter((r) => r.pass).length;
  const total = all.length;
  return (
    <details className="mt-6">
      <summary className="cursor-pointer text-sm text-zinc-600">Self-tests: {passed}/{total} passed (click to view)</summary>
      <div className="mt-3 text-xs grid gap-2">
        {all.map((r, i) => (
          <div key={i} className={`p-2 rounded-lg border ${r.pass ? "border-emerald-300 bg-emerald-50" : "border-rose-300 bg-rose-50"}`}>
            <div className="font-medium">{r.name} — {r.pass ? "PASS" : "FAIL"}</div>
            {r.expect !== undefined && (
              <div className="mt-1">
                <div><span className="text-zinc-500">expect:</span> {JSON.stringify(r.expect)}</div>
                <div><span className="text-zinc-500">got:</span> {JSON.stringify(r.got)}</div>
              </div>
            )}
          </div>
        ))}
      </div>
    </details>
  );
}

// ---------- Pattern cards data with examples ----------
const PATTERNS = [
  {
    id: "it_adj",
    title: "It is ADJ for SB to VP → 对SB来说 + VP + 很ADJ",
    roles: ["TOPIC", "SUBJECT", "VERB", "OBJECT", "DEGREE"],
    hint: "Front stance/topic, then VP, then degree+adj (simplified as DEGREE).",
    exEn: "It is difficult for me to learn Chinese.",
    exZh: "对我来说，学中文很难。",
  },
  {
    id: "rel_clause",
    title: "Relative clause → [REL] 的 + N / EN: N + (that/which …)",
    roles: ["REL", "OBJECT"],
    hint: "ZH: 把后置从句前置为 的‑短语；EN: RC follows the noun.",
    exEn: "the book that I bought yesterday is expensive",
    exZh: "我昨天买的书很贵。",
  },
  {
    id: "existential",
    title: "Existential/there is → (PLACE) + 有 + NP / EN: there is/are + NP (PP)",
    roles: ["PLACE", "EXIST", "OBJECT"],
    hint: "ZH: 地点可前置，EXIST 代表 ‘有’；EN: ‘there is/are’ + NP (+ place/time).",
    exEn: "There is a cat in the room.",
    exZh: "房间里有一只猫。",
  },
  {
    id: "ba",
    title: "Disposal 把 → 把 + OBJ + V + (RESULT)",
    roles: ["BA", "OBJECT", "VERB", "RESULT"],
    hint: "突出对宾语的处置；结果补语可选。",
    exEn: "I put the keys on the table.",
    exZh: "我把钥匙放在桌子上。",
  },
  {
    id: "bei",
    title: "Passive 被 → OBJ + 被 + AGENT + V (+RESULT) / EN: AGENT + V + OBJ (or passive)",
    roles: ["OBJECT", "BEI", "AGENT", "VERB", "RESULT"],
    hint: "ZH: AGENT 明示时放在 被 之后；EN: passive optional; active often preferred.",
    exEn: "The door was opened by the wind.",
    exZh: "门被风吹开了。",
  },
];

const SIMPLE_TITLES = {
  it_adj: "Topic → VP → 很+ADJ",
  rel_clause: "的‑relative before noun",
  existential: "Place + 有 + NP",
  ba: "把 disposal pattern",
  bei: "被 passive pattern",
} as Record<string, string>;

function ReasoningHints({ direction, cards }) {
  if (!cards || cards.length === 0) return null;
  const roles = cards.map((c) => c.role);
  const hints = [];
  const has = (r) => roles.includes(r);
  if (direction.startsWith("EN")) {
    if (has("TIME") || has("PLACE") || has("TOPIC")) hints.push("Fronted TIME/PLACE/TOPIC reflects Chinese topic/comment tendency.");
    if (has("REL") && has("OBJECT")) hints.push("Prenominal 的‑relative clause before the noun (ZH).");
    if (has("BA")) hints.push("把‑construction focuses disposal/result on the object.");
    if (has("BEI")) hints.push("被‑passive highlights the patient/topic; agent optional.");
    if (has("EXIST")) hints.push("Existential 有 introduces new entities: (Place)+有+NP.");
  } else {
    if (has("REL") && has("OBJECT")) hints.push("English relative clauses follow the noun: N + (that/which …).");
    if (has("EXIST")) hints.push("Translate 存现句 with 'there is/are' when introducing new entities.");
    if (has("TIME") || has("PLACE")) hints.push("English often places time/place adverbials after the verb phrase or at sentence end.");
  }
  if (hints.length === 0) return null;
  return (
    <ul className="list-disc ml-5">{hints.map((h, i) => <li key={i}>{h}</li>)}</ul>
  );
}

export default function SyntaxReorderer() {
  const [direction, setDirection] = useState("EN → ZH");
  const [sourceText, setSourceText] = useState("");
  const [literalText, setLiteralText] = useState("");

  const prevLiteralRef = useRef("");
  const fileRef = useRef(null);

  // Character-separator mode (ZH)
  const [zhCharMode, setZhCharMode] = useState(false);

  const baseTokens = useMemo(() => tokenize(sourceText), [sourceText]);
  const hasHanInSource = useMemo(() => /\p{Script=Han}/u.test(sourceText), [sourceText]);
  const srcTokens = useMemo(() => (zhCharMode ? explodeHanChars(baseTokens) : baseTokens), [baseTokens, zhCharMode]);

  const [boundaries, setBoundaries] = useState([]);

  React.useEffect(() => {
    setBoundaries((prev) => {
      const count = boundaryCount(srcTokens);
      const next = new Array(count).fill(false);
      for (let i = 0; i < Math.min(prev.length, next.length); i++) next[i] = prev[i];
      return next;
    });
  }, [srcTokens.length]);

  const groups = useMemo(() => groupsFrom(srcTokens, boundaries), [srcTokens, boundaries]);
  const groupsKey = useMemo(() => groups.join("|"), [groups]);

  const [cards, setCards] = useState([]); // {id, src, lit, role}
  const [order, setOrder] = useState([]);
  const [joiner, setJoiner] = useState("auto");

  const [autoBuild, setAutoBuild] = useState(true);
  const [hasDragged, setHasDragged] = useState(false);

  const [dragId, setDragId] = useState(null);
  const [dragOverId, setDragOverId] = useState(null);
  const [isDragging, setIsDragging] = useState(false);

  // Pattern highlight state
  const [highlightRoles, setHighlightRoles] = useState([]);

  function rebuild({ preserveByIndex }) {
    const litChunks = splitLiteralChunks(literalText, groups);
    const built = groups.map((g, i) => ({ id: `${i}-${hash6(g)}`, src: g, lit: litChunks[i] ?? "", role: cards[i]?.role || "OTHER" }));

    if (preserveByIndex && cards.length === built.length) {
      for (let i = 0; i < built.length; i++) {
        if (cards[i]) {
          if (typeof cards[i].lit === "string" && cards[i].lit.trim() !== "") built[i].lit = cards[i].lit;
          if (cards[i].role) built[i].role = cards[i].role;
        }
      }
    }

    setCards(built);
    setOrder((prev) => {
      if (prev.length === built.length) {
        const setIds = new Set(built.map((b) => b.id));
        const prevFiltered = prev.filter((id) => setIds.has(id));
        if (prevFiltered.length === built.length) return prevFiltered;
      }
      return built.map((c) => c.id);
    });

    if (joiner === "auto") {
      const anyHan = built.some((c) => containsHan(c.lit));
      setJoiner(anyHan ? "none" : "space");
    }
  }

  React.useEffect(() => {
    if (!autoBuild || hasDragged) return;
    const prevLit = prevLiteralRef.current;
    const litChanged = prevLit !== literalText;
    rebuild({ preserveByIndex: !litChanged });
    prevLiteralRef.current = literalText;
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [groupsKey, literalText, autoBuild, hasDragged]);

  // Helpers for drag guard
  function startedOnInteractiveTarget(e) {
    const target = e.target as HTMLElement | null;
    const tag = (target && target.tagName) || '';
    if (["INPUT", "TEXTAREA", "SELECT", "BUTTON"].includes(tag)) return true;
    if (target && (target as any).isContentEditable) return true;
    return false;
  }

  // HTML5 DnD handlers
  function handleCardDragStart(e, id) {
    if (startedOnInteractiveTarget(e)) { e.preventDefault(); return; }
    setDragId(id);
    setIsDragging(true);
    e.dataTransfer.effectAllowed = "move";
    try { e.dataTransfer.setData("text/plain", String(id)); } catch {}
  }
  function handleCardDragEnter(e, overId) { if (!isDragging) return; e.preventDefault(); setDragOverId(overId); }
  function handleCardDragOver(e, overId) { if (!isDragging) return; e.preventDefault(); setDragOverId(overId); }
  function handleCardDrop(e, overId) {
    if (!isDragging) return;
    e.preventDefault();
    const fromId = dragId || e.dataTransfer.getData("text/plain");
    if (!fromId || !overId || fromId === overId) { setDragId(null); setDragOverId(null); setIsDragging(false); return; }
    setOrder((items) => {
      const from = items.indexOf(fromId);
      const to = items.indexOf(overId);
      if (from === -1 || to === -1) return items;
      return moveItem(items, from, to);
    });
    setHasDragged(true); setDragId(null); setDragOverId(null); setIsDragging(false);
  }
  function handleCardDragEnd() {
    setIsDragging(false);
    setDragId(null);
    setDragOverId(null);
  }

  function handleKeyMove(id, delta) {
    setOrder((items) => {
      const i = items.indexOf(id);
      if (i === -1) return items;
      let j = i + delta; if (j < 0) j = 0; if (j >= items.length) j = items.length - 1; if (i === j) return items;
      return moveItem(items, i, j);
    });
    setHasDragged(true);
  }

  function onEditCard(id, patch) { setCards((prev) => prev.map((c) => (c.id === id ? { ...c, ...patch } : c))); }

  // Compose outputs
  const orderedCards = useMemo(() => order.map((id) => cards.find((c) => c.id === id)).filter(Boolean), [order, cards]);
  const composedTarget = useMemo(() => {
    const raw = orderedCards.map((c) => c.lit).filter((s) => s != null);
    if ((joiner === "none") || (joiner === "auto" && raw.some(containsHan))) return raw.join("");
    return raw.join(" ");
  }, [orderedCards, joiner]);

  // Clear helpers
  const clearSource = () => { setSourceText(""); setBoundaries([]); setHasDragged(false); };
  const clearLiteral = () => { setLiteralText(""); setHasDragged(false); };
  const clearCards = () => { setCards([]); setOrder([]); setHasDragged(false); };

  function exportJSON() {
    const payload = { direction, sourceText, literalText, tokens: srcTokens, boundaries, groups, cards, order, joiner, composedTarget, timestamp: new Date().toISOString() };
    const json = JSON.stringify(payload, null, 2);
    try {
      const blob = new Blob([json], { type: "application/json;charset=utf-8" });
      const url = URL.createObjectURL(blob);
      const a = document.createElement("a");
      a.href = url;
      a.download = "syntax-reorderer-activity.json";
      a.rel = "noopener";
      document.body.appendChild(a);
      a.click();
      setTimeout(() => { try { document.body.removeChild(a); } catch {} URL.revokeObjectURL(url); }, 0);
    } catch (err) {
      // Fallback: copy to clipboard
      try {
        if (navigator.clipboard && navigator.clipboard.writeText) {
          navigator.clipboard.writeText(json);
          alert("Exported by copying JSON to clipboard.");
        } else {
          alert("Export failed: " + (err?.message || String(err)));
        }
      } catch (e2) {
        alert("Export failed: " + (err?.message || String(err)));
      }
    }
  }

  function normalizeBoundaries(b, tokens) {
    const n = Math.max(0, (tokens?.length || 0) - 1);
    const out = new Array(n).fill(false);
    if (Array.isArray(b)) {
      for (let i = 0; i < Math.min(n, b.length); i++) out[i] = !!b[i];
    }
    return out;
  }

  function importJSON(file) {
    const reader = new FileReader();
    reader.onload = (e) => {
      try {
        const text = String(e.target?.result || "");
        const data = JSON.parse(text);
        const src = typeof data.sourceText === "string" ? data.sourceText : "";
        const base = tokenize(src);
        const chars = explodeHanChars(base);
        const wantCharMode = (/\p{Script=Han}/u.test(src)) && Array.isArray(data.tokens) && data.tokens.length === chars.length;

        setDirection(typeof data.direction === "string" ? data.direction : direction);
        setZhCharMode(wantCharMode);
        setSourceText(src);
        const toks = wantCharMode ? chars : base;
        setBoundaries(normalizeBoundaries(data.boundaries, toks));
        setLiteralText(typeof data.literalText === "string" ? data.literalText : "");
        setCards(Array.isArray(data.cards) ? data.cards : []);
        setOrder(Array.isArray(data.order) ? data.order : []);
        setJoiner(data.joiner === "space" || data.joiner === "none" ? data.joiner : "auto");
        prevLiteralRef.current = typeof data.literalText === "string" ? data.literalText : "";
        setHasDragged(false);
      } catch (err) {
        alert("Invalid JSON");
      }
    };
    reader.readAsText(file);
  }

  function loadDemo() {
    if (direction.startsWith("EN")) {
      // EN → ZH demo
      const demoSrc = "It is difficult for me to learn Chinese.";
      const demoLit = "很难 | 对我来说 | 学习中文";
      setDirection("EN → ZH");
      setSourceText(demoSrc);
      setBoundaries(boundariesFromPunctuation(tokenize(demoSrc)));
      setLiteralText(demoLit);
      setHasDragged(false);
      prevLiteralRef.current = demoLit;
      setZhCharMode(false);
    } else {
      // ZH → EN demo
      const demoSrc = "对我来说，学中文很难。";
      const demoLit = "for me | learn Chinese | very difficult";
      setDirection("ZH → EN");
      setSourceText(demoSrc);
      // Default to char separators for Chinese demo
      const base = tokenize(demoSrc);
      const chars = explodeHanChars(base);
      setBoundaries(boundariesFromPunctuation(chars));
      setZhCharMode(true);
      setLiteralText(demoLit);
      setHasDragged(false);
      prevLiteralRef.current = demoLit;
    }
  }

  const literalChunks = useMemo(() => splitLiteralChunks(literalText, groups), [literalText, groups]);

  // Pattern helpers
  function applyPatternOrder(roleSequence) {
    setOrder((prev) => {
      const seqIndex = new Map(roleSequence.map((r, i) => [r, i]));
      const withIdx = prev.map((id, i) => {
        const card = cards.find((c) => c.id === id);
        const rank = seqIndex.has(card?.role) ? seqIndex.get(card.role) : 999;
        return { id, i, rank };
      });
      withIdx.sort((a, b) => (a.rank - b.rank) || (a.i - b.i));
      return withIdx.map((x) => x.id);
    });
  }

  function toggleHighlightRoles(roleSequence) {
    setHighlightRoles((prev) => {
      const same = prev.length === roleSequence.length && prev.every((r, i) => r === roleSequence[i]);
      return same ? [] : roleSequence.slice();
    });
  }

  // ------- Render -------
  return (
    <div className="min-h-screen bg-zinc-50 text-zinc-900 p-6 md:p-10">
      <div className="max-w-6xl mx-auto">
        <header className="mb-6">
          <h1 className="text-2xl md:text-3xl font-semibold tracking-tight">EN↔ZH Syntax Reorderer</h1>
          <p className="text-zinc-600 mt-1">Label chunks, apply patterns, and drag to restructure. Inputs won’t start drags; use the card background.</p>
        </header>

        {/* Controls */}
        <div className="bg-white border border-zinc-200 rounded-2xl p-4 md:p-6 shadow-sm mb-6">
          <div className="flex flex-wrap gap-3 items-center">
            <label className="text-sm" title="Target language direction; affects the recipe strip only.">Direction
              <select {...NO_DRAG} className="ml-2 rounded-xl border border-zinc-300 px-3 py-2 text-sm" value={direction} onChange={(e) => setDirection(e.target.value)}>
                <option>EN → ZH</option>
                <option>ZH → EN</option>
              </select>
            </label>

            <label className="text-sm" title="How to join the final literal chunks.">Joiner
              <select {...NO_DRAG} className="ml-2 rounded-xl border border-zinc-300 px-3 py-2 text-sm" value={joiner} onChange={(e) => setJoiner(e.target.value)}>
                <option value="auto">auto</option>
                <option value="space">space</option>
                <option value="none">none</option>
              </select>
            </label>

            <div className="ml-auto flex items-center gap-2" title="Auto‑build: when you change separators or the literal list, cards rebuild themselves. It pauses after you drag so your custom order isn’t overwritten.">
              <label className="text-sm flex items-center gap-2">
                <input type="checkbox" className="scale-110" checked={autoBuild} onChange={(e) => { setAutoBuild(e.target.checked); if (e.target.checked) setHasDragged(false); }} />
                Auto‑build
              </label>
              {hasDragged && autoBuild && (
                <button className="px-2 py-1 text-xs rounded-lg bg-indigo-50 hover:bg-indigo-100" onClick={() => setHasDragged(false)} title="Resume auto‑build after a manual drag">Resume</button>
              )}
            </div>

            <button className="px-3 py-2 text-sm rounded-xl bg-zinc-100 hover:bg-zinc-200" title="Force a fresh rebuild from your separators and literal list. Use this if Auto‑build is off." onClick={() => { setHasDragged(false); rebuild({ preserveByIndex: false }); }}>Rebuild now</button>
            <button className="px-3 py-2 text-sm rounded-xl bg-rose-100 hover:bg-rose-200" onClick={() => { setSourceText(""); setLiteralText(""); setBoundaries([]); setCards([]); setOrder([]); setHasDragged(false); setHighlightRoles([]); }}>Clear ALL</button>
            <button className="px-3 py-2 text-sm rounded-xl bg-zinc-100 hover:bg-zinc-200" onClick={loadDemo}>Load demo</button>
            <button className="px-3 py-2 text-sm rounded-xl bg-zinc-100 hover:bg-zinc-200" onClick={exportJSON}>Export JSON</button>
            <button className="px-3 py-2 text-sm rounded-xl bg-zinc-100 hover:bg-zinc-200" onClick={() => fileRef.current?.click()}>Import JSON</button>
            <input ref={fileRef} type="file" accept="application/json" className="hidden" onChange={(e) => e.target.files?.[0] && importJSON(e.target.files[0])} />
          </div>

          {/* Recipe strip — switches with direction */}
          {direction.startsWith("EN") ? (
            <div className="mt-4">
              <div className="text-xs text-zinc-500 mb-1">Chinese “light recipe” (typical order):</div>
              <div className="flex flex-wrap gap-2">
                {["TIME","PLACE","TOPIC","SUBJECT","MANNER","DEGREE","VERB","OBJECT","RESULT"].map((r) => (
                  <span key={r} className={`px-2 py-1 rounded-lg border text-xs ${ROLE_COLOUR[r]}`}>{ROLE_LABEL[r]}</span>
                ))}
              </div>
            </div>
          ) : (
            <div className="mt-4">
              <div className="text-xs text-zinc-500 mb-1">English “light recipe” (typical clause order):</div>
              <div className="flex flex-wrap gap-2">
                {["TIME","SUBJECT","AUX","DEGREE","MANNER","VERB","OBJECT","PLACE","RESULT"].map((r) => (
                  <span key={r} className={`px-2 py-1 rounded-lg border text-xs ${ROLE_COLOUR[r] || ROLE_COLOUR.OTHER}`}>{r === "AUX" ? "Auxiliary" : (ROLE_LABEL[r] || r)}</span>
                ))}
              </div>
              <div className="text-[11px] text-zinc-500 mt-1">Notes: English RCs are post‑nominal ("the book [that I bought]"); many adverbials follow the verb phrase; existential uses “there is/are”.</div>
            </div>
          )}
        </div>

        {/* What are roles? */}
        <details className="mb-6 bg-white border border-zinc-200 rounded-2xl p-4 md:p-6 shadow-sm">
          <summary className="cursor-pointer font-medium">What are “roles” and what do these buttons mean?</summary>
          <div className="mt-3 text-sm text-zinc-700 grid gap-2">
            <div><strong>Roles</strong> are the <em>function</em> of a chunk (Time, Place, Topic/Stance, Subject, Verb, Object, etc.). Assigning roles lets patterns hint or reorder cards intelligently.</div>
            <ul className="list-disc ml-5">
              <li><strong>Auto‑build</strong>: when you change separators or the literal list, the cards below update automatically. After you drag manually, it pauses to preserve your custom order.</li>
              <li><strong>Rebuild now</strong>: forces an immediate rebuild (useful if Auto‑build is off or paused).</li>
              <li><strong>Apply order</strong> (on a pattern): reorders cards so roles that belong to the pattern come first, in the pattern’s typical order. It’s disabled until at least one card has one of those roles.</li>
            </ul>
          </div>
        </details>

        {/* Pattern cards (collapsed by default) */}
        <details className="bg-white border border-zinc-200 rounded-2xl p-4 md:p-6 shadow-sm mb-6">
          <summary className="cursor-pointer font-medium">Pattern cards</summary>
          <div className="mt-3 grid md:grid-cols-2 gap-3">
            {PATTERNS.map((p) => {
              const matchCount = cards.filter((c) => p.roles.includes(c.role)).length;
              return (
                <div key={p.id} className="p-3 rounded-xl border border-zinc-200 bg-zinc-50">
                  <div className="text-sm font-medium flex items-start justify-between gap-2">
                    <div className="flex-1 min-w-0 pr-2">
                      <div className="whitespace-normal break-words hyphens-auto">{p.title}</div>
                      <div className="text-[11px] text-zinc-500 mt-0.5">{SIMPLE_TITLES[p.id] || ''}</div>
                    </div>
                    <span className="shrink-0 text-[11px] px-2 py-0.5 rounded-full bg-white border border-zinc-200">matches: {matchCount}</span>
                  </div>
                  <div className="text-xs text-zinc-600 mt-1">{p.hint}</div>
                  <div className="text-xs text-zinc-600 mt-1"><span className="font-medium">Example:</span> {p.exEn} → <span className="text-zinc-800">{p.exZh}</span></div>
                  <div className="flex flex-wrap gap-2 mt-2">
                    {p.roles.map((r) => (
                      <span key={r} className={`px-2 py-0.5 rounded-lg border text-[11px] ${ROLE_COLOUR[r]}`}>{ROLE_LABEL[r]}</span>
                    ))}
                  </div>
                  <div className="mt-2 flex gap-2">
                    <button className="text-xs px-2 py-1 rounded-md bg-zinc-100 hover:bg-zinc-200" onClick={() => toggleHighlightRoles(p.roles)}>Highlight roles</button>
                    <button disabled={matchCount === 0} className={`text-xs px-2 py-1 rounded-md ${matchCount === 0 ? "bg-zinc-100 text-zinc-400 border border-zinc-200" : "bg-indigo-100 hover:bg-indigo-200"}`} title={matchCount === 0 ? "Set roles on some cards first" : "Reorder cards to reflect this pattern"} onClick={() => applyPatternOrder(p.roles)}>Apply order</button>
                  </div>
                </div>
              );
            })}
          </div>
          {highlightRoles.length > 0 && (
            <div className="text-xs text-zinc-500 mt-2">Highlighting: {highlightRoles.map((r) => ROLE_LABEL[r]).join(" → ")}</div>
          )}
        </details>

        {/* Sentence + literal inputs */}
        <div className="grid md:grid-cols-2 gap-6 mb-6">
          <div className="bg-white border border-zinc-200 rounded-2xl p-4 md:p-6 shadow-sm">
            <div className="flex items-center justify-between mb-2">
              <h2 className="font-medium">1) Source sentence</h2>
              <button className="text-xs px-2 py-1 rounded-md bg-rose-50 hover:bg-rose-100" onClick={clearSource}>Clear source</button>
            </div>
            <textarea
              {...NO_DRAG}
              className="w-full min-h-[110px] rounded-xl border border-zinc-300 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500"
              placeholder={direction.startsWith("EN") ? "Paste English here…" : "粘贴中文句子…"}
              value={sourceText}
              onChange={(e) => setSourceText(e.target.value)}
            />

            <div className="mt-2 flex items-center gap-3">
              {hasHanInSource && (
                <label className="text-xs flex items-center gap-2" title="Split Chinese text by character so you can place a separator between every Han character.">
                  <input type="checkbox" className="scale-110" checked={zhCharMode} onChange={(e) => {
                    const checked = e.target.checked; setZhCharMode(checked);
                    const base = tokenize(sourceText);
                    const toks = checked ? explodeHanChars(base) : base;
                    setBoundaries(new Array(Math.max(0, toks.length - 1)).fill(false));
                    setHasDragged(false);
                  }} />
                  Character separators (ZH)
                </label>
              )}
            </div>

            <div className="mt-3">
              <div className="text-xs text-zinc-500 mb-2">Click separators to toggle phrase boundaries. Cards update below.</div>
              <div className="bg-zinc-50 border border-zinc-200 rounded-xl p-3 overflow-x-auto">
                {srcTokens.length === 0 ? (
                  <div className="text-zinc-400 text-sm">Tokens will appear here…</div>
                ) : (
                  <div className="flex flex-wrap items-center gap-1">
                    {srcTokens.map((t, i) => (
                      <React.Fragment key={i}>
                        <span className={`px-2 py-1 rounded-lg border ${isPunc(t) ? "border-amber-300 bg-amber-50" : "border-zinc-300 bg-white"}`}>{t}</span>
                        {i < srcTokens.length - 1 && (
                          <button
                            className={`mx-1 px-2 py-1 rounded-full text-xs border ${boundaries[i] ? "bg-indigo-600 text-white border-indigo-600" : "bg-white text-zinc-600 border-zinc-300"}`}
                            onClick={() => { setBoundaries((prev) => prev.map((b, j) => (j === i ? !b : b))); setHasDragged(false); }}
                            title="Toggle boundary"
                          >
                            |
                          </button>
                        )}
                      </React.Fragment>
                    ))}
                  </div>
                )}
              </div>

              <div className="mt-3">
                <div className="text-xs text-zinc-500 mb-1">Chunk preview ({groups.length}):</div>
                <div className="flex flex-wrap gap-2">
                  {groups.length === 0 ? (
                    <span className="text-zinc-400 text-sm">(Add boundaries to see chunks)</span>
                  ) : (
                    groups.map((g, idx) => (
                      <span key={idx} className="px-2 py-1 rounded-lg border border-zinc-300 bg-white text-sm">{g}</span>
                    ))
                  )}
                </div>
              </div>

              <div className="flex flex-wrap gap-2 mt-3">
                <button className="px-3 py-2 text-sm rounded-xl bg-zinc-100 hover:bg-zinc-200" onClick={() => { setBoundaries(boundariesFromPunctuation(srcTokens)); setHasDragged(false); }}>Auto by punctuation</button>
                <button className="px-3 py-2 text-sm rounded-xl bg-zinc-100 hover:bg-zinc-200" onClick={() => { const v = new Array(boundaryCount(srcTokens)).fill(true); setBoundaries(v); setHasDragged(false); }}>Boundary after every token</button>
                <button className="px-3 py-2 text-sm rounded-xl bg-zinc-100 hover:bg-zinc-200" onClick={() => { const v = new Array(boundaryCount(srcTokens)).fill(false); setBoundaries(v); setHasDragged(false); }}>Clear boundaries</button>
              </div>
            </div>
          </div>

          <div className="bg-white border border-zinc-200 rounded-2xl p-4 md:p-6 shadow-sm">
            <div className="flex items-center justify-between mb-2">
              <h2 className="font-medium">2) Literal translation (phrase list)</h2>
              <button className="text-xs px-2 py-1 rounded-md bg-rose-50 hover:bg-rose-100" onClick={clearLiteral}>Clear literal</button>
            </div>
            <textarea
              {...NO_DRAG}
              className="w-full min-h-[110px] rounded-xl border border-zinc-300 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500"
              placeholder={direction.startsWith("EN") ? "Use | or new lines to separate (e.g., 很难 | 对我来说 | 学习中文)" : "Use | or new lines (e.g., it is difficult | for me | to learn Chinese)"}
              value={literalText}
              onChange={(e) => { setLiteralText(e.target.value); setHasDragged(false); }}
            />
            <div className="text-xs text-zinc-500 mt-2">Literal chunks detected: <strong>{literalChunks.length}</strong>. Source chunks: <strong>{groups.length}</strong>.
              {literalChunks.length !== groups.length && <span className="text-rose-600"> — counts don’t match; extra/short chunks will be ignored/padded.</span>}
            </div>
          </div>
        </div>

        {/* Drag board */}
        <div className="grid md:grid-cols-2 gap-6">
          <div className="bg-white border border-zinc-200 rounded-2xl p-4 md:p-6 shadow-sm">
            <div className="flex items-center justify-between mb-2">
              <h2 className="font-medium">Phrase cards</h2>
              <button className="text-xs px-2 py-1 rounded-md bg-rose-50 hover:bg-rose-100" onClick={clearCards}>Clear cards</button>
            </div>
            {cards.length === 0 ? (
              <div className="text-sm text-zinc-500">No cards yet. Add text above.</div>
            ) : (
              order.map((id, idx) => {
                const c = cards.find((x) => x.id === id);
                if (!c) return null;
                return (
                  <SortableCard
                    key={id}
                    id={id}
                    idx={idx}
                    src={c.src}
                    lit={c.lit}
                    role={c.role || "OTHER"}
                    onEdit={onEditCard}
                    onDragStart={handleCardDragStart}
                    onDragEnter={handleCardDragEnter}
                    onDragOver={handleCardDragOver}
                    onDrop={handleCardDrop}
                    onKeyMove={handleKeyMove}
                    onDragEnd={handleCardDragEnd}
                    isDragOver={dragOverId === id}
                    highlightRoles={highlightRoles}
                  />
                );
              })
            )}
          </div>

          <div className="bg-white border border-zinc-200 rounded-2xl p-4 md:p-6 shadow-sm">
            <h2 className="font-medium mb-2">Result</h2>
            <div className="text-xs text-zinc-500 mb-2">Joined literal translation in the new order:</div>
            <div className="min-h-[64px] border border-zinc-200 rounded-xl p-3 bg-zinc-50 text-lg leading-8">
              {composedTarget || <span className="text-zinc-400">(Will appear here once you reorder)</span>}
            </div>

            {/* Reasoning hints */}
            <div className="mt-3 text-xs text-zinc-600">
              <ReasoningHints direction={direction} cards={orderedCards} />
            </div>

            <div className="mt-4 grid gap-2">
              <button className="px-3 py-2 text-sm rounded-xl bg-zinc-100 hover:bg-zinc-200" onClick={() => setOrder(cards.map((c) => c.id))}>Reset order</button>
              <div className="text-xs text-zinc-500">Auto‑build is <strong>{autoBuild ? 'ON' : 'OFF'}</strong>{hasDragged ? ' (paused after drag)' : ''}. Use <em>Resume</em> to re‑enable.</div>
            </div>

            {/* Self-tests intentionally not mounted */}
            {/* <SelfTests /> */}
          </div>
        </div>
      </div>
    </div>
  );
}


