using System; using System.Collections.Generic; using System.Linq; using FlowAPI.Application.DTOs.AI; using FlowAPI.Application.DTOs.Graph; namespace FlowAPI.Application.Services { public static class AutoLayoutEngine { private const double StepX = 300.0; private const double StepY = 180.0; public static FullGraphResponseDto LayoutGraph(QuestSchemaDto schema, Guid userId) { var graphId = Guid.NewGuid(); var response = new FullGraphResponseDto { Id = graphId, UserId = userId, Title = schema.Title, Description = schema.Description, CreatedAt = DateTime.UtcNow, IsShared = false, Permission = "Owner", Nodes = new List(), Edges = new List() }; if (schema.Nodes == null || !schema.Nodes.Any()) return response; // 1. Map string string-based IDs from LLM into Guids var idMap = new Dictionary(); foreach (var node in schema.Nodes) { idMap[node.Id] = Guid.NewGuid(); } // 2. Calculate logical layers (Sugiyama-like ranking) var layerMap = new Dictionary(); foreach (var node in schema.Nodes) { layerMap[node.Id] = 0; // Default layer } // Transitively propagate layer ranks based on dependencies (max N passes) int n = schema.Nodes.Count; for (int pass = 0; pass < n; pass++) { bool changed = false; foreach (var node in schema.Nodes) { if (!string.IsNullOrEmpty(node.DependsOn) && idMap.ContainsKey(node.DependsOn)) { int parentLayer = layerMap[node.DependsOn]; int currentLayer = layerMap[node.Id]; if (currentLayer < parentLayer + 1) { layerMap[node.Id] = parentLayer + 1; changed = true; } } } if (!changed) break; // Optimization if stabilized early } // 3. Group nodes by layers and calculate coordinates var layers = new Dictionary>(); foreach (var node in schema.Nodes) { int layer = layerMap[node.Id]; if (!layers.ContainsKey(layer)) { layers[layer] = new List(); } layers[layer].Add(node); } // Set coordinates symmetrically per layer foreach (var layerKvp in layers) { int layer = layerKvp.Key; var layerNodes = layerKvp.Value; int count = layerNodes.Count; for (int i = 0; i < count; i++) { var node = layerNodes[i]; var nodeGuid = idMap[node.Id]; // Symmetric distribution horizontally around X = 0, and sequential progression top-to-bottom double posX = (i - (count - 1) / 2.0) * StepX; double posY = layer * StepY; // Determine visual properties string? nodeColor = node.Color; if (string.IsNullOrEmpty(nodeColor) && !string.IsNullOrEmpty(node.Condition)) { if (node.Condition.Equals("Fail", StringComparison.OrdinalIgnoreCase)) { nodeColor = "pink"; } else if (node.Condition.Equals("Success", StringComparison.OrdinalIgnoreCase)) { nodeColor = "green"; } } // Map Status string to integer State Enum value (Todo = 0, In Progress = 1, Review = 2, Done = 3) int stateVal = 0; if (!string.IsNullOrEmpty(node.Status)) { if (node.Status.Equals("Done", StringComparison.OrdinalIgnoreCase)) stateVal = 3; else if (node.Status.Equals("Review", StringComparison.OrdinalIgnoreCase)) stateVal = 2; else if (node.Status.Equals("In Progress", StringComparison.OrdinalIgnoreCase) || node.Status.Equals("Progress", StringComparison.OrdinalIgnoreCase)) stateVal = 1; } // Build rich data object for serialization into Decision column var decisionObj = new Dictionary(); decisionObj["decision"] = node.Condition; decisionObj["description"] = node.Description ?? ""; decisionObj["progress"] = node.Progress ?? 0; decisionObj["showProgress"] = node.ShowProgress ?? false; decisionObj["color"] = nodeColor ?? "default"; decisionObj["isPinned"] = node.IsPinned ?? false; decisionObj["tags"] = node.Tags ?? new List(); decisionObj["status"] = node.Status ?? "Todo"; decisionObj["assignees"] = node.Assignees ?? new List(); if (node.Subgoals != null) { decisionObj["subgoals"] = new { total = node.Subgoals.Total, done = node.Subgoals.Done }; } if (node.Options != null) { decisionObj["options"] = node.Options.Select(o => new { id = o.Id, label = o.Label }).ToList(); } if (node.SelectedOptions != null) { decisionObj["selectedOptions"] = node.SelectedOptions; } if (node.MultiSelect != null) { decisionObj["multiSelect"] = node.MultiSelect.Value; } if (node.ResetRule != null) { decisionObj["resetRule"] = node.ResetRule; } if (!string.IsNullOrEmpty(node.Url)) { decisionObj["url"] = node.Url; } // For image/path/text nodes, save payload inside Data if needed string? nodeData = null; if (node.Type == "image" && !string.IsNullOrEmpty(node.Url)) { nodeData = node.Url; } string decisionJson = System.Text.Json.JsonSerializer.Serialize(decisionObj); response.Nodes.Add(new NodeDto { Id = nodeGuid, Label = node.Label, PosX = posX, PosY = posY, Type = node.Type ?? "sketch", State = stateVal, Decision = decisionJson, // JSON string matches frontend expected parsing! Description = node.Description, Color = nodeColor, IsPinned = node.IsPinned ?? false, Width = node.Width ?? 220, Height = node.Height ?? 116, Data = nodeData }); // 4. Create Edges if (!string.IsNullOrEmpty(node.DependsOn) && idMap.ContainsKey(node.DependsOn)) { response.Edges.Add(new EdgeDto { Id = Guid.NewGuid(), FromNodeId = idMap[node.DependsOn], ToNodeId = nodeGuid, Condition = node.Condition ?? "True" }); } } } return response; } } }