<script lang="ts">
  import { ArrowRight, FileCode, GitCompare, RotateCcw, X } from "@lucide/svelte";
  import type { GitCommitComparison, GitDiffFile, FileStatusKind } from "../types";

  type SplitRow =
    | { type: "span"; kind: "meta" | "hunk"; text: string }
    | {
        type: "pair";
        leftNum?: number;  leftText?: string;  leftKind:  "del" | "context" | "empty";
        rightNum?: number; rightText?: string; rightKind: "add" | "context" | "empty";
      };

  interface DiffMarker {
    start: number;
    end: number;
    kind: "add" | "delete" | "mixed";
  }

  interface Props {
    comparison: GitCommitComparison;
    selectedDiffPath: string;
    isBusy: boolean;
    fromLabel?: string;
    toLabel?: string;
    restoreLabel?: string;
    /** When opened from a search hit, the term to highlight on matching lines. */
    highlightQuery?: string;
    language?: "en" | "de";
    onClose: () => void;
    onRestore?: () => void;
    onSelectFile: (file: GitDiffFile) => void;
  }

  let {
    comparison,
    selectedDiffPath = "",
    isBusy = false,
    fromLabel = "",
    toLabel = "",
    restoreLabel = "",
    highlightQuery = "",
    language = "en",
    onClose = () => {},
    onRestore = undefined,
    onSelectFile = () => {},
  }: Props = $props();

  let isGerman = $derived(language === "de");

  // Needle = first non-empty line of the search query, lowercased for matching.
  let highlightNeedle = $derived(
    highlightQuery
      .split("\n")
      .map((line) => line.trim())
      .find((line) => line.length > 0)
      ?.toLowerCase() ?? ""
  );

  function isMatch(text?: string): boolean {
    return highlightNeedle.length > 0 && !!text && text.toLowerCase().includes(highlightNeedle);
  }

  let beforePane = $state<HTMLDivElement | null>(null);
  let afterPane = $state<HTMLDivElement | null>(null);
  let isSyncingSplitScroll = false;

  function syncSplitScroll(source: "before" | "after") {
    if (isSyncingSplitScroll) return;
    const sourcePane = source === "before" ? beforePane : afterPane;
    const targetPane = source === "before" ? afterPane : beforePane;
    if (!sourcePane || !targetPane) return;

    isSyncingSplitScroll = true;
    targetPane.scrollTop = sourcePane.scrollTop;
    targetPane.scrollLeft = sourcePane.scrollLeft;
    requestAnimationFrame(() => {
      isSyncingSplitScroll = false;
    });
  }

  function displayDiffFile(file: GitDiffFile): string {
    return file.old_path ? `${file.old_path} -> ${file.path}` : file.path;
  }

  function buildDiffByPath(patch: string): Map<string, string> {
    const map = new Map<string, string>();
    if (!patch.trim()) return map;
    let current: string[] = [];
    for (const line of patch.split("\n")) {
      if (line.startsWith("diff --git ") && current.length > 0) {
        const seg = current.join("\n");
        const p = segmentPath(seg);
        if (p) map.set(p, seg);
        current = [];
      }
      current.push(line);
    }
    if (current.length > 0) {
      const seg = current.join("\n");
      const p = segmentPath(seg);
      if (p) map.set(p, seg);
    }
    return map;
  }

  function segmentPath(seg: string): string {
    let plus = "";
    let minus = "";
    for (const line of seg.split("\n")) {
      if (line.startsWith("+++ ")) {
        const r = line.slice(4).trim();
        plus = r.startsWith("b/") ? r.slice(2) : r;
      } else if (line.startsWith("--- ")) {
        const r = line.slice(4).trim();
        minus = r.startsWith("a/") ? r.slice(2) : r;
      } else if (line.startsWith("@@")) break;
    }
    return (plus && plus !== "/dev/null") ? plus : minus;
  }

  function buildSplitRows(patch: string): SplitRow[] {
    if (!patch.trim()) return [];
    const lines = patch.replace(/\n$/, "").split("\n");
    const rows: SplitRow[] = [];
    let leftNum = 0;
    let rightNum = 0;
    const dels: string[] = [];
    const adds: string[] = [];

    function flush() {
      if (dels.length === 0 && adds.length === 0) return;
      const count = Math.max(dels.length, adds.length);
      for (let i = 0; i < count; i++) {
        const hasDel = i < dels.length;
        const hasAdd = i < adds.length;
        if (hasDel) leftNum++;
        if (hasAdd) rightNum++;
        rows.push({
          type: "pair",
          leftNum:  hasDel ? leftNum  : undefined,
          leftText: hasDel ? dels[i].slice(1) : undefined,
          leftKind: hasDel ? "del" : "empty",
          rightNum:  hasAdd ? rightNum  : undefined,
          rightText: hasAdd ? adds[i].slice(1) : undefined,
          rightKind: hasAdd ? "add" : "empty",
        });
      }
      dels.length = 0;
      adds.length = 0;
    }

    for (const line of lines) {
      const isMeta =
        line.startsWith("diff ") || line.startsWith("index ") ||
        line.startsWith("--- ")  || line.startsWith("+++ ")   ||
        line.startsWith("new file") || line.startsWith("deleted file") ||
        line.startsWith("rename ")  || line.startsWith("similarity ");

      if (isMeta) {
        flush();
        continue;
      } else if (line.startsWith("@@")) {
        flush();
        const m = line.match(/@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
        if (m) { leftNum = parseInt(m[1]) - 1; rightNum = parseInt(m[2]) - 1; }
      } else if (line.startsWith("\\ ")) {
        flush();
      } else if (line.startsWith("-")) {
        dels.push(line);
      } else if (line.startsWith("+")) {
        adds.push(line);
      } else if (line.startsWith(" ")) {
        flush();
        leftNum++;
        rightNum++;
        rows.push({
          type: "pair",
          leftNum,  leftText:  line.slice(1), leftKind:  "context",
          rightNum, rightText: line.slice(1), rightKind: "context",
        });
      } else {
        flush();
        rows.push({ type: "span", kind: "meta", text: line });
      }
    }
    flush();
    return rows;
  }

  function buildDiffMarkers(rows: SplitRow[]): DiffMarker[] {
    const markers: DiffMarker[] = [];
    let current: DiffMarker | null = null;
    for (let index = 0; index < rows.length; index++) {
      const row = rows[index];
      if (row.type !== "pair" || (row.leftKind === "context" && row.rightKind === "context")) {
        current = null;
        continue;
      }
      const kind = row.leftKind === "del" && row.rightKind === "add"
        ? "mixed"
        : row.rightKind === "add" ? "add" : "delete";
      if (current && current.end === index - 1 && current.kind === kind) {
        current.end = index;
      } else {
        current = { start: index, end: index, kind };
        markers.push(current);
      }
    }
    return markers;
  }

  function scrollToDiffMarker(rowIndex: number) {
    const ratio = rowIndex / Math.max(splitRows.length - 1, 1);
    for (const pane of [beforePane, afterPane]) {
      if (pane) pane.scrollTop = ratio * Math.max(pane.scrollHeight - pane.clientHeight, 0);
    }
  }

  let diffByPath      = $derived(buildDiffByPath(comparison.patch));
  let selectedFile    = $derived(comparison.files.find((f) => f.path === selectedDiffPath) ?? null);
  let selectedPatch   = $derived(selectedFile ? (diffByPath.get(selectedFile.path) ?? "") : "");
  let splitRows       = $derived(buildSplitRows(selectedPatch));
  let diffMarkers     = $derived(buildDiffMarkers(splitRows));
</script>

<div
  class="dialog-backdrop compare-dialog-backdrop"
  role="presentation"
>
  <div class="dialog compare-dialog" role="dialog" aria-modal="true" aria-label="Branch or commit comparison" tabindex="-1">

    <header class="compare-dialog-head">
      <div class="compare-dialog-title">
        <span class="compare-dialog-mark"><GitCompare size={18} aria-hidden="true" /></span>
        <div>
          <h2>{isGerman ? "Änderungen vergleichen" : "Compare changes"}</h2>
          <p class="dialog-range">
            <span class="hash" title={comparison.from_hash}>{fromLabel || comparison.from_short}</span>
            <ArrowRight size={14} aria-hidden="true" />
            <span class="hash" title={comparison.to_hash}>{toLabel || comparison.to_short}</span>
          </p>
        </div>
      </div>
      <div class="dialog-header-actions">
        {#if restoreLabel && onRestore}
          <button class="btn-secondary compare-restore" type="button" onclick={onRestore} disabled={isBusy} title={restoreLabel}>
            <RotateCcw size={15} aria-hidden="true" />
            <span>{restoreLabel}</span>
          </button>
        {/if}
        <button class="dialog-close" type="button" onclick={onClose} title={isGerman ? "Schließen" : "Close"} aria-label={isGerman ? "Vergleich schließen" : "Close comparison"}>
          <X size={18} aria-hidden="true" />
        </button>
      </div>
    </header>

    {#if comparison.files.length === 0}
      <div class="blank-state">{isGerman ? "Keine Unterschiede - diese Versionen sind identisch." : "No differences - these versions are identical."}</div>
    {:else}
      <div class="dialog-body">

        <!-- File list -->
        <aside class="dialog-files" aria-label={isGerman ? "Geänderte Dateien" : "Changed files"}>
          <div class="compare-files-head">
            <span>{isGerman ? "Geänderte Dateien" : "Changed files"}</span>
            <strong>{comparison.files.length}</strong>
          </div>
          {#each comparison.files as file (`${file.old_path ?? ""}:${file.path}`)}
            <button
              class="dialog-file-row"
              class:active={selectedDiffPath === file.path}
              type="button"
              onclick={() => onSelectFile(file)}
              title={displayDiffFile(file)}
            >
              <span class={`status-badge ${file.status}`}>{file.status}</span>
              <strong>{displayDiffFile(file)}</strong>
              <span class="diff-counts">
                <span class="adds">+{file.additions}</span>
                <span class="dels">-{file.deletions}</span>
              </span>
            </button>
          {/each}
        </aside>

        <!-- Diff pane -->
        <div class="dialog-diff">
          {#if !selectedFile}
            <div class="blank-state">{isGerman ? "Wähle eine Datei aus, um ihre Änderungen zu sehen." : "Select a file to see its changes."}</div>
          {:else if splitRows.length === 0}
            <div class="blank-state">{isGerman ? "Keine textuellen Änderungen für diese Datei." : "No textual changes for this file."}</div>
          {:else}
            <!-- Path bar -->
            <div class="diff-header">
              <FileCode size={13} aria-hidden="true" />
              <span>{displayDiffFile(selectedFile)}</span>
              <span class="diff-counts" style="margin-left: auto; flex-shrink: 0;">
                <span class="adds">+{selectedFile.additions}</span>
                <span class="dels">-{selectedFile.deletions}</span>
              </span>
            </div>

            <!-- Column headers -->
            <div class="split-col-headers">
              <div class="split-col-label">
                <span>{isGerman ? "Vorher" : "Before"}</span>
                <span class="split-col-hash" title={comparison.from_hash}>{fromLabel || comparison.from_short}</span>
              </div>
              <div class="split-col-label">
                <span>{isGerman ? "Nachher" : "After"}</span>
                <span class="split-col-hash" title={comparison.to_hash}>{toLabel || comparison.to_short}</span>
              </div>
            </div>

            <!-- Split diff grid -->
            <div class="split-diff-shell">
            <div class="split-diff" role="table" aria-label="Side-by-side diff">
              <div
                class="split-pane"
                bind:this={beforePane}
                aria-label="Before file content"
                onscroll={() => syncSplitScroll("before")}
              >
                <div class="split-pane-grid">
                  {#each splitRows as row, i (`left-${i}`)}
                    {#if row.type === "span"}
                      <div class="split-span split-{row.kind}">{row.text}</div>
                    {:else}
                      <div class="split-num" class:del={row.leftKind === "del"} class:empty={row.leftKind === "empty"} class:match={isMatch(row.leftText)}>{row.leftNum ?? ""}</div>
                      <div class="split-cell" class:del={row.leftKind === "del"} class:empty={row.leftKind === "empty"} class:match={isMatch(row.leftText)}>{row.leftText ?? " "}</div>
                    {/if}
                  {/each}
                </div>
              </div>
              <div
                class="split-pane"
                bind:this={afterPane}
                aria-label="After file content"
                onscroll={() => syncSplitScroll("after")}
              >
                <div class="split-pane-grid">
                  {#each splitRows as row, i (`right-${i}`)}
                    {#if row.type === "span"}
                      <div class="split-span split-{row.kind}">{row.text}</div>
                    {:else}
                      <div class="split-num" class:add={row.rightKind === "add"} class:empty={row.rightKind === "empty"} class:match={isMatch(row.rightText)}>{row.rightNum ?? ""}</div>
                      <div class="split-cell" class:add={row.rightKind === "add"} class:empty={row.rightKind === "empty"} class:match={isMatch(row.rightText)}>{row.rightText ?? " "}</div>
                    {/if}
                  {/each}
                </div>
              </div>
            </div>
            <nav class="diff-overview" aria-label="Change overview">
              {#each diffMarkers as marker, index (`${marker.start}-${marker.end}-${marker.kind}`)}
                <button
                  class="diff-overview-marker {marker.kind}"
                  type="button"
                  style={`--marker-position: ${(marker.start / Math.max(splitRows.length - 1, 1)) * 100}%`}
                  onclick={() => scrollToDiffMarker(marker.start)}
                  title={`Jump to change ${index + 1} of ${diffMarkers.length}`}
                  aria-label={`Jump to change ${index + 1} of ${diffMarkers.length}`}
                ></button>
              {/each}
            </nav>
            </div>
          {/if}
        </div>

      </div>
    {/if}
  </div>
</div>
