using System;
using System.Collections.Generic;
using DatReaderWriter;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Enums;
using DatReaderWriter.Types;
namespace AcDream.App.UI.Layout;
///
/// The result of importing a retail LayoutDesc: a tree with
/// an O(1) lookup table for finding any element by its dat id.
///
public sealed class ImportedLayout
{
/// Root widget of the imported tree.
public UiElement Root { get; }
private readonly Dictionary _byId;
public ImportedLayout(UiElement root, Dictionary byId)
{
Root = root;
_byId = byId;
}
/// Find a widget by its dat element id (e.g. 0x100000E6).
/// Returns null if the id was skipped (Type-12 prototype) or not present.
public UiElement? FindElement(uint id)
=> _byId.TryGetValue(id, out var e) ? e : null;
}
///
/// Two-layer layout importer for retail LayoutDesc dat objects.
///
///
/// Pure layer ( / ):
/// converts a pre-resolved tree into a
/// tree via . Testable without dats or OpenGL — all tests
/// in LayoutImporterTests.cs exercise this layer only.
///
///
///
/// Dat shell (): reads a ,
/// converts each top-level to a fully resolved
/// (applying BaseElement / BaseLayoutId
/// inheritance with a cycle guard), then delegates to .
///
///
///
/// Meter elements (Type 7) consume their own dat-children:
/// reads the grandchild slice-sprite ids during construction, so the
/// children must NOT be added as separate nodes in the tree.
/// Every other element type recurses its children generically.
///
///
public static class LayoutImporter
{
// ── Pure layer ────────────────────────────────────────────────────────────
///
/// Convenience for tests: attach to
/// , then call .
/// The children list is set directly on ;
/// any existing children are replaced.
///
public static ImportedLayout BuildFromInfos(
ElementInfo rootInfo,
IEnumerable children,
Func resolve,
UiDatFont? datFont)
{
rootInfo.Children = new List(children);
return Build(rootInfo, resolve, datFont);
}
///
/// Pure builder: produce the widget tree from a fully resolved
/// tree (children already attached).
///
public static ImportedLayout Build(
ElementInfo rootInfo,
Func resolve,
UiDatFont? datFont)
{
var byId = new Dictionary();
// Root is never a Type-12 prototype in practice; fall back to a generic
// container if the factory returns null for an exotic root type.
var root = BuildWidget(rootInfo, resolve, datFont, byId);
if (root is null)
{
Console.WriteLine($"[D.2b] LayoutImporter: root element 0x{rootInfo.Id:X8} (type {rootInfo.Type}) produced no widget — using empty container fallback.");
root = new UiDatElement(rootInfo, resolve);
}
return new ImportedLayout(root, byId);
}
private static UiElement? BuildWidget(
ElementInfo info,
Func resolve,
UiDatFont? datFont,
Dictionary byId)
{
var w = DatWidgetFactory.Create(info, resolve, datFont);
if (w is null) return null; // Type-12 style prototype — skip
if (info.Id != 0) byId[info.Id] = w;
// Meters consume their own children: DatWidgetFactory already extracted the
// slice-sprite ids from the grandchild image elements during UiMeter construction.
// Adding those children as separate UiElement nodes would produce duplicate
// geometry and wrong widget semantics. Every other element type recurses normally.
if (w is not UiMeter)
{
foreach (var child in info.Children)
{
var cw = BuildWidget(child, resolve, datFont, byId);
if (cw is not null) w.AddChild(cw);
}
}
return w;
}
// ── Dat shell ─────────────────────────────────────────────────────────────
///
/// Dat shell, ElementInfo half: load the layout + resolve inheritance + build the
/// ElementInfo tree (no widgets). Exposed for fixture generation + conformance tests.
/// Returns null if the layout is missing.
///
/// The dat collection to read the LayoutDesc from.
/// The LayoutDesc dat id to read.
public static ElementInfo? ImportInfos(DatCollection dats, uint layoutId)
{
var ld = dats.Get(layoutId);
if (ld is null) return null;
var tops = new List();
foreach (var kv in ld.Elements)
tops.Add(Resolve(dats, kv.Value, new HashSet<(uint, uint)>()));
return tops.Count == 1
? tops[0]
: new ElementInfo { Id = 0, Type = 3, Children = tops };
}
///
/// Dat shell: load the LayoutDesc, resolve inheritance for every top-level
/// element, and build the widget tree. Returns null if the layout is absent
/// from the dats.
///
public static ImportedLayout? Import(
DatCollection dats,
uint layoutId,
Func resolve,
UiDatFont? datFont)
{
var rootInfo = ImportInfos(dats, layoutId);
if (rootInfo is null) return null;
return Build(rootInfo, resolve, datFont);
}
// ── Inheritance resolution ────────────────────────────────────────────────
///
/// Converts an to a resolved :
/// reads own fields + media, applies the BaseElement / BaseLayoutId chain
/// (cycle-guarded by ), then resolves + attaches children.
///
private static ElementInfo Resolve(
DatCollection dats,
ElementDesc d,
HashSet<(uint layoutId, uint elementId)> baseChain)
{
// Read this element's own fields + media (no inheritance, no children yet).
var self = ToInfo(d);
var result = self;
// Apply BaseElement / BaseLayoutId inheritance if present.
if (d.BaseElement != 0 && d.BaseLayoutId != 0
&& baseChain.Add((d.BaseLayoutId, d.BaseElement)))
{
var baseLd = dats.Get(d.BaseLayoutId);
var baseDesc = baseLd is null ? null : FindDesc(baseLd, d.BaseElement);
if (baseDesc is not null)
{
// Recurse the base chain (already guarded by the HashSet add above).
var baseInfo = Resolve(dats, baseDesc, baseChain);
// Derived fields override the base; result.Children is still empty here
// — children are attached below from the DERIVED element's own tree.
result = ElementReader.Merge(baseInfo, self);
}
}
// Resolve + attach children. Each child gets a FRESH base-chain set:
// the cycle guard is per-element, not shared across siblings.
foreach (var kv in d.Children)
result.Children.Add(Resolve(dats, kv.Value, new HashSet<(uint, uint)>()));
return result;
}
///
/// Read an 's own scalar fields + state media into a
/// fresh . No inheritance is applied; children are not
/// attached (the caller handles those).
///
private static ElementInfo ToInfo(ElementDesc d)
{
// Normalize DefaultState: UIStateId.ToString() gives "Undef"/"Undefined" or "0" when
// no default is set; map those to "" so UiDatElement treats them as "no preference".
var defState = d.DefaultState.ToString();
var info = new ElementInfo
{
Id = d.ElementId,
Type = d.Type,
X = (float)d.X,
Y = (float)d.Y,
Width = (float)d.Width,
Height = (float)d.Height,
Left = d.LeftEdge,
Top = d.TopEdge,
Right = d.RightEdge,
Bottom = d.BottomEdge,
ReadOrder = d.ReadOrder,
DefaultStateName = (defState is "Undef" or "Undefined" or "0") ? "" : defState,
};
// DirectState (unnamed, key "").
if (d.StateDesc is not null)
ReadState(d.StateDesc, "", info);
// Named states (e.g. UIStateId.HideDetail → "HideDetail").
foreach (var s in d.States)
ReadState(s.Value, s.Key.ToString(), info);
return info;
}
///
/// Read the first from into
/// info.StateMedia[name] and extract the font DID from property 0x1A
/// (ArrayBaseProperty → DataIdBaseProperty) if not yet set.
///
private static void ReadState(StateDesc sd, string name, ElementInfo info)
{
// Only MediaDescImage is read for rendering; MediaDescCursor items (on grips/drag bars)
// are intentionally skipped — cursor behavior is Plan 2.
foreach (var m in sd.Media)
{
if (m is MediaDescImage img && img.File != 0)
{
info.StateMedia[name] = (img.File, (int)img.DrawMode);
break;
}
}
// Font DID: Properties[0x1A] is ArrayBaseProperty{ DataIdBaseProperty }.
// Format doc §3: "ArrayBaseProperty containing ONE DataIdBaseProperty".
if (info.FontDid == 0 && sd.Properties is not null
&& sd.Properties.TryGetValue(0x1Au, out var raw)
&& raw is ArrayBaseProperty arr && arr.Value.Count > 0
&& arr.Value[0] is DataIdBaseProperty did)
{
info.FontDid = did.Value;
}
}
// ── Element tree search ───────────────────────────────────────────────────
///
/// Find an by id anywhere in the top-level tree of
/// (depth-first). Returns null if not found.
///
private static ElementDesc? FindDesc(LayoutDesc ld, uint id)
{
foreach (var kv in ld.Elements)
{
var f = FindDescIn(kv.Value, id);
if (f is not null) return f;
}
return null;
}
private static ElementDesc? FindDescIn(ElementDesc d, uint id)
{
if (d.ElementId == id) return d;
foreach (var kv in d.Children)
{
var f = FindDescIn(kv.Value, id);
if (f is not null) return f;
}
return null;
}
}