import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { buildVersionGraph, layoutVersionGraph } from '../../data/versionGraph';
import { formatChangeType } from '../../data/historyUtils';
import { useServerData } from '../../context/ServerDataContext';
import { UserAvatar } from '../UserAvatar';

// Pixel geometry: grid coords from layoutVersionGraph × these strides.
const CARD_W = 176;
const CARD_H = 118;
const GAP_X = 36;
const GAP_Y = 48;
const STRIDE_X = CARD_W + GAP_X;
const STRIDE_Y = CARD_H + GAP_Y;
const ZOOM_MIN = 0.25;
const ZOOM_MAX = 2.5;
const CORNER = 10; // connector corner radius

const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));

// Elbow connector from a parent's bottom-center to a child's top-center, with
// rounded corners at the mid-line bend.
function connectorPath(px, py, cx, cy) {
  const ym = (py + cy) / 2;
  if (px === cx) return `M ${px} ${py} V ${cy}`;
  const dir = cx > px ? 1 : -1;
  const r = Math.min(CORNER, Math.abs(cx - px) / 2, (cy - py) / 2);
  return (
    `M ${px} ${py} V ${ym - r} ` +
    `Q ${px} ${ym} ${px + dir * r} ${ym} ` +
    `H ${cx - dir * r} ` +
    `Q ${cx} ${ym} ${cx} ${ym + r} ` +
    `V ${cy}`
  );
}

/**
 * Zoomable/pannable VERSION tree for a single file. Each card is a unique
 * content state; the server-head state is highlighted, the last-synced local
 * state gets an orange outline when it differs. Wheel zooms toward the viewport
 * center; dragging the grey background pans; clicking a card opens the revision
 * that first created that version.
 */
