/**
 * Pure git-graph lane layout (L8 / git-graph view).
 *
 * `computeGraphRows` turns a newest-first commit list into render-ready rows that
 * a renderer can draw as a GitKraken-style graph: **continuous coloured lanes**
 * with curved connectors at branch points (forks) and merges.
 *
 * ## Model
 *
 * We keep an ordered list of "active lanes": `lanes[i]` is the hash of the commit
 * we expect next on lane `i` (or `null` when the lane is free). A lane index is a
 * stable column; we never re-index existing lanes (a freed lane is reused in
 * place), so a branch that doesn't bend draws a **straight vertical line**.
 *
 * Each row is split into two halves around the commit node (drawn at mid-height):
 * - `incoming` links go from the **top** boundary to the node's mid level,
 * - `outgoing` links go from the node's mid level to the **bottom** boundary.
 *
 * Straight links (`fromLane === toLane`) are pass-through lane lines; bent links
 * are the fork/merge connectors. Because lane x-positions are identical across
 * rows, a row's `outgoing` meets the next row's `incoming` at the shared
 * boundary, yielding unbroken lanes top-to-bottom.
 */

import type { GraphCommit } from "@/domain";

// ---------------------------------------------------------------------------
// Public types
// ---------------------------------------------------------------------------

/** A line segment within one half of a row. */
export interface GraphLink {
  /** Lane index at the start of the segment. */
  fromLane: number;
  /** Lane index at the end of the segment. */
  toLane: number;
  /** Stroke colour. */
  color: string;
}

export interface GraphRow {
  commit: GraphCommit;
  /** Lane (column) of this commit's node. */
  lane: number;
  /** Node colour (its lane's colour). */
  color: string;
  /** Links in the top half: top boundary → node mid. */
  incoming: GraphLink[];
  /** Links in the bottom half: node mid → bottom boundary. */
  outgoing: GraphLink[];
  /** Number of lane columns occupied around this row (for SVG width). */
  laneCount: number;
}

// ---------------------------------------------------------------------------
// Colour palette (dark-theme-friendly)
// ---------------------------------------------------------------------------

const LANE_COLORS: readonly string[] = [
  "#4f9cf9", // blue
  "#a78bfa", // violet
  "#34d399", // emerald
  "#fbbf24", // amber
  "#f87171", // rose
  "#38bdf8", // sky
  "#fb923c", // orange
  "#c084fc", // purple
  "#f472b6", // pink
  "#2dd4bf", // teal
];

/** Stable colour for a lane index. */
export function laneColor(laneIndex: number): string {
  return LANE_COLORS[laneIndex % LANE_COLORS.length];
}

// ---------------------------------------------------------------------------
// Core algorithm
// ---------------------------------------------------------------------------

/** Index of the first free (null) lane, or -1. */
function firstFree(lanes: (string | null)[]): number {
  return lanes.indexOf(null);
}

/** Places `hash` on a lane: an existing free slot if any, else a new column. */
function allocLane(lanes: (string | null)[], hash: string): number {
  const free = firstFree(lanes);
  if (free !== -1) {
    lanes[free] = hash;
    return free;
  }
  lanes.push(hash);
  return lanes.length - 1;
}

/**
 * Assigns lanes + links to each commit.
 *
 * @param commits Commits ordered **newest-first** (as `git log`).
 */
export function computeGraphRows(commits: GraphCommit[]): GraphRow[] {
  // lanes[i] = hash expected next on lane i (null = free).
  const lanes: (string | null)[] = [];
  const rows: GraphRow[] = [];

  for (const commit of commits) {
    const { hash, parents } = commit;
    const top = lanes.slice();

    // ── Resolve the commit's lane ────────────────────────────────────────
    let commitLane = lanes.indexOf(hash);
    if (commitLane === -1) {
      // A tip nobody pointed at yet: open a lane for it.
      commitLane = allocLane(lanes, hash);
    }

    // Other lanes also expecting this hash converge into the node and end.
    for (let i = 0; i < top.length; i++) {
      if (i !== commitLane && top[i] === hash) lanes[i] = null;
    }

    // ── Route parents ────────────────────────────────────────────────────
    const parentLanes: number[] = [];
    if (parents.length === 0) {
      lanes[commitLane] = null; // root: lane ends here.
    } else {
      lanes[commitLane] = parents[0]; // first parent keeps the lane.
      parentLanes.push(commitLane);
      for (let k = 1; k < parents.length; k++) {
        const ph = parents[k];
        const existing = lanes.indexOf(ph);
        parentLanes.push(existing !== -1 ? existing : allocLane(lanes, ph));
      }
    }

    const bottom = lanes.slice();

    // ── Top half: every active top lane → node mid ───────────────────────
    const incoming: GraphLink[] = [];
    for (let i = 0; i < top.length; i++) {
      if (top[i] === null) continue;
      if (i === commitLane || top[i] === hash) {
        // Into the node (straight if i === commitLane, a converging curve else).
        incoming.push({ fromLane: i, toLane: commitLane, color: laneColor(i) });
      } else {
        incoming.push({ fromLane: i, toLane: i, color: laneColor(i) }); // pass-through
      }
    }

    // ── Bottom half: node mid → every active bottom lane ─────────────────
    const outgoing: GraphLink[] = [];
    for (let j = 0; j < bottom.length; j++) {
      if (bottom[j] === null) continue;
      if (parentLanes.includes(j)) {
        outgoing.push({ fromLane: commitLane, toLane: j, color: laneColor(j) }); // node → parent
      } else {
        outgoing.push({ fromLane: j, toLane: j, color: laneColor(j) }); // pass-through
      }
    }

    rows.push({
      commit,
      lane: commitLane,
      color: laneColor(commitLane),
      incoming,
      outgoing,
      laneCount: Math.max(top.length, bottom.length, commitLane + 1),
    });
  }

  return rows;
}
