namespace EnvelopeRenderer.Cli.Render;
///
/// One `<text>` element from the template. Sprint 5, "Mix static text and CSV fields within
/// a single text element": content is now an ordered sequence of s
/// (literal-text runs and field-token runs), concatenated per record at render time — the
/// pre-Sprint-5 "either pure static text or a single bound column" rule is simply the one-run
/// special case of this same model (/ below are
/// back-compat read-only projections for that one-run case, not separate storage), not a
/// parallel code path that could drift from it. enforces "every
/// run is exactly one of literal/field" at parse time.
///
/// Sprint 4, "Collapse blank optional address lines consistently":
/// when true and this element's resolved text is blank for a given record, the element
/// is not drawn and every other element sharing its X position (an "address line stack" — see
/// ) shifts upward to close the gap. Defaults to false
/// so every template written before this story renders identically (a blank line simply prints
/// nothing at its fixed position, exactly as before).
/// Sprint 4, "Set a rotation angle for text and dynamic field elements":
/// degrees, positive or negative, applied around the element's own bounding-box center (not its
/// X/Y anchor). Defaults to 0 so every template written before this story renders
/// unchanged.
/// Sprint 9, "Add an adjustable width and height with text wrapping...":
/// when set (together with ), the element's content wraps within this
/// width instead of drawing as one unbounded line. null (the default) means every
/// template written before this story renders exactly as before — no wrap, no width concept at
/// all.
/// Paired with : wrapped content that would exceed
/// this height is clipped rather than growing the box or shrinking the text. Only meaningful when
/// is also set; null is the pre-Sprint-9 default.
public sealed record TemplateElement(
double X,
double Y,
string FontName,
double Size,
IReadOnlyList Runs,
bool Collapsible = false,
double Angle = 0,
int RenderOrder = 0,
double? Width = null,
double? Height = null)
{
/// True only when both and are set — a
/// one-sided value (e.g. a width with no height) is treated as "no box" rather than guessing
/// a missing dimension, since wrap-without-a-clip-boundary or clip-without-a-wrap-width both
/// have no well-defined meaning for this story.
public bool HasBox => Width is > 0 && Height is > 0;
/// Back-compat convenience factory matching the pre-Sprint-5 "pure static text"
/// shape — builds the equivalent one-literal-run list. (A second
/// constructor overload was deliberately rejected here in favor of named factory methods: a
/// positional/named-argument overload alongside the primary constructor's own
/// Collapsible/Angle parameters is ambiguous to the compiler — CS1744 — for any
/// caller using named arguments for either, which every Collapsible: true/
/// Angle: 45 call site in this codebase does.)
public static TemplateElement Static(
double x, double y, string fontName, double size, string text, bool collapsible = false, double angle = 0) =>
new(x, y, fontName, size, new[] { new TemplateTextRun(text, null) }, collapsible, angle);
/// Back-compat convenience factory matching the pre-Sprint-5 "single bound column"
/// shape — builds the equivalent one-field-run list.
public static TemplateElement Dynamic(
double x, double y, string fontName, double size, string columnName, bool collapsible = false, double angle = 0) =>
new(x, y, fontName, size, new[] { new TemplateTextRun(null, columnName) }, collapsible, angle);
public bool IsDynamic => Runs.Any(r => r.IsField);
/// True only for the legacy "exactly one field run, nothing else" shape — the only
/// case that still has a single, unambiguous bound column (used by the render-time pre-flight
/// column check's error-message grouping and by any code that only makes sense for a
/// single-column binding).
public bool HasSingleColumnRun => Runs.Count == 1 && Runs[0].IsField;
/// Back-compat projection: non-null only for the legacy single-literal-run case.
public string? StaticText => Runs.Count == 1 && !Runs[0].IsField ? Runs[0].Literal : null;
/// Back-compat projection: non-null only for the legacy single-field-run case.
public string? ColumnName => HasSingleColumnRun ? Runs[0].ColumnName : null;
}