using System.Text.RegularExpressions;
using EchoHub.Client.Themes;
using EchoHub.Core.DTOs;
using EchoHub.Core.Models;
using Terminal.Gui.App;
using Terminal.Gui.Configuration;
using Terminal.Gui.Input;
using Terminal.Gui.ViewBase;
using Terminal.Gui.Views;
using Attribute = Terminal.Gui.Drawing.Attribute;
namespace EchoHub.Client.UI;
///
/// Main Terminal.Gui window for the EchoHub chat client.
///
public sealed class MainWindow : Runnable
{
private readonly IApplication _app;
private readonly ListView _channelList;
private readonly ListView _messageList;
private readonly TextView _inputField;
private readonly FrameView _chatFrame;
private readonly FrameView _inputFrame;
private readonly Label _statusLabel;
private readonly Label _topicLabel;
private MenuBar _menuBar;
// Online users panel
private readonly FrameView _usersFrame;
private readonly ListView _usersList;
private readonly UserListSource _usersListSource;
private bool _usersPanelVisible = true;
private const int UsersPanelWidth = 22;
private static readonly Key F2Key = Key.F2;
internal static readonly string AppVersion =
typeof(MainWindow).Assembly.GetName().Version?.ToString(3) ?? "?";
// Cached Key constants — compare via .KeyCode to avoid Key.Equals (which also checks Handled)
private static readonly Key EnterKey = Key.Enter;
private static readonly Key NewlineKey = Key.N.WithCtrl;
private static readonly Key AltQKey = Key.Q.WithAlt;
private static readonly Key TabKey = Key.Tab;
// Available slash commands for Tab autocomplete
private static readonly string[] SlashCommands =
[
"/status", "/nick", "/color", "/theme", "/send",
"/avatar", "/profile", "/servers", "/join", "/leave",
"/topic", "/users", "/kick", "/ban", "/unban",
"/mute", "/unmute", "/role", "/nuke", "/quit", "/help"
];
private readonly List _channelNames = [];
private readonly Dictionary> _channelMessages = [];
private readonly Dictionary _channelUnread = [];
private readonly Dictionary _channelTopics = [];
private readonly ChannelListSource _channelListSource;
private string _currentChannel = string.Empty;
private string _currentUser = string.Empty;
private int _lastChatWidth;
///
/// Fired when the user selects a channel. Parameter is the channel name.
///
public event Action? OnChannelSelected;
///
/// Fired when the user presses Enter in the input field. Parameters: channel name, message content.
///
public event Action? OnMessageSubmitted;
///
/// Fired when the user requests to connect via the menu.
///
public event Action? OnConnectRequested;
///
/// Fired when the user requests to disconnect via the menu.
///
public event Action? OnDisconnectRequested;
///
/// Fired when the user requests to open their profile panel.
///
public event Action? OnProfileRequested;
///
/// Fired when the user requests to set their status.
///
public event Action? OnStatusRequested;
///
/// Fired when the user selects a theme from the menu. Parameter is the theme name.
///
public event Action? OnThemeSelected;
///
/// Fired when the user requests to view saved servers.
///
public event Action? OnSavedServersRequested;
///
/// Fired when the user requests to create a new channel.
///
public event Action? OnCreateChannelRequested;
///
/// Fired when the user requests to delete the current channel.
///
public event Action? OnDeleteChannelRequested;
public MainWindow(IApplication app)
{
_app = app;
Arrangement = ViewArrangement.Fixed;
// Menu bar at the top
_menuBar = BuildMenuBar();
Add(_menuBar);
// Left panel - channels
var channelsFrame = new FrameView
{
Title = "Channels",
X = 0,
Y = 1, // below menu bar
Width = 22,
Height = Dim.Fill(1) // leave room for status bar
};
_channelList = new ListView
{
X = 0,
Y = 0,
Width = Dim.Fill(),
Height = Dim.Fill()
};
_channelListSource = new ChannelListSource();
_channelList.Source = _channelListSource;
_channelList.ValueChanged += OnChannelListSelectionChanged;
channelsFrame.Add(_channelList);
Add(channelsFrame);
// Topic bar — sits above the chat frame in the right column
_topicLabel = new Label
{
Text = "",
X = 22,
Y = 1,
Width = Dim.Fill(UsersPanelWidth),
Height = 1,
Visible = false
};
Add(_topicLabel);
// Center panel - messages
_chatFrame = new FrameView
{
Title = "Chat",
X = 22,
Y = 1, // below menu bar (shifts to 2 when topic is visible)
Width = Dim.Fill(UsersPanelWidth),
Height = Dim.Fill(6) // leave room for input area and status bar
};
_messageList = new ListView
{
X = 0,
Y = 0,
Width = Dim.Fill(),
Height = Dim.Fill()
};
_messageList.Source = new ChatListSource();
_chatFrame.Add(_messageList);
Add(_chatFrame);
// Bottom input area
_inputFrame = new FrameView
{
Title = "Message \u2502 Enter=send \u2502 Ctrl+N=newline \u2502 Tab=complete",
X = 22,
Y = Pos.Bottom(_chatFrame),
Width = Dim.Fill(UsersPanelWidth),
Height = 5
};
_inputField = new TextView
{
X = 0,
Y = 0,
Width = Dim.Fill(),
Height = Dim.Fill(),
WordWrap = true
};
_inputField.KeyDown += OnInputKeyDown;
_inputFrame.Add(_inputField);
Add(_inputFrame);
// Right panel - online users
_usersFrame = new FrameView
{
Title = "Users",
X = Pos.AnchorEnd(UsersPanelWidth),
Y = 1,
Width = UsersPanelWidth,
Height = Dim.Fill(1)
};
_usersList = new ListView
{
X = 0,
Y = 0,
Width = Dim.Fill(),
Height = Dim.Fill()
};
_usersListSource = new UserListSource();
_usersList.Source = _usersListSource;
_usersFrame.Add(_usersList);
Add(_usersFrame);
// Status bar at the very bottom
_statusLabel = new Label
{
Text = "Disconnected",
X = 0,
Y = Pos.AnchorEnd(1),
Width = Dim.Fill(),
Height = 1
};
_statusLabel.SetScheme(SchemeManager.GetScheme("Menu"));
Add(_statusLabel);
// Apply our custom color schemes to all views
ApplyColorSchemes();
// Re-wrap messages when the chat area is resized
// Subscribe to both ListView and FrameView viewport changes for reliable resize detection
_messageList.ViewportChanged += (_, _) => OnChatViewportChanged();
_chatFrame.ViewportChanged += (_, _) => OnChatViewportChanged();
// Window-level key handling for Alt+Q (quit), F2 (toggle users panel)
KeyDown += OnWindowKeyDown;
}
///
/// Applies the currently registered color schemes to all views.
/// Call after theme changes to refresh colors.
///
public void ApplyColorSchemes()
{
var baseScheme = SchemeManager.GetScheme("Base");
var menuScheme = SchemeManager.GetScheme("Menu");
if (baseScheme is not null)
{
this.SetScheme(baseScheme);
// Propagate to all child views that should use the base scheme
foreach (var sub in SubViews)
{
if (sub != _menuBar && sub != _statusLabel && sub != _topicLabel)
sub.SetScheme(baseScheme);
}
}
if (menuScheme is not null)
{
_menuBar.SetScheme(menuScheme);
_statusLabel.SetScheme(menuScheme);
_topicLabel.SetScheme(menuScheme);
}
}
///
/// Builds the menu bar with File, Server, User menus and a theme submenu.
///
private MenuBar BuildMenuBar()
{
// Build theme menu items and prepend them with a separator header
var themeItems = new List