using System;
using System.Collections.Generic;
using System.Numerics;
namespace AcDream.App.UI.Layout;
///
/// Shared multi-segment rich-text composer for the chargen description
/// boxes (Campaign CC gate round 1 Batch C — GF-2/GF-3/GF-11a, and the
/// Summary how-to text). Ports retail's
/// UIElement_Text::SetStringInfoWithFont /
/// AppendStringInfoWithFont @ 0x00469D70 composition model: a text
/// box is built from an ORDERED list of string segments, each carrying its
/// OWN font-color palette index
/// (UIElement_Text::AppendStringInfoWithFont's
/// SetFontColorHelper -> InqProperty(0x1B) array lookup —
/// see ).
///
///
/// The description pages used to bypass this entirely: they assigned a raw
/// LinesProvider lambda returning ONE unwrapped
/// per composed string, with no word-wrap. Historical symptom (Batch C):
/// for the Town page an unwrapped single line meant the town-specific
/// SUFFIX of the composed string rendered far outside the box's clipped
/// viewport, so switching towns looked like "the text never changes" even
/// though the underlying string genuinely did (only its INVISIBLE tail
/// differed). Escape decoding (the DAT's literal two-character "\n",
/// Batch C's other symptom) has since moved to the string source
/// ( → ,
/// the 2026-08-17 systemic round) — segments reach this composer with real
/// line breaks already in place.
///
///
internal static class DatRichText
{
/// One composed segment: text plus the color it should render
/// in. A null or empty is silently skipped (mirrors
/// retail's own null-string-info no-op guards throughout this text
/// composition family).
public readonly record struct Segment(string? Text, Vector4 Color);
///
/// Word-wraps every segment (independently, so
/// each segment's wrapped lines keep ITS OWN color), then concatenates
/// the results in order. No separator is inserted between segments —
/// retail's own composition calls concatenate directly
/// (AppendStringInfoWithFont/append_n_chars with no
/// interposed literal), so any blank-line spacing between sections
/// comes from the authored DAT string content itself, not from code
/// here.
///
public static IReadOnlyList Compose(
UiText target,
IReadOnlyList segments)
{
ArgumentNullException.ThrowIfNull(target);
ArgumentNullException.ThrowIfNull(segments);
var lines = new List();
// R2-1 (Campaign CC gate round 1 Batch E): the wrap width must shrink
// by the SAME left+right inset the draw path now applies (Padding
// plus the four retail margins, UiText.MarginLeft's own doc) — the
// Batch-C regression's second half: text wasn't just drawing at the
// wrong X, it was also wrapping to the FULL box width instead of the
// authored interior width, overflowing the visible right edge too.
float maximumWidth = MathF.Max(
1f,
target.Width - (target.Padding + target.MarginLeft) - (target.Padding + target.MarginRight));
Func measure = target.DatFont is { } font
? font.MeasureWidth
: static value => value.Length * 8f;
foreach (Segment segment in segments)
{
if (string.IsNullOrEmpty(segment.Text))
continue;
// Escape decoding (the DAT's literal two-character "\n") happens
// at the string source (DatStringResolver → RetailStringEscapes,
// 2026-08-17 systemic round — retail's own placement), so
// segments arrive with real line breaks; WrapWords preserves
// them and drops any stray CR itself.
foreach (string wrapped in UiText.WrapWords(segment.Text, measure, maximumWidth))
lines.Add(new UiText.Line(wrapped, segment.Color));
}
return lines;
}
///
/// Resolves 's own authored font-color
/// palette (dat property 0x1B) entry at ,
/// falling back to when the palette is
/// absent or too short. Mirrors the same fallback shape
/// CharacterStatController.BuildSelectedTitleRuns already uses
/// for its own palette-indexed colors.
///
public static Vector4 PaletteColor(UiText target, int index, Vector4 fallback) =>
index >= 0 && index < target.FontColorPalette.Count
? target.FontColorPalette[index]
: fallback;
}