import { useMemo, useState, type KeyboardEvent } from "react";
import type { CaptureRow } from "../regex/extraction/capture-rows";
import { writeClipboardText } from "../browser/clipboard";

const PAGE_SIZE = 200;
const MAXIMUM_CODE_POINTS_SHOWN = 64;
const MAXIMUM_VALUE_PREVIEW_UTF16 = 512;

type SortKey =
  | "matchNumber"
  | "groupNumber"
  | "groupName"
  | "status"
  | "value"
  | "start"
  | "end"
  | "length"
  | "line"
  | "nativeStart";

const SORT_OPTIONS: readonly {
  readonly value: SortKey;
  readonly label: string;
}[] = [
  { value: "matchNumber", label: "Match" },
  { value: "groupNumber", label: "Group number" },
  { value: "groupName", label: "Group name" },
  { value: "status", label: "Status" },
  { value: "value", label: "Value" },
  { value: "start", label: "UTF-16 start" },
  { value: "end", label: "UTF-16 end" },
  { value: "length", label: "Length" },
  { value: "line", label: "Line" },
  { value: "nativeStart", label: "Native start" },
];

function visibleWhitespace(value: string): string {
  return value
    .replaceAll(" ", "·")
    .replaceAll("\t", "⇥")
    .replaceAll("\r", "␍")
    .replaceAll("\n", "↵\n");
}

function visibleValue(row: CaptureRow, showWhitespace: boolean): string {
  const value = row.value;
  if (value === undefined) return "—";
  const visible = value.slice(0, MAXIMUM_VALUE_PREVIEW_UTF16);
  const rendered = showWhitespace ? visibleWhitespace(visible) : visible;
  const uiTruncated = value.length > MAXIMUM_VALUE_PREVIEW_UTF16;
  if (row.status === "truncated") {
    return `${rendered}${uiTruncated ? "…" : ""} (engine value prefix: ${value.length.toLocaleString()} of ${row.length?.toLocaleString() ?? "unknown"} UTF-16 units${uiTruncated ? "; UI shows the first 512" : ""})`;
  }
  return uiTruncated
    ? `${rendered}… (${(row.length ?? value.length).toLocaleString()} UTF-16 units total; UI preview)`
    : rendered;
}

function download(text: string, fileName: string, type: string): void {
  const url = URL.createObjectURL(new Blob([text], { type }));
  const anchor = document.createElement("a");
  anchor.href = url;
  anchor.download = fileName;
  anchor.click();
  window.setTimeout(() => URL.revokeObjectURL(url), 1_000);
}

function compareValues(
  left: string | number | undefined,
  right: string | number | undefined,
): number {
  if (left === right) return 0;
  if (left === undefined) return 1;
  if (right === undefined) return -1;
  return typeof left === "number" && typeof right === "number"
    ? left - right
    : String(left).localeCompare(String(right));
}

function codePointOffsets(
  subject: string,
  requestedOffsets: readonly number[],
): ReadonlyMap<number, number> {
  const pending = [...new Set(requestedOffsets)]
    .filter((offset) => offset >= 0 && offset <= subject.length)
    .sort((left, right) => left - right);
  const result = new Map<number, number>();
  let pendingIndex = 0;
  let utf16Offset = 0;
  let codePointOffset = 0;

  while (pendingIndex < pending.length) {
    const target = pending[pendingIndex];
    if (target === undefined) break;
    if (target === utf16Offset) {
      result.set(target, codePointOffset);
      pendingIndex += 1;
      continue;
    }
    if (target < utf16Offset || utf16Offset >= subject.length) {
      pendingIndex += 1;
      continue;
    }
    const first = subject.charCodeAt(utf16Offset);
    const second = subject.charCodeAt(utf16Offset + 1);
    const width =
      first >= 0xd800 && first <= 0xdbff && second >= 0xdc00 && second <= 0xdfff
        ? 2
        : 1;
    utf16Offset += width;
    codePointOffset += 1;
  }
  return result;
}

