using System;
using System.Threading;
namespace AcDream.Core.Chat;
///
/// Retail's per-chat-window text-type filter and open/visible state — window
/// id 0 is the main chat window, ids 1-4 are the four
/// floating chat windows (Campaign CH slice CH6b,
/// docs/research/2026-08-09-chat-retail-window-shell.md §1.2 and
/// docs/research/2026-08-09-chat-retail-color-table.md §4). These are
/// acdream's OWN compact indices, not retail's raw m_eWindowID values:
/// retail's actual main window is m_eWindowID == 8, its floaties are
/// 2-5, and m_eWindowID == 0 is retail's ctor-default
/// "unauthored" sentinel — none of that is 0-4 (CH6a/b
/// REJECT-review SHOULD-FIX 2,
/// docs/research/2026-08-10-ch6ab-review-findings.md).
///
///
/// Ports two retail mechanisms exactly:
///
/// - ChatInterface::PostInit @0x004F3DD0's m_oldState
/// switch seeds each window's default 64-bit
/// m_llTextTypeFilter (color-table doc §4's table — the constants
/// below are byte-identical to that table; retail's own main-window default
/// 0xFBFFFFFF is shared by m_eWindowID 1 AND 8, and the floaty
/// defaults come from retail m_eWindowID 2-5, one higher than
/// acdream's compact 1-4).
/// - ChatInterface::RecvNotice_DisplayFinalStringInfo
/// @0x004F4640's display predicate: a line shows in window
/// W when the message's target window id equals W
/// (explicit addressing) OR the message is broadcast
/// () AND W's filter accepts the
/// line's (ChatInterface::TypeIsActive
/// @0x004F2F10).
///
///
///
///
/// is a sentinel distinct from every real
/// window id (0-4) — the acdream-internal analogue of retail's
/// wire arg5 == 0 broadcast marker, deliberately kept separate from
/// window id 0 (main). Earlier CH6b code conflated the two (both were
/// literal 0), so 's explicit-addressing
/// branch (targetWindowId == windowId) short-circuited true for EVERY
/// broadcast line evaluated against the main window, regardless of its own
/// filter — the CH6a/b REJECT-review BLOCKER. With the sentinel separated
/// out, the main window's filter is genuinely consulted for broadcast lines,
/// is a real (not inert) write for window 0
/// too, and a future explicit targetWindowId == MainWindowId (AP-180's
/// m_idCurrentCommandSource per-window echo) stays distinguishable
/// from an ordinary broadcast line. /
/// keep their own, UNRELATED no-op for window 0 — retail's main window
/// is simply never closable, independent of the broadcast-sentinel fix.
///
///
///
/// No production carries an explicit target window
/// id yet (register row AP-180 — the windowId dual-destination echo
/// is deferred); every current line is effectively broadcast
/// (targetWindowId == ).
/// still accepts the full retail shape so the
/// routing predicate does not need to change shape when AP-180 lands.
///
///
public sealed class ChatWindowState
{
public const int MainWindowId = 0;
public const int MinFloatingWindowId = 1;
public const int MaxFloatingWindowId = 4;
///
/// Retail's authored per-window text-type filter defaults
/// (gmChatOptionsUI::InitOptions @0x0049FC60 /
/// AddCheckboxBitfield64Option @0x0049EDA0's SetDefaultValue calls,
/// docs/research/2026-08-10-options-panel-structure.md §5.2). Named here
/// (rather than only as the private literals in )
/// because Campaign OP slice OP5's Chat-tab Defaults button needs the SAME
/// values seeds — one source, not two independently
/// literal tables that could drift.
///
public const ulong MainWindowDefaultFilter = 0xFBFFFFFFul;
public const ulong Floaty1DefaultFilter = 0x0000101Cul;
public const ulong Floaty2DefaultFilter = 0x00040C00ul;
public const ulong Floaty3DefaultFilter = 0x00080000ul;
public const ulong Floaty4DefaultFilter = 0x78000000ul;
///
/// Sentinel targetWindowId meaning "broadcast to every window,
/// subject to each window's own filter" — the acdream-internal analogue
/// of retail's wire arg5 == 0. Deliberately outside the
/// 0-4 real-window-id range (see the class doc) so it can
/// never collide with .
///
public const uint BroadcastTargetWindow = uint.MaxValue;
private const int WindowCount = MaxFloatingWindowId + 1;
private readonly object _gate = new();
private readonly ulong[] _filters = new ulong[WindowCount];
private readonly bool[] _open = new bool[WindowCount];
private long _revision;
public ChatWindowState() => ResetToDefaults();
///
/// Monotonic counter bumped on every filter or open-state change.
/// Lets presentation caches (per-window transcript layout) detect a
/// filter/visibility change without re-deriving it from the raw arrays.
///
public long Revision => Interlocked.Read(ref _revision);
///
/// Reset every window to retail's PostInit defaults (color-table
/// doc §4). The high dword is always 0 for every window — Society
/// (0x20) and the reserved slot (0x21) are opt-in only,
/// matching retail. Windows 1-4 start closed; window 0 (main) is always
/// open.
///
public void ResetToDefaults()
{
lock (_gate)
{
// "everything 0x00-0x1F except 0x1A" (m_oldState 1/8) — genuinely
// consulted for broadcast lines now that BroadcastTargetWindow is
// distinct from MainWindowId (see class doc).
_filters[0] = MainWindowDefaultFilter;
// Speech, Tell, Speech_Direct_Send, Emote (m_oldState 2).
_filters[1] = Floaty1DefaultFilter;
// Social, Social_Send, Allegiance (m_oldState 3).
_filters[2] = Floaty2DefaultFilter;
// Fellowship (m_oldState 4).
_filters[3] = Floaty3DefaultFilter;
// Turbine General/Trade/LFG/Roleplay (m_oldState 5).
_filters[4] = Floaty4DefaultFilter;
_open[0] = true;
for (int i = MinFloatingWindowId; i <= MaxFloatingWindowId; i++)
_open[i] = false;
Interlocked.Increment(ref _revision);
}
}
public ulong GetFilter(int windowId)
{
ValidateWindowId(windowId);
lock (_gate) return _filters[windowId];
}
///
/// Set window 's 64-bit type filter — including
/// the main window (id 0): CH6a/b REJECT-review SHOULD-FIX 2 drops
/// the old no-op (see the class doc's
/// paragraph). Retail's main window IS user-settable through the options
/// page; only the settings UI to drive this is future work.
///
public void SetFilter(int windowId, ulong filter)
{
ValidateWindowId(windowId);
lock (_gate)
{
if (_filters[windowId] == filter) return;
_filters[windowId] = filter;
Interlocked.Increment(ref _revision);
}
}
/// Main window (id 0) is always open.
public bool IsOpen(int windowId)
{
ValidateWindowId(windowId);
if (windowId == MainWindowId) return true;
lock (_gate) return _open[windowId];
}
/// No-op for the main window — it cannot be closed.
public void SetOpen(int windowId, bool open)
{
ValidateWindowId(windowId);
if (windowId == MainWindowId) return;
lock (_gate)
{
if (_open[windowId] == open) return;
_open[windowId] = open;
Interlocked.Increment(ref _revision);
}
}
///
/// Flip window 's open state and return the
/// new value. Always returns for the main window
/// (it cannot be toggled closed).
///
public bool Toggle(int windowId)
{
ValidateWindowId(windowId);
if (windowId == MainWindowId) return true;
lock (_gate)
{
bool next = !_open[windowId];
_open[windowId] = next;
Interlocked.Increment(ref _revision);
return next;
}
}
///
/// ChatInterface::TypeIsActive @0x004F2F10: does window
/// 's filter accept retail
/// ?
/// Types >= 64 are never active (there is no such bit).
///
public bool TypeIsActive(int windowId, uint logTextType)
{
ValidateWindowId(windowId);
if (logTextType >= 64u) return false;
ulong filter;
lock (_gate) filter = _filters[windowId];
return ((1UL << (int)logTextType) & filter) != 0UL;
}
///
/// ChatInterface::RecvNotice_DisplayFinalStringInfo @0x004F4640's
/// exact display predicate — see the class doc for the two branches.
/// is either a real window id
/// (0-4, explicit addressing) or
/// (broadcast, filtered per window).
///
public bool ShouldDisplay(int windowId, uint targetWindowId, uint logTextType)
{
ValidateWindowId(windowId);
if (targetWindowId == (uint)windowId) return true;
return targetWindowId == BroadcastTargetWindow && TypeIsActive(windowId, logTextType);
}
private static void ValidateWindowId(int windowId)
{
if (windowId < MainWindowId || windowId > MaxFloatingWindowId)
throw new ArgumentOutOfRangeException(
nameof(windowId), windowId, "chat window id must be 0 (main) through 4 (floating).");
}
}