export function HistoryGraphView({
  fileHistory,
  relativePath,
  serverHeadHash,
  syncedHash,
  onOpenRevision,
}) {
  const { getUser } = useServerData();
  const areaRef = useRef(null);
  const [view, setView] = useState({ x: 0, y: 0, zoom: 1 });

  const graph = useMemo(() => {
    if (!relativePath || !fileHistory || !fileHistory.length) return null;
    const built = buildVersionGraph(fileHistory, { serverHeadHash, syncedHash });
    layoutVersionGraph(built.nodes);
    return built;
  }, [relativePath, fileHistory, serverHeadHash, syncedHash]);

  const showGraph = Boolean(graph?.nodes.length);

  // Wheel zoom (native non-passive: React's synthetic onWheel is passive, so
  // preventDefault would not stop page scroll).
  useEffect(() => {
    const el = areaRef.current;
    if (!el) return undefined;
    const onWheel = (e) => {
      e.preventDefault();
      const rect = el.getBoundingClientRect();
      const cx = rect.width / 2;
      const cy = rect.height / 2;
      setView((v) => {
        const zoom = clamp(v.zoom * Math.exp(-e.deltaY * 0.001), ZOOM_MIN, ZOOM_MAX);
        const scale = zoom / v.zoom;
        return { zoom, x: cx - (cx - v.x) * scale, y: cy - (cy - v.y) * scale };
      });
    };
    el.addEventListener('wheel', onWheel, { passive: false });
    return () => el.removeEventListener('wheel', onWheel);
  }, [showGraph]);

  // Start each file centered on its current (server-head) node, falling back to
  // the newest node when the head matches nothing.
  useLayoutEffect(() => {
    const el = areaRef.current;
    if (!el || !graph?.nodes.length) return;
    const focus = graph.nodes.find((n) => n.isCurrent) ?? graph.nodes[graph.nodes.length - 1];
    const rect = el.getBoundingClientRect();
    setView({
      zoom: 1,
      x: rect.width / 2 - (focus.x * STRIDE_X + CARD_W / 2),
      y: rect.height / 2 - (focus.y * STRIDE_Y + CARD_H / 2),
    });
  }, [relativePath, graph?.nodes.length]);

  const handleMouseDown = (e) => {
    if (e.button !== 0) return;
    e.preventDefault();
    const start = { mx: e.clientX, my: e.clientY, x: view.x, y: view.y };
    const onMove = (ev) => {
      setView((v) => ({ ...v, x: start.x + (ev.clientX - start.mx), y: start.y + (ev.clientY - start.my) }));
    };
    const onUp = () => {
      document.removeEventListener('mousemove', onMove);
      document.removeEventListener('mouseup', onUp);
    };
    document.addEventListener('mousemove', onMove);
    document.addEventListener('mouseup', onUp);
  };

  if (!relativePath) {
    return <div className="graph-view graph-view--empty">Select a file to see its revision graph.</div>;
  }
  if (!showGraph) {
    return <div className="graph-view graph-view--empty">No revisions recorded for this file.</div>;
  }

  const contentW = Math.max(...graph.nodes.map((n) => n.x)) * STRIDE_X + CARD_W;
  const contentH = Math.max(...graph.nodes.map((n) => n.y)) * STRIDE_Y + CARD_H;
  const byVersion = new Map(graph.nodes.map((n) => [n.version, n]));

  return (
    <div className="graph-view" ref={areaRef} onMouseDown={handleMouseDown}>
      <div className="graph-view-file" onMouseDown={(e) => e.stopPropagation()}>{relativePath}</div>
      <div
        className="graph-canvas"
        style={{ transform: `translate(${view.x}px, ${view.y}px) scale(${view.zoom})` }}
      >
        <svg className="graph-edges" width={contentW} height={contentH}>
          {graph.nodes
            .filter((n) => n.parent != null)
            .map((n) => {
              const p = byVersion.get(n.parent);
              const px = p.x * STRIDE_X + CARD_W / 2;
              const py = p.y * STRIDE_Y + CARD_H;
              const cx = n.x * STRIDE_X + CARD_W / 2;
              const cy = n.y * STRIDE_Y;
              return (
                <path
                  key={n.version}
                  className={`graph-edge${n.isDead ? ' graph-edge--dead' : ''}`}
                  d={connectorPath(px, py, cx, cy)}
                />
              );
            })}
        </svg>
        {graph.nodes.map((node) => {
          const user = getUser(node.author);
          // Cap the name so a long one can't collide with the right-aligned
          // status; the full name stays available on hover.
          const shortName =
            user.displayName.length > 12
              ? `${user.displayName.slice(0, 12).trimEnd()}…`
              : user.displayName;
          const open = () => onOpenRevision(node.firstRevision);
          return (
            // A div, not a <button>: WebKit (both WKWebView and WebKitGTK)
            // mangles flexbox height distribution inside a <button>, which
            // collapsed the description box to its text. role/tabIndex/keydown
            // keep it keyboard-accessible.
            <div
              key={node.version}
              role="button"
              tabIndex={0}
              className={`graph-card${node.isCurrent ? ' graph-card--current' : ''}${
                node.isLocal && !node.isCurrent ? ' graph-card--local' : ''
              }${node.isDead ? ' graph-card--dead' : ''}`}
              style={{ left: node.x * STRIDE_X, top: node.y * STRIDE_Y }}
              onMouseDown={(e) => e.stopPropagation()}
              onClick={open}
              onKeyDown={(e) => {
                if (e.key === 'Enter' || e.key === ' ') {
                  e.preventDefault();
                  open();
                }
              }}
            >
              <div className="graph-card-version">{node.depth + 1}</div>
              <div className="graph-card-author">
                <UserAvatar user={user} size={16} />
                <span className="graph-card-author-name" title={user.displayName}>
                  {shortName}
                </span>
                <span className={`graph-card-status graph-card-status--${node.change}`}>
                  {formatChangeType(node.change).toUpperCase()}
                </span>
              </div>
              <div className="graph-card-desc">
                <span
                  className={`graph-card-desc-text${node.description ? '' : ' graph-card-desc-text--empty'}`}
                >
                  {node.description || '(no description)'}
                </span>
              </div>
            </div>
          );
        })}
      </div>
    </div>
  );
}
