<script lang="ts">
  import { ChevronDown, ChevronRight, GitBranch, RotateCcw } from "@lucide/svelte";
  import type { FileStatusKind, GitCommit, GitCommitFile } from "../types";

  interface GraphSegment {
    fromCol: number;
    toCol: number;
    color: string;
  }

  interface GraphRow {
    dotCol: number;
    dotColor: string;
    top: GraphSegment[];
    bottom: GraphSegment[];
  }

  const GRAPH_COLORS = [
    "#2f6fb0", "#4aa777", "#c9851f", "#a05bd0",
    "#cc4b6e", "#1f9ab0", "#7a8a1f", "#b0631f",
  ];
  const GRAPH_LANE = 16;

  interface Props {
    commits: GitCommit[];
    hasRepository: boolean;
    isBusy: boolean;
    expandedCommitHashes: Set<string>;
    onRestoreCommit: (commit: GitCommit) => void;
    onPreviewCommitFile: (commit: GitCommit, file: GitCommitFile) => void;
    onToggleCommitFiles: (hash: string) => void;
    onCreateBranchFromCommit: (commit: GitCommit) => void;
  }

  let {
    commits = [],
    hasRepository = false,
    isBusy = false,
    expandedCommitHashes = new Set(),
    onRestoreCommit = () => {},
    onPreviewCommitFile = () => {},
    onToggleCommitFiles = () => {},
    onCreateBranchFromCommit = () => {},
  }: Props = $props();

  function laneColor(col: number): string {
    return GRAPH_COLORS[((col % GRAPH_COLORS.length) + GRAPH_COLORS.length) % GRAPH_COLORS.length];
  }

  function graphColX(col: number): number {
    return col * GRAPH_LANE + GRAPH_LANE / 2;
  }

  function computeGraph(items: GitCommit[]): { rows: GraphRow[]; columns: number } {
    const rows: GraphRow[] = [];
    let lanes: (string | null)[] = [];
    let maxColumns = 1;

    for (const commit of items) {
      const before = lanes.slice();

      let col = before.indexOf(commit.hash);
      if (col === -1) {
        col = before.indexOf(null);
        if (col === -1) col = before.length;
      }

      const after = before.slice();
      while (after.length <= col) after.push(null);

      for (let k = 0; k < after.length; k++) {
        if (after[k] === commit.hash) after[k] = null;
      }

      after[col] = commit.parents.length > 0 ? commit.parents[0] : null;

      const fromCommit = new Set<number>([col]);
      for (let p = 1; p < commit.parents.length; p++) {
        let slot = after.indexOf(null);
        if (slot === -1) { slot = after.length; after.push(null); }
        after[slot] = commit.parents[p];
        fromCommit.add(slot);
      }

      const top: GraphSegment[] = [];
      for (let k = 0; k < before.length; k++) {
        const target = before[k];
        if (target == null) continue;
        top.push({ fromCol: k, toCol: target === commit.hash ? col : k, color: laneColor(k) });
      }

      const bottom: GraphSegment[] = [];
      for (let k = 0; k < after.length; k++) {
        if (after[k] == null) continue;
        bottom.push({ fromCol: fromCommit.has(k) ? col : k, toCol: k, color: laneColor(k) });
      }

      rows.push({ dotCol: col, dotColor: laneColor(col), top, bottom });

      lanes = after.slice();
      while (lanes.length > 0 && lanes[lanes.length - 1] == null) lanes.pop();
      maxColumns = Math.max(maxColumns, before.length, after.length, col + 1);
    }

    return { rows, columns: maxColumns };
  }

  function statusLabel(kind: FileStatusKind): string {
    return kind;
  }

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

  function formatCommitDate(value: string): string {
    const date = new Date(value);
    if (Number.isNaN(date.getTime())) return value;
    return new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short" }).format(date);
  }

  let graph = $derived(computeGraph(commits));
  let graphRows = $derived(graph.rows);
  let graphWidth = $derived(Math.max(graph.columns, 1) * GRAPH_LANE);
</script>

