using ClosedXML.Excel; using FactorioCalc.Domain; namespace FactorioCalc.Reporting; /// /// Exports production results to an Excel (.xlsx) file. /// public sealed class ExcelReporter : IReporter { private readonly string _outputPath; public ExcelReporter(string outputPath) { _outputPath = outputPath ?? throw new ArgumentNullException(nameof(outputPath)); } public void Report(ProductionResult result, IReadOnlyDictionary resources, IReadOnlyDictionary recipes, IReadOnlyDictionary machines) { using var workbook = new XLWorkbook(); // Sheet 1: Recipe Executions var execSheet = workbook.Worksheets.Add("Executions"); execSheet.Cell(1, 1).Value = "Recipe"; execSheet.Cell(1, 2).Value = "Rate (cycles/sec)"; execSheet.Cell(1, 3).Value = "Machine"; execSheet.Cell(1, 4).Value = "Machine Count"; execSheet.Cell(1, 5).Value = "Effective Speed"; execSheet.Cell(1, 6).Value = "Effective Productivity"; execSheet.Cell(1, 7).Value = "Modules"; var execHeader = execSheet.Range(1, 1, 1, 7); execHeader.Style.Font.Bold = true; execHeader.Style.Fill.SetBackgroundColor(XLColor.LightBlue); int execRow = 2; foreach (var exec in result.Executions) { var recipeName = recipes.TryGetValue(exec.RecipeId, out var r) ? r.Name : $"Recipe#{exec.RecipeId}"; var machineName = machines.TryGetValue(exec.MachineId, out var m) ? m.Name : $"Machine#{exec.MachineId}"; var modulesStr = string.Join(", ", exec.Modules.Select(m => m.Name)); execSheet.Cell(execRow, 1).Value = recipeName; execSheet.Cell(execRow, 2).Value = exec.RecipeRate; execSheet.Cell(execRow, 3).Value = machineName; execSheet.Cell(execRow, 4).Value = exec.MachineCount; execSheet.Cell(execRow, 5).Value = exec.EffectiveSpeed; execSheet.Cell(execRow, 6).Value = exec.EffectiveProductivity; execSheet.Cell(execRow, 7).Value = modulesStr; execRow++; } execSheet.Columns().AdjustToContents(); // Sheet 2: Resource Flows var flowSheet = workbook.Worksheets.Add("Resource Flows"); flowSheet.Cell(1, 1).Value = "Resource"; flowSheet.Cell(1, 2).Value = "Flow (items/sec)"; flowSheet.Cell(1, 3).Value = "Flow (items/min)"; var flowHeader = flowSheet.Range(1, 1, 1, 3); flowHeader.Style.Font.Bold = true; flowHeader.Style.Fill.SetBackgroundColor(XLColor.LightGreen); int flowRow = 2; foreach (var (resourceId, flow) in result.ResourceFlows.OrderBy(k => k.Value)) { var resourceName = resources.TryGetValue(resourceId, out var res) ? res.Name : $"Resource#{resourceId}"; flowSheet.Cell(flowRow, 1).Value = resourceName; flowSheet.Cell(flowRow, 2).Value = flow; flowSheet.Cell(flowRow, 3).Value = flow * 60; flowRow++; } flowSheet.Columns().AdjustToContents(); // Sheet 3: Required Inputs var inputSheet = workbook.Worksheets.Add("Required Inputs"); inputSheet.Cell(1, 1).Value = "Resource"; inputSheet.Cell(1, 2).Value = "items/sec"; inputSheet.Cell(1, 3).Value = "items/min"; var inputHeader = inputSheet.Range(1, 1, 1, 3); inputHeader.Style.Font.Bold = true; inputHeader.Style.Fill.SetBackgroundColor(XLColor.Orange); int inputRow = 2; foreach (var (resourceId, amount) in result.RequiredInputs) { var resourceName = resources.TryGetValue(resourceId, out var res) ? res.Name : $"Resource#{resourceId}"; inputSheet.Cell(inputRow, 1).Value = resourceName; inputSheet.Cell(inputRow, 2).Value = amount; inputSheet.Cell(inputRow, 3).Value = amount * 60; inputRow++; } inputSheet.Columns().AdjustToContents(); workbook.SaveAs(_outputPath); Console.WriteLine($" [Excel] Report saved to: {_outputPath}"); } }