// Per-file VERSION graph for the History panel's Graph View.
//
// A node is a unique CONTENT STATE of one file, not a revision. Reverting to an
// old state, deleting an already-deleted file, or editing to content that
// already exists all REVISIT an existing node (moving the "current" pointer)
// instead of adding one. A new node appears only when a revision produces
// content never seen before. A node's parent is fixed the moment it is created
// and revisits never change it, so the graph is always a tree.

const DELETED = ' deleted'; // content key shared by every deleted state

/**
 * Walk the whole revision log ONCE into a per-path history, ascending by
 * revision. Cheap to slice per file; memoize on the log at the call site.
 */
export function buildFileHistoryIndex(revisions) {
  const index = new Map();
  const ordered = [...(revisions ?? [])].sort((a, b) => a.revision - b.revision);
  for (const entry of ordered) {
    for (const change of entry.changes) {
      let list = index.get(change.path);
      if (!list) {
        list = [];
        index.set(change.path, list);
      }
      list.push({
        revision: entry.revision,
        change: change.change,
        hash: change.change === 'deleted' ? null : change.hash,
        author: entry.author,
        date: entry.date,
        description: entry.description,
      });
    }
  }
  return index;
}

function keyOf(historyEntry) {
  return historyEntry.change === 'deleted' ? DELETED : historyEntry.hash;
}

/**
 * Build the version graph for one file's history (ascending by revision).
 * serverHeadHash: current server content hash, or null if deleted at head —
 *   selects the highlighted "current" node. syncedHash: hash the local copy was
 *   last synced at (or null/undefined) — selects the "local" node.
 */
export function buildVersionGraph(fileHistory, { serverHeadHash = null, syncedHash = null } = {}) {
  const nodes = [];
  const nodesByKey = new Map();
  let current = null;
  let version = 0;

  for (const h of fileHistory ?? []) {
    const key = keyOf(h);
    let node = nodesByKey.get(key);
    if (!node) {
      version += 1;
      node = {
        version,
        key,
        // Distance from the root (root = 0). The card shows depth + 1, so
        // sibling branches share a number — the "am I on the wrong path?"
        // ambiguity is deliberate. `version` stays a unique internal id.
        depth: current ? current.depth + 1 : 0,
        firstRevision: h.revision,
        change: h.change,
        author: h.author,
        date: h.date,
        description: h.description,
        parent: current ? current.version : null,
        children: [],
        isCurrent: false,
        isLocal: false,
        isDead: false,
        x: 0,
        y: 0,
      };
      nodesByKey.set(key, node);
      nodes.push(node);
      if (current) current.children.push(version);
    }
    current = node;
  }

  const byVersion = new Map(nodes.map((n) => [n.version, n]));
  const serverKey = serverHeadHash == null ? DELETED : serverHeadHash;
  const currentNode = nodesByKey.get(serverKey) ?? null;
  const localNode = syncedHash == null ? null : nodesByKey.get(syncedHash) ?? null;

  if (currentNode) {
    currentNode.isCurrent = true;
    const alive = new Set();
    for (let n = currentNode; n; n = n.parent != null ? byVersion.get(n.parent) : null) {
      alive.add(n.version);
    }
    for (const n of nodes) n.isDead = !alive.has(n.version);
  }
  if (localNode) localNode.isLocal = true;

  return {
    nodes,
    currentVersion: currentNode ? currentNode.version : null,
    localVersion: localNode ? localNode.version : null,
  };
}

/**
 * Fill grid coords: y = depth, x = column (fractional for parents centered over
 * their subtree). Children are already ascending by version, so older branches
 * land left. Pixel spacing is the renderer's job.
 */
export function layoutVersionGraph(nodes) {
  if (!nodes.length) return nodes;
  const byVersion = new Map(nodes.map((n) => [n.version, n]));

  function subtreeWidth(node) {
    if (!node.children.length) return 1;
    return node.children.reduce((sum, c) => sum + subtreeWidth(byVersion.get(c)), 0);
  }
  function place(node, x0, depth) {
    node.y = depth;
    if (!node.children.length) {
      node.x = x0;
      return;
    }
    let cx = x0;
    for (const c of node.children) {
      const child = byVersion.get(c);
      place(child, cx, depth + 1);
      cx += subtreeWidth(child);
    }
    const first = byVersion.get(node.children[0]);
    const last = byVersion.get(node.children[node.children.length - 1]);
    node.x = (first.x + last.x) / 2;
  }

  place(nodes[0], 0, 0); // version 1 = first state = single root
  return nodes;
}

/**
 * Tree filename fraction: the version the local copy has over the version the
 * server head is on. Returns { your, server } or null when there is nothing
 * meaningful to show (no history, or either hash matches no node).
 */
export function versionFraction(fileHistory, serverHeadHash, syncedHash) {
  if (!fileHistory || !fileHistory.length) return null;
  const { nodes, currentVersion, localVersion } = buildVersionGraph(fileHistory, {
    serverHeadHash,
    syncedHash,
  });
  if (currentVersion == null || localVersion == null) return null;
  // The displayed number is generations-from-root (depth + 1), matching the
  // graph cards — not the internal creation-order `version`.
  const byVersion = new Map(nodes.map((n) => [n.version, n]));
  return {
    your: byVersion.get(localVersion).depth + 1,
    server: byVersion.get(currentVersion).depth + 1,
  };
}
