using System.Linq;
using System.Numerics;
using AcDream.Core.Chat;
using AcDream.Core.Combat;
namespace AcDream.UI.Abstractions.Panels.Chat;
///
/// The chat panel. Shows the tail of + an input
/// field at the bottom that submits on Enter.
///
///
/// Phase I.4 added the input field and slash-command parsing. Supported
/// prefixes (alias-matched against the verb token, not by string-prefix
/// — so /general is NOT /g):
///
/// - /say <msg> or no prefix → Say (default)
/// - /t / /tell <name> <msg> → whisper
/// - /r / /reply <msg> → reply to most recent
/// INCOMING Tell (uses ;
/// drops the message if no Tell has arrived yet)
/// - /g, /f, /a, /m, /p, /v, /cv, /lfg, /trade, /role, /society,
/// /olthoi <msg> → corresponding channel
/// - unknown /xyz hello → Say with the literal text intact
/// (matches holtburger fall-through)
///
///
///
///
/// Empty / whitespace-only / target-but-no-message inputs are silently
/// dropped — the input field clears and no command goes out.
///
///
public sealed class ChatPanel : IPanel
{
private const int InputBufferMaxLen = 512;
private readonly ChatVM _vm;
private string _input = string.Empty;
// Phase J Tier 3: tracks the chat-tail size between frames so we
// can auto-scroll the scrollable child to the bottom on new
// entries without yanking the user's manual scroll.
private int _lastRenderedCount;
// Phase K.2: one-shot focus request for the chat input. Set by
// FocusInput() (driven by Tab → ToggleChatEntry); the next Render
// call emits SetKeyboardFocusHere immediately before the input
// field and clears the flag. Without the one-shot semantics, the
// panel would steal focus on every frame and the user could never
// click into another widget.
private bool _focusRequested;
// L.0 follow-up: "Copy mode" — when true, render the chat tail as
// a read-only multi-line text widget the user can click+drag to
// select + Ctrl+C to copy. Trades per-line color for selectability;
// user toggles when they want to grab specific text out of the
// log (item names, coordinates, NPC dialogue, etc).
private bool _copyMode;
public ChatPanel(ChatVM vm)
{
_vm = vm ?? throw new ArgumentNullException(nameof(vm));
}
///
public string Id => "acdream.chat";
///
public string Title => "Chat";
///
public bool IsVisible { get; set; } = true;
///
/// Phase K.2: request keyboard focus for the chat input on the
/// NEXT . One-shot — fires once and resets,
/// so callers (e.g. GameWindow's Tab handler subscribing to
/// ToggleChatEntry) can drive it on a single key press
/// without trapping the user permanently in the input field.
///
public void FocusInput() => _focusRequested = true;
///
public void Render(PanelContext ctx, IPanelRenderer renderer)
{
if (!renderer.Begin(Title))
{
renderer.End();
return;
}
// L.0 follow-up: top-of-panel "Copy mode" toggle. When on, the
// chat tail rendering swaps to TextMultilineReadOnly so the
// user can mark + Ctrl+C any text. Off (default) preserves the
// colored per-line render with combat highlights. The checkbox
// sits ABOVE the chat tail (not in the footer) so it's always
// visible regardless of scroll position.
bool copyMode = _copyMode;
if (renderer.Checkbox("Copy mode (select text to Ctrl+C)", ref copyMode))
_copyMode = copyMode;
renderer.Separator();
// Phase J Tier 3: keep the input field at the bottom of the
// window across resizes by reserving footer space and putting
// the chat tail in a scrollable child that fills the rest.
// The reserved footer holds: one Separator + one InputText.
// FrameHeightWithSpacing covers the input; we add a small fudge
// (~6px) for the separator above it.
float footerHeight = renderer.FrameHeightWithSpacing() + 6f;
// Phase I.7: pull the typed-line view so combat entries can
// route through TextColored. Non-combat entries still take
// the plain Text path (visually identical to the I.4 panel).
var lines = _vm.RecentLinesDetailed();
if (_copyMode)
{
// Copy mode: one big read-only multiline text widget
// holding every visible line, joined with newlines. Loses
// per-line color but lets the user click+drag to select
// arbitrary spans of text + Ctrl+C to copy. Sized to fill
// the available space minus the footer.
string joined = lines.Count == 0
? "(no messages yet)"
: string.Join("\n", lines.Select(l => l.Text));
renderer.TextMultilineReadOnly(
"##chattailcopy", joined,
new System.Numerics.Vector2(0f, -footerHeight));
}
else if (renderer.BeginChild("##chattail", new System.Numerics.Vector2(0, -footerHeight)))
{
if (lines.Count == 0)
{
renderer.Text("(no messages yet)");
}
else
{
for (int i = 0; i < lines.Count; i++)
{
var line = lines[i];
if (line.Kind == ChatKind.Combat && line.CombatKind is { } ck)
{
renderer.TextColored(ColorForCombat(ck), line.Text);
}
else
{
renderer.Text(line.Text);
}
}
}
// Auto-scroll to bottom only when a new line was appended
// since the last render. Manual user scroll-up isn't fought
// against; new messages will jump the view back down once
// they arrive.
if (lines.Count > _lastRenderedCount)
{
renderer.SetScrollHereY(1.0f);
}
_lastRenderedCount = lines.Count;
}
if (!_copyMode) renderer.EndChild();
// Phase I.4: input field. Backend implementation clears _input
// on submit per the IPanelRenderer contract.
renderer.Separator();
// Phase K.2: honor a pending FocusInput() request — emit
// SetKeyboardFocusHere immediately before the input widget so
// ImGui (or the future custom backend) applies it to that
// field. One-shot: clear the flag after firing.
if (_focusRequested)
{
renderer.SetKeyboardFocusHere();
_focusRequested = false;
}
if (renderer.InputTextSubmit("##chatinput", ref _input, InputBufferMaxLen, out var submitted)
&& submitted is not null)
{
var trimmed = submitted.Trim();
// Phase J follow-up: client-side commands intercepted before
// the server-bound parse path. Avoids the /help round-trip
// that produced "Unknown command: help" duplicates from
// ACE's command-error replies, AND gives users a discoverable
// local cheat-sheet of acdream's own slash prefixes.
if (TryHandleClientCommand(trimmed))
{
_input = string.Empty;
renderer.End();
return;
}
// Phase J Tier 4: any /-prefixed input that ISN'T one of our
// known verbs gets a local "Unknown command" message instead
// of being broadcast to the server as plain speech. The
// user reported "/ls" / "/mp /path" leaking out as chat —
// a / prefix is a command, never speech. (@-prefixed unknown
// verbs still pass through to ACE because ACE's
// CommandManager intercepts @ server-side and replies with
// its own "Unknown command" / valid command output.)
if (trimmed.Length > 0 && trimmed[0] == '/')
{
string verb = ChatInputParser.GetVerbToken(trimmed);
if (!ChatInputParser.IsKnownVerb(verb))
{
_vm.ShowSystemMessage(
$"Unknown command: {verb}. Type /help for the list of supported commands.");
_input = string.Empty;
renderer.End();
return;
}
}
var parsed = ChatInputParser.Parse(
trimmed,
ChatChannelKind.Say,
_vm.LastIncomingTellSender,
_vm.LastOutgoingTellTarget);
if (parsed is { } p)
{
ctx.Commands.Publish(new SendChatCmd(p.Channel, p.TargetName, p.Text));
}
// Defensive: if the backend ever forgot to clear on submit,
// do it here. Cheap; no harm if already empty.
_input = string.Empty;
}
renderer.End();
}
///
/// Phase I.7: per-severity color for combat-feedback chat lines.
/// Maps onto holtburger's color_for_tags at chat.rs:330-333
/// (info → yellowish, warning → red incoming, error → deep red).
///
public static Vector4 ColorForCombat(CombatLineKind kind) => kind switch
{
CombatLineKind.Info => new Vector4(1.0f, 1.0f, 0.6f, 1.0f),
CombatLineKind.Warning => new Vector4(1.0f, 0.5f, 0.5f, 1.0f),
CombatLineKind.Error => new Vector4(1.0f, 0.3f, 0.3f, 1.0f),
_ => new Vector4(1f, 1f, 1f, 1f),
};
///
/// Phase J follow-up: handle client-side slash commands before
/// the parser passes anything to the server bus. Returns true
/// when the input was consumed (and the caller should clear the
/// buffer + skip the SendChatCmd path); false otherwise.
///
///
/// Recognised client-side commands:
///
/// - /help, /?, /h — render the slash-prefix
/// cheat-sheet locally. Avoids the server's "Unknown command"
/// round-trip when the user just wants to know what they can
/// type.
/// - /clear, /cls — drain the chat log so the
/// panel starts empty.
///
///
private bool TryHandleClientCommand(string trimmed)
{
if (trimmed.Length == 0) return false;
// /help, /?, /h — also @help, @?, @h per ACE's "/ ↔ @" equivalence.
if (EqAny(trimmed, "/help", "/?", "/h", "@help", "@?", "@h"))
{
_vm.ShowSystemMessage(BuildHelpText());
return true;
}
// /clear, /cls — also @clear, @cls.
if (EqAny(trimmed, "/clear", "/cls", "@clear", "@cls"))
{
_vm.Clear();
return true;
}
// /framerate — also @framerate. Prints current FPS to chat.
if (EqAny(trimmed, "/framerate", "@framerate"))
{
_vm.ShowFps();
return true;
}
// /loc — also @loc. Prints current player position to chat.
// ACE has a server-side @loc too; client-side wins here
// (instantaneous + uses our local interpolated position).
if (EqAny(trimmed, "/loc", "@loc"))
{
_vm.ShowLocation();
return true;
}
return false;
}
/// Case-insensitive multi-string equality test.
private static bool EqAny(string s, params string[] options)
{
for (int i = 0; i < options.Length; i++)
if (s.Equals(options[i], StringComparison.OrdinalIgnoreCase)) return true;
return false;
}
///
/// Multi-line cheat-sheet text rendered by /help. ImGui's
/// Text path flows embedded newlines naturally so this lands
/// as one ChatLog entry that visually wraps to several lines.
///
private static string BuildHelpText() =>
"Note: / and @ are equivalent prefixes.\n" +
"Chat: /say (default), /tell , /reply, /retell\n" +
"Channels: /general /trade /fellowship /allegiance\n" +
" /patron /vassals /monarch /covassals\n" +
" /lfg /roleplay /society /olthoi\n" +
"Client: /help (this) /clear /framerate /loc\n" +
"Server: type @acehelp or @acecommands for ACE's full list.";
}