using System.Drawing;
using System.Windows.Forms;
using EnvelopeRenderer.Desktop.Core.Launch;
using EnvelopeRenderer.Desktop.Views;
namespace EnvelopeRenderer.Desktop;
///
/// Operator-facing shell: pick a template, CSV, and output PDF path, then launch a render and
/// watch it through to completion. Sprint 1, Batch 4 ("Launch a text-only render from the
/// desktop app") proved the process could be launched without freezing the UI; Batch 5 ("Show
/// render progress and completion summary") adds reading the CLI's `PROGRESS` stream while it
/// runs, showing a non-technical completion summary once it exits, and — since the button is now
/// only re-enabled after the process actually exits rather than as soon as it starts — closes the
/// double-launch gap previously logged in logs/technical_debt_log.md.
///
/// All non-UI logic (input validation, argument construction, CLI discovery, process launch,
/// progress parsing, and summary formatting) lives in EnvelopeRenderer.Desktop.Core.Launch and is
/// unit tested there — this class is intentionally thin event-handler wiring over that logic.
///
public sealed class MainForm : Form
{
private readonly TextBox _templatePathTextBox = new();
private readonly TextBox _csvPathTextBox = new();
private readonly TextBox _outputPathTextBox = new();
private readonly Button _renderButton = new() { Text = "&Render", AutoSize = true };
private readonly Button _designTemplateButton = new() { Text = "&Design Template...", AutoSize = true };
private readonly Label _statusLabel = new()
{
AutoSize = false,
Height = 48,
TextAlign = ContentAlignment.TopLeft,
};
private readonly CliProcessLauncher _launcher;
public MainForm() : this(new CliProcessLauncher())
{
}
/// Internal constructor allowing a launcher to be injected (used by tests/composition).
internal MainForm(CliProcessLauncher launcher)
{
_launcher = launcher;
Text = "Envelope Renderer";
MinimumSize = new Size(640, 260);
StartPosition = FormStartPosition.CenterScreen;
Controls.Add(BuildLayout());
}
private Control BuildLayout()
{
var layout = new TableLayoutPanel
{
Dock = DockStyle.Fill,
ColumnCount = 3,
RowCount = 5,
Padding = new Padding(12),
AutoSize = true,
};
layout.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
layout.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
AddPickerRow(
layout,
row: 0,
labelText: "Template (XML):",
textBox: _templatePathTextBox,
browse: () => BrowseForOpenFile(_templatePathTextBox, "XML template files (*.xml)|*.xml|All files (*.*)|*.*"));
AddPickerRow(
layout,
row: 1,
labelText: "CSV data:",
textBox: _csvPathTextBox,
browse: () => BrowseForOpenFile(_csvPathTextBox, "CSV files (*.csv)|*.csv|All files (*.*)|*.*"));
AddPickerRow(
layout,
row: 2,
labelText: "Output PDF:",
textBox: _outputPathTextBox,
browse: () => BrowseForSaveFile(_outputPathTextBox, "PDF files (*.pdf)|*.pdf|All files (*.*)|*.*"));
_renderButton.Click += OnRenderClick;
_designTemplateButton.Click += (_, _) => OnDesignTemplateClick();
var buttonPanel = new FlowLayoutPanel { Dock = DockStyle.Fill, FlowDirection = FlowDirection.RightToLeft, AutoSize = true };
buttonPanel.Controls.Add(_renderButton);
buttonPanel.Controls.Add(_designTemplateButton);
layout.Controls.Add(buttonPanel, 1, 3);
layout.SetColumnSpan(buttonPanel, 2);
_statusLabel.Dock = DockStyle.Fill;
layout.Controls.Add(_statusLabel, 0, 4);
layout.SetColumnSpan(_statusLabel, 3);
return layout;
}
private static void AddPickerRow(TableLayoutPanel layout, int row, string labelText, TextBox textBox, Action browse)
{
var label = new Label
{
Text = labelText,
AutoSize = true,
Anchor = AnchorStyles.Left,
Margin = new Padding(3, 9, 3, 3),
};
textBox.Dock = DockStyle.Fill;
textBox.Margin = new Padding(3, 6, 3, 3);
var browseButton = new Button { Text = "Browse...", AutoSize = true, Margin = new Padding(3, 3, 3, 3) };
browseButton.Click += (_, _) => browse();
layout.Controls.Add(label, 0, row);
layout.Controls.Add(textBox, 1, row);
layout.Controls.Add(browseButton, 2, row);
}
private static void BrowseForOpenFile(TextBox target, string filter)
{
using var dialog = new OpenFileDialog { Filter = filter, CheckFileExists = true, Title = "Select a file" };
if (dialog.ShowDialog() == DialogResult.OK)
{
target.Text = dialog.FileName;
}
}
private static void BrowseForSaveFile(TextBox target, string filter)
{
using var dialog = new SaveFileDialog { Filter = filter, OverwritePrompt = false, Title = "Choose where to save the PDF" };
if (dialog.ShowDialog() == DialogResult.OK)
{
target.Text = dialog.FileName;
}
}
private async void OnRenderClick(object? sender, EventArgs e)
{
var inputs = new RenderLaunchInputs(_templatePathTextBox.Text, _csvPathTextBox.Text, _outputPathTextBox.Text);
var validation = RenderLaunchValidator.Validate(inputs);
if (!validation.IsValid)
{
SetStatus(string.Join(" ", validation.Errors), isError: true);
return;
}
var cliPath = CliExecutablePathResolver.Resolve(AppContext.BaseDirectory);
if (!cliPath.IsFound)
{
SetStatus(cliPath.Error!, isError: true);
ShowLaunchFailure(cliPath.Error!);
return;
}
// Disabled here and only re-enabled once RunAsync's awaited Task completes below (i.e.
// once the child process has actually exited) — not the moment it starts — so an
// operator can't launch a second render against the same output while one is in flight.
_renderButton.Enabled = false;
SetStatus("Starting render...", isError: false);
try
{
var arguments = CliArgumentListBuilder.Build(inputs);
// Constructed on the UI thread so Progress captures this thread's
// SynchronizationContext and marshals each Report() call back to it automatically —
// OnRenderProgress can update controls directly without a manual Invoke.
var progress = new Progress(OnRenderProgress);
var result = await _launcher.RunAsync(cliPath.Path!, arguments, progress);
var summary = RenderCompletionSummaryFormatter.Format(result);
if (result.Succeeded)
{
SetStatus(summary, isError: false);
}
else if (!result.Started)
{
// A launch failure (the process never started) keeps the message-box treatment
// from Batch 4 — it means something is wrong with the desktop app's own setup,
// not with the operator's data, and deserves an explicit acknowledgement.
SetStatus(summary, isError: true);
ShowLaunchFailure(summary);
}
else
{
SetStatus(summary, isError: true);
}
}
finally
{
_renderButton.Enabled = true;
}
}
private void OnDesignTemplateClick()
{
using var designer = new TemplateDesignerForm();
designer.ShowDialog(this);
}
private void OnRenderProgress(ProgressEvent progressEvent)
{
SetStatus(RenderProgressStatusFormatter.Format(progressEvent), isError: progressEvent.Kind == ProgressEventKind.Failure);
}
private void ShowLaunchFailure(string message)
{
MessageBox.Show(this, message, "Could not start render", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
private void SetStatus(string message, bool isError)
{
_statusLabel.Text = message;
_statusLabel.ForeColor = isError ? Color.Firebrick : Color.DarkGreen;
}
}