namespace EnvelopeRenderer.Cli.Render;
///
/// Pure geometry for Sprint 4's "Collapse blank optional address lines consistently": given a
/// set of text lines (each with an X/Y position and whether it should collapse for this
/// particular record), decides which lines are visible and where the visible ones land after
/// closing the gaps left by any collapsed lines above them.
///
/// Grouping rule (a Development Team design decision — the template format has no
/// explicit "address block" concept): lines that share the same X position (rounded to 2
/// decimal places, matching the precision the designer's properties panel and XML persistence
/// already use) are treated as one vertical stack — i.e. one address block — ordered top-to-
/// bottom by descending Y (PDF points increase upward, so the top line of a block has the
/// highest Y). Lines at a different X never affect each other's collapsing math. This mirrors
/// how a real address block is authored today: every line in it shares the same left margin.
///
/// Shift math: for a stack ordered top-to-bottom, line k's "row height" is defined
/// as the gap between it and the line immediately below it, i.e. Y[k] - Y[k+1] (using the
/// stack's original, undisturbed Y values — never an already-shifted one, so consecutive
/// collapsed lines don't compound rounding error). When line k collapses, every line below it in
/// the same stack shifts upward by that row height. The last line in a stack has no line below
/// it, so its own row height is irrelevant (nothing shifts if it collapses) — a deliberate
/// no-op, not a bug.
///
public static class AddressLineCollapser
{
/// One input line: its original template position and whether this particular
/// record's resolved value makes it collapse (i.e. the caller has already combined "is this
/// element configured as collapsible" with "is its resolved text blank for this record").
public readonly record struct Line(double X, double Y, bool ShouldCollapse);
/// One resolved line, same order/count as the input. is
/// false exactly for lines whose own was true — the
/// caller should skip drawing those. is the Y every line (visible or
/// not) should be treated as occupying after any collapsed lines above it in its stack have
/// closed their gaps.
public readonly record struct Resolved(bool Visible, double EffectiveY);
/// Rounding precision (decimal places) used to decide whether two lines share the
/// same X position and therefore belong to the same address-line stack. 2 decimal places
/// matches the precision already used by the designer's XML persistence and properties
/// panel, so two lines placed at "the same" X via the UI reliably group together even after a
/// save/reload round trip through floating point.
private const int GroupingPrecision = 2;
public static IReadOnlyList Resolve(IReadOnlyList lines)
{
var result = new Resolved[lines.Count];
var groups = lines
.Select((line, index) => (Line: line, Index: index))
.GroupBy(t => Math.Round(t.Line.X, GroupingPrecision));
foreach (var group in groups)
{
var stack = group.OrderByDescending(t => t.Line.Y).ToList();
var cumulativeShift = 0.0;
for (var i = 0; i < stack.Count; i++)
{
var (line, index) = stack[i];
result[index] = new Resolved(!line.ShouldCollapse, line.Y + cumulativeShift);
if (line.ShouldCollapse && i < stack.Count - 1)
{
var nextY = stack[i + 1].Line.Y;
cumulativeShift += line.Y - nextY;
}
}
}
return result;
}
}