namespace StellaOps.ElkSharp; internal sealed class ElkCompoundHierarchy { private const string RootKey = "\u0001__root__"; private readonly Dictionary nodesById; private readonly Dictionary parentByNodeId; private readonly Dictionary> childrenByParentId; private readonly Dictionary ancestorsNearestFirstByNodeId; private readonly Dictionary depthByNodeId; private readonly Dictionary originalOrderByNodeId; private ElkCompoundHierarchy( Dictionary nodesById, Dictionary parentByNodeId, Dictionary> childrenByParentId, Dictionary ancestorsNearestFirstByNodeId, Dictionary depthByNodeId, Dictionary originalOrderByNodeId) { this.nodesById = nodesById; this.parentByNodeId = parentByNodeId; this.childrenByParentId = childrenByParentId; this.ancestorsNearestFirstByNodeId = ancestorsNearestFirstByNodeId; this.depthByNodeId = depthByNodeId; this.originalOrderByNodeId = originalOrderByNodeId; } internal bool HasCompoundNodes => nodesById.Keys.Any(IsCompoundNode); internal IEnumerable NodeIds => nodesById.Keys; internal IEnumerable NonLeafNodeIds => nodesById.Keys.Where(IsCompoundNode); internal static ElkCompoundHierarchy Build(IReadOnlyCollection nodes) { var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal); var originalOrderByNodeId = nodes .Select((node, index) => new KeyValuePair(node.Id, index)) .ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal); var parentByNodeId = new Dictionary(StringComparer.Ordinal); var childrenByParentId = new Dictionary>(StringComparer.Ordinal) { [RootKey] = [], }; foreach (var node in nodes) { var parentNodeId = string.IsNullOrWhiteSpace(node.ParentNodeId) ? null : node.ParentNodeId.Trim(); if (parentNodeId is not null && !nodesById.ContainsKey(parentNodeId)) { throw new InvalidOperationException( $"Node '{node.Id}' references unknown parent '{parentNodeId}'."); } if (string.Equals(parentNodeId, node.Id, StringComparison.Ordinal)) { throw new InvalidOperationException( $"Node '{node.Id}' cannot be its own parent."); } parentByNodeId[node.Id] = parentNodeId; var key = parentNodeId ?? RootKey; if (!childrenByParentId.TryGetValue(key, out var children)) { children = []; childrenByParentId[key] = children; } children.Add(node.Id); } foreach (var children in childrenByParentId.Values) { children.Sort((left, right) => originalOrderByNodeId[left].CompareTo(originalOrderByNodeId[right])); } foreach (var node in nodes) { var visited = new HashSet(StringComparer.Ordinal) { node.Id }; var currentNodeId = node.Id; while (parentByNodeId[currentNodeId] is { } parentNodeId) { if (!visited.Add(parentNodeId)) { throw new InvalidOperationException( $"Containment cycle detected while resolving parent chain for '{node.Id}'."); } currentNodeId = parentNodeId; } } var ancestorsNearestFirstByNodeId = new Dictionary(StringComparer.Ordinal); var depthByNodeId = new Dictionary(StringComparer.Ordinal); foreach (var node in nodes) { ResolveAncestors(node.Id); } return new ElkCompoundHierarchy( nodesById, parentByNodeId, childrenByParentId, ancestorsNearestFirstByNodeId, depthByNodeId, originalOrderByNodeId); string[] ResolveAncestors(string nodeId) { if (ancestorsNearestFirstByNodeId.TryGetValue(nodeId, out var cachedAncestors)) { return cachedAncestors; } if (parentByNodeId[nodeId] is not { } parentNodeId) { depthByNodeId[nodeId] = 0; cachedAncestors = []; } else { var parentAncestors = ResolveAncestors(parentNodeId); cachedAncestors = [parentNodeId, .. parentAncestors]; depthByNodeId[nodeId] = depthByNodeId[parentNodeId] + 1; } ancestorsNearestFirstByNodeId[nodeId] = cachedAncestors; return cachedAncestors; } } internal bool IsCompoundNode(string nodeId) => childrenByParentId.TryGetValue(nodeId, out var children) && children.Count > 0; internal bool IsLeafNode(string nodeId) => !IsCompoundNode(nodeId); internal bool IsLayoutVisibleNode(string nodeId) => IsLeafNode(nodeId); internal bool ContainsNode(string nodeId) => nodesById.ContainsKey(nodeId); internal int GetDepth(string nodeId) => depthByNodeId[nodeId]; internal int GetOriginalOrder(string nodeId) => originalOrderByNodeId[nodeId]; internal string? GetParentNodeId(string nodeId) => parentByNodeId[nodeId]; internal IReadOnlyList GetChildIds(string? parentNodeId) { var key = parentNodeId ?? RootKey; return childrenByParentId.TryGetValue(key, out var children) ? children : []; } internal IReadOnlyList GetAncestorIdsNearestFirst(string nodeId) => ancestorsNearestFirstByNodeId[nodeId]; internal IEnumerable GetNonLeafNodeIdsByDescendingDepth() => NonLeafNodeIds .OrderByDescending(GetDepth) .ThenBy(GetOriginalOrder); internal string? GetLowestCommonAncestor(string leftNodeId, string rightNodeId) { var leftAncestors = new HashSet(GetAncestorIdsNearestFirst(leftNodeId), StringComparer.Ordinal); foreach (var ancestorNodeId in GetAncestorIdsNearestFirst(rightNodeId)) { if (leftAncestors.Contains(ancestorNodeId)) { return ancestorNodeId; } } return null; } }