using System;
using System.Collections.Generic;
using System.Numerics;
using AcDream.App.Rendering;
using AcDream.App.UI;
using AcDream.Core.Chat;
using AcDream.UI.Abstractions;
using AcDream.UI.Abstractions.Panels.Chat;
namespace AcDream.App.UI.Layout;
///
/// Binds one of retail's four floating chat windows — LayoutDesc
/// 0x2100005B, window elements 0x10000505/0x1000050E/
/// 0x1000050F/0x10000510 — to live behavior (Campaign CH
/// slice CH6b). All four windows share the SAME LayoutDesc; only the
/// window-id attribute (0x1000007E, carried here as
/// rather than re-read from the dat, since acdream's
/// importer does not resolve LayoutDesc attributes into runtime state) and
/// the mounted screen position differ per instance.
///
///
/// Shares with
/// (the main window) rather than
/// duplicating the word-wrap/color-carry algorithm — both are views over the
/// SAME transcript (research doc §6.1's "one
/// ChatWindowController instance per window" recommendation, generalized to
/// two sibling classes because the main and floaty layouts diverge enough
/// in their own element sets — talk-focus menu, max/min button, four
/// indicator buttons on main; title bar and close button on floaty,
/// research doc §2.1 vs §2.2 — to make one shared Bind() unreadable).
///
///
///
/// A floaty window has no talk-focus menu (research doc §2.2), so its chat
/// entry always sends on — there is no
/// authored control to pick another channel from this window.
///
///
public sealed class FloatingChatWindowController : IRetainedPanelController
{
public const uint LayoutId = 0x2100005Bu;
// Element ids from floating chat LayoutDesc 0x2100005B (research doc §2.2).
private const uint RootId = 0x100004F7u; // window root, 250x108
private const uint TranscriptPanelId = 0x10000010u;
private const uint TranscriptId = 0x10000011u; // Type-12 prototype — skipped by factory
private const uint TrackId = 0x10000012u;
private const uint InputRowId = 0x10000509u; // NOTE: differs from the main window's 0x10000013
private const uint InputId = 0x10000016u; // Type-12 Text + Editable 0x16 -> UiField
private const uint SendId = 0x10000019u;
private const uint TitleBarId = 0x100004D9u; // gmFloatyChatUI::SetWindowTitle target
private const uint CloseButtonId = 0x1000052Au;
private bool _disposed;
/// The retail window id this instance is bound to (1-4).
public int WindowId { get; }
public UiElement Root { get; private set; } = null!;
public UiText Transcript { get; private set; } = null!;
public UiField Input { get; private set; } = null!;
public UiScrollbar? Scrollbar { get; private set; }
/// Resolved window-root metadata, including DAT size constraints.
public ElementInfo DatWindowInfo { get; private set; } = null!;
public RetailWindowHandle? WindowHandle { get; private set; }
// Same idle-frame caching shape as ChatWindowController — an unchanged
// transcript/filter/wrap-width does not re-wrap or re-resolve colors.
private IReadOnlyList _cachedTranscriptLines = Array.Empty();
private long _cachedTranscriptRevision = -1;
private ulong _cachedFilter;
private float _cachedTranscriptWrapWidth = float.NaN;
private UiDatFont? _cachedTranscriptDatFont;
private BitmapFont? _cachedTranscriptDebugFont;
internal int TranscriptLayoutBuildCount { get; private set; }
private FloatingChatWindowController(int windowId)
{
WindowId = windowId;
}
///
/// Bind an imported floating-chat layout to live behavior.
///
/// Retail chat window id, 1 through 4.
/// Full tree from an
/// call against
/// .
/// Widget tree from a matching
/// call — a FRESH call per window
/// instance, since four independent windows need four independent
/// widget trees even though they share one imported rootInfo.
/// The SAME chat view-model the main window binds —
/// one canonical transcript, filtered per window.
/// Factory for the live command bus at submit time.
///
/// Runtime's canonical per-window filter/open state
/// (). Read live on
/// every transcript rebuild — never copied — so a filter change is
/// visible on the next frame without a separate notification.
///
public static FloatingChatWindowController? Bind(
int windowId,
ElementInfo rootInfo,
ImportedLayout layout,
ChatVM vm,
Func busProvider,
ChatWindowState windowFilters,
UiDatFont? datFont,
BitmapFont? debugFont,
Func resolve)
{
if (windowId < ChatWindowState.MinFloatingWindowId || windowId > ChatWindowState.MaxFloatingWindowId)
throw new ArgumentOutOfRangeException(nameof(windowId));
ArgumentNullException.ThrowIfNull(windowFilters);
var transcriptPanel = layout.FindElement(TranscriptPanelId);
var inputRow = layout.FindElement(InputRowId);
var input = layout.FindElement(InputId) as UiField;
if (input is null || transcriptPanel is null || inputRow is null)
{
Console.WriteLine(
$"[D.2b] FloatingChatWindowController.Bind(window {windowId}): missing required elements " +
$"(input={input is not null}, panel={transcriptPanel is not null}, row={inputRow is not null}) — " +
$"floating chat window will not be interactive.");
return null;
}
var window = layout.FindElement(RootId) ?? layout.Root;
var c = new FloatingChatWindowController(windowId)
{
Root = window,
DatWindowInfo = FindInfo(rootInfo, RootId) ?? rootInfo,
};
// ── Transcript ───────────────────────────────────────────────────
c.Transcript = layout.FindElement(TranscriptId) as UiText
?? throw new InvalidOperationException("floating chat transcript 0x10000011 not built as UiText");
c.Transcript.DatFont = datFont;
c.Transcript.Font = debugFont;
c.Transcript.Centered = false;
c.Transcript.RightAligned = false;
c.Transcript.OneLine = false;
c.Transcript.Selectable = true;
c.Transcript.LinesProvider = () => c.GetTranscriptLines(vm, windowFilters);
// ── Input — no talk-focus menu on the floaty layout, so the
// channel is always Say (class doc). ─────────────────────────────
c.Input = input;
c.Input.DatFont = datFont;
c.Input.Font = debugFont;
c.Input.SpriteResolve = resolve;
c.Input.OnSubmit = text => ChatCommandRouter.Submit(text, vm, busProvider(), ChatChannelKind.Say);
// Same right-edge-tracks-resize fix as the main window
// (ChatWindowController.Bind) — see that method's comment for the
// full retail edge-mode citation.
if (c.Input.LayoutPolicy is { } inputPolicy)
{
c.Input.LayoutPolicy = new UiLayoutPolicy(
inputPolicy.LeftMode,
inputPolicy.TopMode,
rightMode: 1u,
inputPolicy.BottomMode,
inputPolicy.OriginalChild,
inputPolicy.OriginalParent);
}
else
{
c.Input.Anchors |= AnchorEdges.Right;
}
// ── Scrollbar — bind the factory-built Type-11 track element ────
if (layout.FindElement(TrackId) is UiScrollbar bar)
{
bar.Model = c.Transcript.Scroll;
bar.SpriteResolve ??= resolve;
c.Scrollbar = bar;
}
// ── Send button ─────────────────────────────────────────────────
if (layout.FindElement(SendId) is UiButton sendEl)
{
sendEl.OnClick = () => c.Input.Submit();
sendEl.Label = "Send";
sendEl.LabelFont = datFont;
sendEl.LabelColor = new Vector4(1f, 0.92f, 0.72f, 1f);
}
// ── Title bar — gmFloatyChatUI::SetWindowTitle @0x004CEAA0 sets a
// localized "Chat N" string here; acdream has no LayoutDesc string
// table wired for it yet, so this is a hardcoded English label —
// the same stopgap the channel menu's item labels already use
// (ChatWindowController.ChannelItems). ──────────────────────────
if (layout.FindElement(TitleBarId) is UiText titleText)
{
titleText.DatFont = datFont;
titleText.Font = debugFont;
titleText.OneLine = true;
string title = $"Chat {windowId}";
var titleColor = new Vector4(1f, 0.92f, 0.72f, 1f);
titleText.LinesProvider = () => new[] { new UiText.Line(title, titleColor) };
}
// ── Close button — gmFloatyChatUI::ListenToElementMessage
// @0x004CE330: idMessage==1 (clicked) on 0x1000052A -> SetVisible(false). ──
if (layout.FindElement(CloseButtonId) is UiButton closeEl)
{
closeEl.OnClick = () => c.WindowHandle?.Hide();
}
return c;
}
///
/// Attach the typed outer-frame handle after the controller's imported
/// content has been mounted. The close button needs this to hide the
/// window (retail's own SetVisible(false)).
///
public void AttachWindow(RetailWindowHandle handle)
{
ArgumentNullException.ThrowIfNull(handle);
if (!ReferenceEquals(handle.ContentRoot, Root))
throw new ArgumentException(
"Floating chat handle content root does not match the bound layout.", nameof(handle));
if (WindowHandle is not null && !ReferenceEquals(WindowHandle, handle))
throw new InvalidOperationException("Floating chat controller is already attached to another window.");
WindowHandle = handle;
}
private static ElementInfo? FindInfo(ElementInfo node, uint id)
{
if (node.Id == id) return node;
foreach (ElementInfo child in node.Children)
{
ElementInfo? found = FindInfo(child, id);
if (found is not null) return found;
}
return null;
}
///
/// Convert the shared ChatVM's detailed lines to this window's filtered,
/// wrapped, colored transcript —
/// is retail's broadcast half of the display rule
/// (ChatInterface::RecvNotice_DisplayFinalStringInfo); the
/// explicit-address half is inert today because no production
/// ChatEntry carries a target window id yet (register row
/// AP-180 — see 's class doc).
///
private IReadOnlyList GetTranscriptLines(ChatVM vm, ChatWindowState windowFilters)
{
float maxW = Transcript.Width - 2f * Transcript.Padding;
UiDatFont? datFont = Transcript.DatFont;
BitmapFont? debugFont = Transcript.Font;
long revision = vm.Revision;
ulong filter = windowFilters.GetFilter(WindowId);
if (_cachedTranscriptRevision == revision
&& _cachedFilter == filter
&& _cachedTranscriptWrapWidth.Equals(maxW)
&& ReferenceEquals(_cachedTranscriptDatFont, datFont)
&& ReferenceEquals(_cachedTranscriptDebugFont, debugFont))
{
return _cachedTranscriptLines;
}
var detailed = vm.RecentLinesDetailed();
Func measure =
datFont is { } df ? s => df.MeasureWidth(s)
: debugFont is { } bf ? s => bf.MeasureWidth(s)
: static s => s.Length * 7f;
bool Accept(uint logTextType) => windowFilters.ShouldDisplay(
WindowId, ChatWindowState.BroadcastTargetWindow, logTextType);
var result = ChatTranscriptRenderer.BuildLines(
detailed, maxW, measure, Accept, Transcript.DefaultColor);
_cachedTranscriptRevision = revision;
_cachedFilter = filter;
_cachedTranscriptWrapWidth = maxW;
_cachedTranscriptDatFont = datFont;
_cachedTranscriptDebugFont = debugFont;
_cachedTranscriptLines = result;
TranscriptLayoutBuildCount++;
return result;
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
}
}