function visibleCodePoints(value: string | undefined): string {
  if (value === undefined) return "—";
  const codePoints = Array.from(value);
  const rendered = codePoints
    .slice(0, MAXIMUM_CODE_POINTS_SHOWN)
    .map((item) => {
      const point = item.codePointAt(0);
      return point === undefined
        ? "U+?"
        : `U+${point.toString(16).toUpperCase().padStart(4, "0")}`;
    });
  return `${rendered.join(" ")}${
    codePoints.length > rendered.length
      ? ` … (${codePoints.length.toLocaleString()} total)`
      : ""
  }`;
}

export function CaptureTable({
  rows,
  subject,
  sourceResultTruncated = false,
  selectedId,
  onSelect,
}: {
  readonly rows: readonly CaptureRow[];
  readonly subject: string;
  readonly sourceResultTruncated?: boolean;
  readonly selectedId?: string;
  readonly onSelect: (row: CaptureRow) => void;
}) {
  const [filter, setFilter] = useState("");
  const [page, setPage] = useState(0);
  const [showWhitespace, setShowWhitespace] = useState(false);
  const [showAdvanced, setShowAdvanced] = useState(false);
  const [sortKey, setSortKey] = useState<SortKey>("matchNumber");
  const [sortDescending, setSortDescending] = useState(false);
  const [copyNotice, setCopyNotice] = useState<{
    readonly kind: "success" | "error";
    readonly message: string;
  }>();
  const filtered = useMemo(() => {
    const needle = filter.toLocaleLowerCase();
    if (!needle) return rows;
    return rows.filter((row) =>
      [
        row.matchNumber,
        row.groupNumber,
        row.groupName ?? "",
        row.status,
        row.value ?? "",
      ]
        .join(" ")
        .toLocaleLowerCase()
        .includes(needle),
    );
  }, [filter, rows]);
  const sorted = useMemo(
    () =>
      filtered
        .map((row, index) => ({ row, index }))
        .sort((left, right) => {
          const comparison = compareValues(
            left.row[sortKey],
            right.row[sortKey],
          );
          return (
            (sortDescending ? -comparison : comparison) ||
            left.index - right.index
          );
        })
        .map(({ row }) => row),
    [filtered, sortDescending, sortKey],
  );
  const selectedIndex =
    selectedId === undefined
      ? -1
      : sorted.findIndex((row) => row.id === selectedId);
  const selectedPage =
    selectedIndex < 0 ? undefined : Math.floor(selectedIndex / PAGE_SIZE);
  const selectionPageKey =
    selectedId === undefined
      ? undefined
      : `${selectedId}:${selectedPage ?? -1}`;
  const [synchronizedSelectionPageKey, setSynchronizedSelectionPageKey] =
    useState<string>();
  if (selectionPageKey !== synchronizedSelectionPageKey) {
    setSynchronizedSelectionPageKey(selectionPageKey);
    if (selectedPage !== undefined) setPage(selectedPage);
  }
  const maximumPage = Math.max(0, Math.ceil(sorted.length / PAGE_SIZE) - 1);
  const activePage = Math.min(page, maximumPage);
  const visible = sorted.slice(
    activePage * PAGE_SIZE,
    (activePage + 1) * PAGE_SIZE,
  );
  const visibleCodePointOffsets = useMemo(
    () =>
      showAdvanced
        ? codePointOffsets(
            subject,
            visible.flatMap((row) => [
              ...(row.start === undefined ? [] : [row.start]),
              ...(row.end === undefined ? [] : [row.end]),
            ]),
          )
        : new Map<number, number>(),
    [showAdvanced, subject, visible],
  );
  const selectedFilteredOut =
    selectedId !== undefined &&
    rows.some((row) => row.id === selectedId) &&
    !sorted.some((row) => row.id === selectedId);

  const copyRow = async (row: CaptureRow): Promise<void> => {
    try {
      await writeClipboardText(JSON.stringify(row));
      setCopyNotice({
        kind: "success",
        message: `Copied match ${row.matchNumber}, group ${row.groupNumber} row JSON.`,
      });
    } catch (error) {
      setCopyNotice({
        kind: "error",
        message: error instanceof Error ? error.message : String(error),
      });
    }
  };
  const copyValue = async (row: CaptureRow): Promise<void> => {
    if (row.value === undefined) return;
    try {
      await writeClipboardText(row.value);
      setCopyNotice({
        kind: "success",
        message:
          row.status === "truncated"
            ? `Copied the available engine value prefix for match ${row.matchNumber}, group ${row.groupNumber}.`
            : `Copied the complete returned value for match ${row.matchNumber}, group ${row.groupNumber}.`,
      });
    } catch (error) {
      setCopyNotice({
        kind: "error",
        message: error instanceof Error ? error.message : String(error),
      });
    }
  };

  const moveRowFocus = (
    event: KeyboardEvent<HTMLTableRowElement>,
    row: CaptureRow,
  ) => {
    if (event.target !== event.currentTarget) return;
    const body = event.currentTarget.closest("tbody");
    const rows = body
      ? Array.from(body.querySelectorAll<HTMLTableRowElement>("tr"))
      : [];
    const index = rows.indexOf(event.currentTarget);
    let target: number;

    switch (event.key) {
      case "ArrowDown":
        target = Math.min(rows.length - 1, index + 1);
        break;
      case "ArrowUp":
        target = Math.max(0, index - 1);
        break;
      case "Home":
        target = 0;
        break;
      case "End":
        target = rows.length - 1;
        break;
      case "Enter":
      case " ":
        event.preventDefault();
        onSelect(row);
        return;
      default:
        return;
    }

    event.preventDefault();
    const next = rows[target];
    next?.focus();
    next?.click();
  };

  return (
    <section className="panel capture-panel" aria-labelledby="capture-heading">
      <header className="panel-heading">
        <div>
          <p className="eyebrow">Compact result view</p>
          <h2 id="capture-heading">Capture table</h2>
        </div>
        <span>
          {filtered.length} row{filtered.length === 1 ? "" : "s"}
        </span>
      </header>
      <div className="table-tools">
        <label>
          <span>Filter captures</span>
          <input
            type="search"
            value={filter}
            onChange={(event) => {
              setFilter(event.target.value);
              setPage(0);
            }}
          />
        </label>
        <label className="check-control">
          <input
            type="checkbox"
            checked={showWhitespace}
            onChange={(event) => setShowWhitespace(event.target.checked)}
          />
          Show whitespace
        </label>
        <label>
          <span>Sort captures</span>
          <select
            value={sortKey}
            onChange={(event) => {
              setSortKey(event.target.value as SortKey);
              setPage(0);
            }}
          >
            {SORT_OPTIONS.map((option) => (
              <option key={option.value} value={option.value}>
                {option.label}
              </option>
            ))}
          </select>
        </label>
        <button
          type="button"
          className="secondary-button"
          aria-pressed={sortDescending}
          onClick={() => {
            setSortDescending((current) => !current);
            setPage(0);
          }}
        >
          {sortDescending ? "Descending" : "Ascending"}
        </button>
        <label className="check-control">
          <input
            type="checkbox"
            checked={showAdvanced}
            onChange={(event) => setShowAdvanced(event.target.checked)}
          />
          Unicode advanced view
        </label>
        <button
          type="button"
          className="secondary-button"
          onClick={() =>
            download(
              `${JSON.stringify(rows, null, 2)}\n`,
              "regex-captures.json",
              "application/json",
            )
          }
          disabled={rows.length === 0 || sourceResultTruncated}
        >
          Export all JSON
        </button>
      </div>
      {sourceResultTruncated ? (
        <p className="privacy-warning" role="alert">
          The engine stopped collecting results at its configured match or
          capture-row limit. Capture JSON export is disabled because these rows
          are only a returned prefix.
        </p>
      ) : null}
      {selectedFilteredOut ? (
        <p className="render-limit-banner" role="status">
          The synchronized capture row is hidden by the current filter. Clear
          the filter to show it.
        </p>
      ) : null}
      {copyNotice ? (
        <p
          className={
            copyNotice.kind === "error" ? "privacy-warning" : "copy-notice"
          }
          role={copyNotice.kind === "error" ? "alert" : "status"}
        >
          {copyNotice.message}
        </p>
      ) : null}
      <div className="capture-table-scroll">
        <table role="grid" aria-label="Captured matches">
          <thead>
            <tr>
              <th>Match</th>
              <th>Group</th>
              <th>Name</th>
              <th>Status</th>
              <th>Value</th>
              <th>Start (UTF-16)</th>
              <th>End (UTF-16)</th>
              <th>Length (UTF-16)</th>
              <th>Line:column</th>
              <th>Native start</th>
              <th>Native end</th>
              <th>Native unit</th>
              {showAdvanced ? (
                <>
                  <th>Start (code point)</th>
                  <th>End (code point)</th>
                  <th>Value code points</th>
                </>
              ) : null}
              <th>
                <span className="visually-hidden">Actions</span>
              </th>
            </tr>
          </thead>
          <tbody>
            {visible.map((row) => (
              <tr
                key={row.id}
                className={row.id === selectedId ? "is-selected" : ""}
                aria-selected={row.id === selectedId}
                aria-label={`Match ${row.matchNumber}, group ${row.groupNumber}${
                  row.groupName ? `, ${row.groupName}` : ""
                }, ${row.status.replaceAll("-", " ")}`}
                tabIndex={
                  row.id === selectedId ||
                  (!selectedId && row.id === visible[0]?.id)
                    ? 0
                    : -1
                }
                onClick={(event) => {
                  event.currentTarget.focus();
                  onSelect(row);
                }}
                onKeyDown={(event) => moveRowFocus(event, row)}
              >
                <td>{row.matchNumber}</td>
                <td>{row.groupNumber}</td>
                <td>{row.groupName ?? "—"}</td>
                <td>{row.status.replaceAll("-", " ")}</td>
                <td className="value-cell">
                  {visibleValue(row, showWhitespace)}
                </td>
                <td>{row.start ?? "—"}</td>
                <td>{row.end ?? "—"}</td>
                <td>{row.length ?? "—"}</td>
                <td>
                  {row.line === undefined ? "—" : `${row.line}:${row.column}`}
                </td>
                <td>{row.nativeStart ?? "—"}</td>
                <td>{row.nativeEnd ?? "—"}</td>
                <td>{row.nativeUnit ?? "—"}</td>
                {showAdvanced ? (
                  <>
                    <td>
                      {row.start === undefined
                        ? "—"
                        : (visibleCodePointOffsets.get(row.start) ??
                          "split surrogate")}
                    </td>
                    <td>
                      {row.end === undefined
                        ? "—"
                        : (visibleCodePointOffsets.get(row.end) ??
                          "split surrogate")}
                    </td>
                    <td className="code-point-cell">
                      {visibleCodePoints(row.value)}
                    </td>
                  </>
                ) : null}
                <td>
                  <span className="row-actions">
                    <button
                      type="button"
                      className="icon-button"
                      aria-label={`Copy value for match ${row.matchNumber}, group ${row.groupNumber}`}
                      disabled={row.value === undefined}
                      onClick={(event) => {
                        event.stopPropagation();
                        void copyValue(row);
                      }}
                    >
                      V
                    </button>
                    <button
                      type="button"
                      className="icon-button"
                      aria-label={`Copy row for match ${row.matchNumber}, group ${row.groupNumber}`}
                      onClick={(event) => {
                        event.stopPropagation();
                        void copyRow(row);
                      }}
                    >
                      ⧉
                    </button>
                  </span>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
      {filtered.length > PAGE_SIZE ? (
        <nav className="pagination" aria-label="Capture table pages">
          <button
            type="button"
            className="secondary-button"
            disabled={activePage === 0}
            onClick={() => setPage((current) => Math.max(0, current - 1))}
          >
            Previous
          </button>
          <span>
            Page {activePage + 1} of {maximumPage + 1}; rendering at most{" "}
            {PAGE_SIZE} rows
          </span>
          <button
            type="button"
            className="secondary-button"
            disabled={activePage === maximumPage}
            onClick={() =>
              setPage((current) => Math.min(maximumPage, current + 1))
            }
          >
            Next
          </button>
        </nav>
      ) : null}
    </section>
  );
}
