using System;
using System.Numerics;
using AcDream.Core.Social;
namespace AcDream.App.UI.Layout;
///
/// Campaign FA slice FA3, D1: the Squelch page's read-only roster — names
/// only, bound directly to the J4.1 owner
/// (RuntimeCommunicationState.Squelch). Lists both squelched
/// characters (, keyed by the
/// character's own display name) and squelched accounts
/// (, whose dictionary KEY is the
/// account name itself). Rebuilds on
/// change; polled every frame from
/// WHILE THE PANEL IS VISIBLE (see the rebuild-discipline paragraph below).
///
///
/// D1 — inert actions. Same disposition as
/// : the three buttons
/// (0x10000547/0x1000054B/0x1000054C) are left
/// AUTHORED and CLICKABLE with no handler. See register row AD-79
/// (docs/architecture/retail-divergence-register.md), which covers
/// the whole D1 Friends/Squelch inert-actions scope.
///
///
///
/// Row template 0x21000060/0x10000541 (FA3 live-mount probe);
/// see for the name-text resolution note
/// (gmSquelchUI is outside this campaign's decompiled scope).
///
///
///
/// Scrollbar (fix-round blast MF-1; FA4 carry-forward 3). The
/// ListBox 0x1000053E authors a direct sibling scrollbar,
/// 0x10000543 — resolved via the built
/// and wired to
/// the same way
/// wires its own (see that
/// class's doc for the full rationale, including why there is no wheel
/// fallback and why the authored field replaces a hardcoded literal).
///
///
///
/// Rebuild discipline (fix-round blast SF-2/SF-3). Same discipline as
/// : only
/// advances after every row resolves, and
/// gates on the
/// panel's own visibility so no DAT-locked rebuild work runs while the
/// panel is closed.
///
///
public sealed class SocialSquelchPageController
{
private const uint ListBoxId = 0x1000053Eu;
// 2026-08-13 gate (AD-79 retirement): probe-verified roles
// (ProbeSocialClickRouting label dump) — the name box, the Remove button,
// and the two add buttons ("Squelch Character" / "Squelch Account").
private const uint NameFieldId = 0x10000540u;
private const uint RemoveButtonId = 0x10000547u;
private const uint SquelchCharacterButtonId = 0x1000054Bu;
private const uint SquelchAccountButtonId = 0x1000054Cu;
/// 2026-08-13 (AD-79 retired for this page): add-by-name rides
/// 0x0058 (character scope, guid 0, type AllChannels — ACE name-
/// looks-up) / 0x0059 (account scope); remove is 0x0058/0x0059
/// with add=0 for the selected row. Wire details:
/// docs/research/2026-08-13-social-wire-completion.md §1.5.
public sealed record Actions(
Action SquelchCharacter,
Action SquelchAccount,
Action RemoveCharacterSquelch,
Action RemoveAccountSquelch);
private readonly UiTemplateListBox _listBox;
private readonly SquelchState _squelch;
private readonly Actions? _actions;
private readonly UiField? _nameField;
private long _lastRevision = long.MinValue;
private (uint Guid, string Name, bool IsAccount)? _selected;
private SocialSquelchPageController(
UiTemplateListBox listBox,
SquelchState squelch,
Actions? actions,
UiField? nameField)
{
_listBox = listBox;
_squelch = squelch;
_actions = actions;
_nameField = nameField;
}
public static SocialSquelchPageController? Bind(
UiElement pageRoot,
SquelchState squelch,
Func templateResolver,
Actions? actions = null)
{
ArgumentNullException.ThrowIfNull(pageRoot);
ArgumentNullException.ThrowIfNull(squelch);
ArgumentNullException.ThrowIfNull(templateResolver);
if (UiElement.FindDescendant(pageRoot, ListBoxId) is not UiTemplateListBox listBox)
{
Console.WriteLine(
$"[D.2b] SocialSquelchPageController: ListBox 0x{ListBoxId:X8} not "
+ "found — Squelch page will not populate.");
return null;
}
listBox.TemplateResolver = templateResolver;
uint scrollbarElementId = listBox.ScrollbarElementId;
UiElement? scrollbarElement = scrollbarElementId == 0
? null
: UiElement.FindDescendant(pageRoot, scrollbarElementId);
if (scrollbarElement is UiScrollbar scrollbar)
scrollbar.Model = listBox.Scroll;
else
Console.WriteLine(
$"[D.2b] SocialSquelchPageController: scrollbar 0x{scrollbarElementId:X8} "
+ "not found — the Squelch list will not scroll.");
var nameField = UiElement.FindDescendant(pageRoot, NameFieldId) as UiField;
var controller = new SocialSquelchPageController(listBox, squelch, actions, nameField);
controller.WireActions(pageRoot);
controller.Refresh();
return controller;
}
private void WireActions(UiElement pageRoot)
{
if (_actions is not { } actions) return; // fixture callers stay inert
if (UiElement.FindDescendant(pageRoot, SquelchCharacterButtonId) is UiButton addCharacter)
addCharacter.OnClick = () =>
{
string name = _nameField?.Text?.Trim() ?? string.Empty;
if (string.IsNullOrWhiteSpace(name)) return;
actions.SquelchCharacter(name);
_nameField?.SetText(string.Empty);
};
if (UiElement.FindDescendant(pageRoot, SquelchAccountButtonId) is UiButton addAccount)
addAccount.OnClick = () =>
{
string name = _nameField?.Text?.Trim() ?? string.Empty;
if (string.IsNullOrWhiteSpace(name)) return;
actions.SquelchAccount(name);
_nameField?.SetText(string.Empty);
};
if (UiElement.FindDescendant(pageRoot, RemoveButtonId) is UiButton remove)
remove.OnClick = () =>
{
if (_selected is not { } selected) return;
if (selected.IsAccount)
actions.RemoveAccountSquelch(selected.Name);
else
actions.RemoveCharacterSquelch(selected.Guid, selected.Name);
};
}
public void Tick()
{
long revision = _squelch.Revision;
if (revision == _lastRevision) return;
Refresh();
}
private void Refresh()
{
long revision = _squelch.Revision;
_listBox.Flush();
SquelchDatabase database = _squelch.Snapshot();
bool allRowsResolved = true;
bool selectedStillPresent = false;
foreach ((uint guid, SquelchInfo character) in database.Characters)
{
allRowsResolved &= AddRow(character.Name, guid, isAccount: false);
if (_selected is { IsAccount: false } s && s.Guid == guid)
selectedStillPresent = true;
}
foreach (string accountName in database.Accounts.Keys)
{
allRowsResolved &= AddRow(accountName, 0u, isAccount: true);
if (_selected is { IsAccount: true } s && s.Name == accountName)
selectedStillPresent = true;
}
if (!selectedStillPresent) _selected = null;
// SF-3: only latch the revision once the rebuild actually reflects it —
// a resolver miss must not silently swallow a future revision bump.
if (allRowsResolved) _lastRevision = revision;
}
private bool AddRow(string name, uint guid, bool isAccount)
{
UiElement? row = _listBox.AddItemFromTemplateList(0);
if (row is null) return false;
if (SocialPanelRowText.FindDeepest(row) is { } text)
{
text.LinesProvider = () => [new UiText.Line(name, Vector4.One)];
// 2026-08-13 gate: row click selects the entry the Remove button
// acts on (UiText.OnClick clears the display-text ClickThrough
// default).
text.OnClick = () => _selected = (guid, name, isAccount);
}
return true;
}
}