<section class="panel grid grid-rows-[auto_1fr] overflow-hidden" aria-label="Commit history">
  <div class="section-head">
    <div>
      <span class="eyebrow">History</span>
      <h2 class="mt-0.5 text-ink text-base font-bold leading-tight">Commits</h2>
    </div>
    <span class="pill pill-count">{commits.length}</span>
  </div>

  {#if !hasRepository}
    <div class="blank-state">No repository loaded.</div>
  {:else if commits.length === 0}
    <div class="blank-state">No commits returned.</div>
  {:else}
    <div class="history-list graph-list overflow-auto">
      {#each commits as item, rowIndex (item.hash)}
        {@const row = graphRows[rowIndex]}
        <article class="commit-row graph-row">
          <div class="graph-gutter" style={`width:${graphWidth}px`} aria-hidden="true">
            {#if row}
              <svg class="graph-svg" viewBox={`0 0 ${graphWidth} 100`} preserveAspectRatio="none">
                {#each row.top as seg}
                  <line
                    x1={graphColX(seg.fromCol)} y1="0"
                    x2={graphColX(seg.toCol)} y2="50"
                    stroke={seg.color} stroke-width="2" vector-effect="non-scaling-stroke"
                  />
                {/each}
                {#each row.bottom as seg}
                  <line
                    x1={graphColX(seg.fromCol)} y1="50"
                    x2={graphColX(seg.toCol)} y2="100"
                    stroke={seg.color} stroke-width="2" vector-effect="non-scaling-stroke"
                  />
                {/each}
              </svg>
              <span
                class="graph-dot"
                class:merge={item.parents.length > 1}
                style={`left:${graphColX(row.dotCol)}px; --dot-color:${row.dotColor}`}
              ></span>
            {/if}
          </div>

          <div class="commit-body">
            <div class="commit-line">
              <div>
                <strong title={item.summary}>{item.summary}</strong>
                <span>{item.short_hash} - {item.author_name}</span>
              </div>
            </div>

            {#if item.refs.length > 0}
              <div class="ref-list" aria-label="Commit refs">
                {#each item.refs as ref}
                  <span>{ref}</span>
                {/each}
              </div>
            {/if}

            {#if item.files.length > 0}
              <div class="commit-files">
                <button
                  class="commit-files-toggle"
                  type="button"
                  onclick={() => onToggleCommitFiles(item.hash)}
                  aria-expanded={expandedCommitHashes.has(item.hash)}
                >
                  {#if expandedCommitHashes.has(item.hash)}
                    <ChevronDown size={14} aria-hidden="true" />
                  {:else}
                    <ChevronRight size={14} aria-hidden="true" />
                  {/if}
                  {item.files.length} {item.files.length === 1 ? "file" : "files"} changed
                </button>

                {#if expandedCommitHashes.has(item.hash)}
                  <div class="commit-file-list" aria-label="Changed files">
                    {#each item.files as file (`${item.hash}:${file.old_path ?? ""}:${file.path}`)}
                      <button
                        class="commit-file-button"
                        type="button"
                        onclick={() => onPreviewCommitFile(item, file)}
                        disabled={isBusy}
                        title="Show differences before restoring"
                      >
                        <span class={`status-badge ${file.status}`}>{statusLabel(file.status)}</span>
                        <strong>{displayCommitFile(file)}</strong>
                      </button>
                    {/each}
                  </div>
                {/if}
              </div>
            {/if}

            <div class="commit-actions">
              <time datetime={item.date}>{formatCommitDate(item.date)}</time>
              <div class="commit-action-buttons">
                <button class="btn-sm" type="button" onclick={() => onCreateBranchFromCommit(item)} disabled={isBusy} title="Create a new branch from this commit">
                  <GitBranch size={15} aria-hidden="true" />
                  Branch
                </button>
                <button class="btn-sm" type="button" onclick={() => onRestoreCommit(item)} disabled={isBusy} title="Reset current branch to this commit">
                  <RotateCcw size={15} aria-hidden="true" />
                  Restore
                </button>
              </div>
            </div>
          </div>
        </article>
      {/each}
    </div>
  {/if}
</section>
