using System;
using System.Collections.Generic;
using MergeHub.Models;
namespace MergeHub.Services
{
///
/// 커밋 목록에 레인을 배정해 브랜치 그래프를 만든다.
///
/// 원리: "레인"은 아직 그려지지 않은 부모 커밋 하나를 기다리는 세로줄이다.
/// 목록을 위에서 아래로 훑으면서
/// 1) 지금 커밋을 기다리던 레인들을 찾아 그 중 왼쪽 것을 이 커밋의 레인으로 삼고,
/// 2) 나머지는 그 레인으로 합류시키고(사선),
/// 3) 첫 부모는 같은 레인이 이어받고, 두 번째 부모부터는 새 레인으로 분기시킨다.
/// 커밋은 이미 시간순(topological)으로 정렬되어 들어온다는 전제다.
///
public static class CommitGraphBuilder
{
/// 레인 색은 이 개수로 순환한다. 실제 색상값은 화면(CommitGraphCell)이 정한다.
public const int ColorCount = 6;
public static IReadOnlyList Build(IReadOnlyList commits)
{
var nodes = new List(commits.Count);
// lanes[i] = 그 레인이 기다리는 커밋 SHA (없으면 null)
var lanes = new List();
var laneColors = new List();
var maxLanes = 1;
var nextColor = 0;
foreach (var commit in commits)
{
// 이 행에 "위에서 내려온" 상태를 먼저 찍어둔다.
var top = new List(lanes);
var myLanes = new List();
for (var i = 0; i < lanes.Count; i++)
if (lanes[i] == commit.Sha) myLanes.Add(i);
int lane;
if (myLanes.Count == 0)
{
// 아무도 기다리지 않던 커밋 = 새 가지의 시작(브랜치 팁)
lane = TakeFreeLane(lanes, laneColors, ref nextColor);
lanes[lane] = commit.Sha;
myLanes.Add(lane);
}
else
{
lane = myLanes[0];
}
var colorIndex = laneColors[lane];
// 이 커밋을 기다리던 레인은 전부 비운다.
foreach (var i in myLanes) lanes[i] = null;
// 첫 부모는 같은 레인이 이어받고, 나머지는 새 레인으로 분기한다.
for (var p = 0; p < commit.ParentShas.Count; p++)
{
var parent = commit.ParentShas[p];
if (p == 0)
{
lanes[lane] = parent;
continue;
}
// 이미 그 부모를 기다리는 레인이 있으면 그리로 합친다.
var existing = lanes.IndexOf(parent);
if (existing >= 0) continue;
var branchLane = TakeFreeLane(lanes, laneColors, ref nextColor);
lanes[branchLane] = parent;
}
var edges = new List();
// (1) 위에서 내려온 선들
for (var i = 0; i < top.Count; i++)
{
if (top[i] is null) continue;
if (myLanes.Contains(i))
edges.Add(new GraphEdge(i, lane, laneColors[i])); // 이 커밋으로 합류
else if (i < lanes.Count && lanes[i] is not null)
edges.Add(new GraphEdge(i, i, laneColors[i])); // 그냥 통과
}
// (2) 이 커밋에서 새로 시작하는 선들 (첫 부모의 수직 연장 + 분기)
for (var i = 0; i < lanes.Count; i++)
{
if (lanes[i] is null) continue;
// 위에서 이미 내려온 레인은 (1)에서 그렸다.
if (i < top.Count && top[i] is not null && !myLanes.Contains(i)) continue;
// 이 커밋의 레인이 위에서도 이어져 왔다면 (1)의 i→lane 선과 겹친다.
if (i == lane && lane < top.Count && top[lane] is not null) continue;
edges.Add(new GraphEdge(lane, i, laneColors[i]));
}
if (lanes.Count > maxLanes) maxLanes = lanes.Count;
if (top.Count > maxLanes) maxLanes = top.Count;
nodes.Add(new CommitNode(commit, new GraphRow(lane, colorIndex, edges, commit.IsMerge)));
}
// 열 너비를 전체에서 가장 넓은 행에 맞춘다.
foreach (var node in nodes) node.Graph.LaneCount = maxLanes;
return nodes;
}
/// 비어 있는 레인을 재사용하고, 없으면 하나 늘린다.
private static int TakeFreeLane(List lanes, List laneColors, ref int nextColor)
{
for (var i = 0; i < lanes.Count; i++)
{
if (lanes[i] is not null) continue;
laneColors[i] = nextColor++ % ColorCount;
return i;
}
lanes.Add(null);
laneColors.Add(nextColor++ % ColorCount);
return lanes.Count - 1;
}
}
}