using System;
using System.Linq;
namespace AcDream.App.UI.Layout;
///
/// Hybrid factory: behavioral element Types map to dedicated widgets (verbatim
/// algorithm ports); everything else (and unknown Types) falls back to
/// .
///
///
/// Type 12 (style prototype / BaseElement store) is never instantiated —
/// returns null and the importer skips it.
/// See docs/research/2026-06-15-layoutdesc-format.md Correction 8.
///
///
///
/// The meter's back/front 3-slice sprite ids live on grandchild image elements,
/// NOT on the meter element itself (format doc §11).
/// walks two layers down to extract them: the two Type-3 container children
/// ordered by (back behind = lower, front
/// on top = higher), then within each container the image children that carry
/// a DirectState ("" key) sprite, ordered by their X position to obtain
/// left-cap / center-tile / right-cap.
///
///
///
/// The expand-detail overlay present in the front container carries ONLY named
/// states ("HideDetail"/"ShowDetail") — no "" DirectState entry — so the
/// TryGetValue("") filter in excludes it
/// automatically.
///
///
public static class DatWidgetFactory
{
///
/// Creates the for , sets its
/// rect (Left/Top/Width/Height) and Anchors, and returns it.
///
/// Resolved, merged element snapshot from the LayoutDesc importer.
/// RenderSurface id → (GL tex handle, pixel width, pixel height).
/// Returns (0,0,0) when the texture is not yet uploaded.
/// Retail UI font for the meter's "cur/max" number overlay.
/// May be null pre-load — the meter falls back to the debug bitmap font.
/// The widget, or null for a Type-12 style prototype (caller skips it).
public static UiElement? Create(ElementInfo info,
Func resolve, UiDatFont? datFont)
{
// Type 12 = zero-size style prototype / BaseElement store referenced by
// BaseLayoutId. These are property bags, never rendered. See format doc §8
// ("style prototypes are Type 12 which must be skipped") and Correction 8.
if (info.Type == 12) return null;
UiElement e = info.Type switch
{
7 => BuildMeter(info, resolve, datFont), // UIElement_Meter
_ => new UiDatElement(info, resolve), // generic fallback for all other types
};
// Propagate position + size (pixel-exact from the dat).
e.Left = info.X;
e.Top = info.Y;
e.Width = info.Width;
e.Height = info.Height;
// Map the four raw edge-anchor values to the AnchorEdges bit-flag that the
// UI layout engine uses for reflow.
e.Anchors = ElementReader.ToAnchors(info.Left, info.Top, info.Right, info.Bottom);
return e;
}
// ── Meter ────────────────────────────────────────────────────────────────
///
/// Builds a and populates its six 3-slice sprite ids by
/// reading the meter's grandchild image elements (format doc §11).
///
///
/// Structure the importer produces for each meter (UIElement_Meter):
///
/// meter (Type 7)
/// ├── back-layer container (Type 3, lower ReadOrder — drawn first / behind)
/// │ ├── left-cap image (DirectState "" → File = back-left sprite)
/// │ ├── center image (DirectState "" → File = back-tile sprite)
/// │ └── right-cap image (DirectState "" → File = back-right sprite)
/// ├── front-layer container (Type 3, higher ReadOrder — drawn on top)
/// │ ├── left-cap image (→ front-left sprite)
/// │ ├── center image (→ front-tile sprite)
/// │ ├── right-cap image (→ front-right sprite)
/// │ └── expand overlay (named "ShowDetail"/"HideDetail" only — NO DirectState — IGNORED)
/// └── text label (Type 0) (IGNORED — Fill/Label providers bound by VitalsController in Task 6)
///
///
///
///
/// and are NOT set here.
/// They are bound to the live stat providers in Task 6 (VitalsController).
///
///
private static UiMeter BuildMeter(ElementInfo info,
Func resolve, UiDatFont? datFont)
{
var m = new UiMeter
{
SpriteResolve = resolve,
DatFont = datFont,
};
// The two 3-slice containers are Type-3 children of the meter element.
// ReadOrder determines draw order: the back track has a LOWER ReadOrder
// (drawn first, behind the fill), the front has a HIGHER ReadOrder (on top).
var containers = info.Children
.Where(c => c.Type == 3)
.OrderBy(c => c.ReadOrder)
.ToList();
if (containers.Count >= 1)
{
var (l, t, r) = SliceIds(containers[0]);
m.BackLeft = l;
m.BackTile = t;
m.BackRight = r;
}
if (containers.Count >= 2)
{
var (l, t, r) = SliceIds(containers[1]);
m.FrontLeft = l;
m.FrontTile = t;
m.FrontRight = r;
}
return m;
}
///
/// Returns the (left, tile, right) sprite ids for a 3-slice container,
/// extracting them from the container's image children that carry a DirectState
/// ("" key) with a non-zero file id, ordered left-to-right by their X position.
///
///
/// Children that carry ONLY named states (e.g. the expand-detail overlay with
/// "ShowDetail"/"HideDetail" entries but no "" key) are excluded automatically
/// because for "" returns
/// false.
///
///
private static (uint left, uint tile, uint right) SliceIds(ElementInfo container)
{
// Only children that have a non-zero DirectState image are slice candidates.
// The expand-detail overlay has NO DirectState entry, so it's excluded here.
var slices = container.Children
.Where(c => c.StateMedia.TryGetValue("", out var med) && med.File != 0)
.OrderBy(c => c.X)
.ToList();
static uint File(ElementInfo e)
=> e.StateMedia.TryGetValue("", out var med) ? med.File : 0u;
uint left = slices.Count > 0 ? File(slices[0]) : 0u;
uint tile = slices.Count > 1 ? File(slices[1]) : 0u;
uint right = slices.Count > 2 ? File(slices[2]) : 0u;
return (left, tile, right);
}
}