using System.Diagnostics;
using System.Text.RegularExpressions;
using EchoHub.Client.Services;
using EchoHub.Client.Themes;
using EchoHub.Client.UI.Chat;
using EchoHub.Client.UI.Helpers;
using EchoHub.Client.UI.ListSources;
using EchoHub.Core.DTOs;
using EchoHub.Core.Models;
using Terminal.Gui.App;
using Terminal.Gui.Configuration;
using Terminal.Gui.Drawing;
using Terminal.Gui.Input;
using Terminal.Gui.Text;
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 partial 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;
private static readonly Key CtrlKKey = Key.K.WithCtrl;
// 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", "/test-sound", "/quit", "/help"
];
private readonly List _channelNames = [];
private readonly Dictionary _channelTopics = [];
private readonly Dictionary _channelPublic = [];
private readonly ChannelListSource _channelListSource;
private readonly ChatMessageManager _messageManager;
private string _connectionStatus = "Disconnected";
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 logout (disconnect + revoke session).
///
public event Action? OnLogoutRequested;
///
/// 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 check for updates.
///
public event Action? OnCheckForUpdatesRequested;
///
/// Fired when the user requests to view saved servers.
///
public event Action? OnSavedServersRequested;
///
/// Fired when the user scrolls to the top of the message list and older messages should be loaded.
///
public event Action? OnLoadMoreRequested;
///
/// 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;
///
/// Fired when the user requests to rollback to the previous version.
///
public event Action? OnRollbackRequested;
///
/// Fired when the user activates (Enter/click) an audio message. Parameters: attachmentUrl, fileName.
///
public event Action? OnAudioPlayRequested;
///
/// Fired when the user activates (Enter/click) a file message. Parameters: attachmentUrl, fileName.
///
public event Action? OnFileDownloadRequested;
///
/// Fired when the user activates a username (in userlist or message). Parameter is the username.
///
public event Action? OnUserProfileRequested;
///
/// Fired when the user activates a #channel reference in a message. Parameter is the channel name.
///
public event Action? OnChannelJoinRequested;
///
/// Fired when the user requests to open the search dialog (via menu or Ctrl+K).
///
public event Action? OnSearchRequested;
public MainWindow(IApplication app, ChatMessageManager messageManager)
{
_app = app;
_messageManager = messageManager;
_messageManager.MessagesChanged += OnMessagesChanged;
_messageManager.HistoryPrepended += OnHistoryPrepended;
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();
_messageList.Accepting += OnMessageListAccepting;
_messageList.VerticalScrollBar.Scrolled += OnMessageListVerticalScrollBarScrolled;
_messageList.VerticalScrollBar.Visible = true;
_chatFrame.Add(_messageList);
Add(_chatFrame);
// Bottom input area
_inputFrame = new FrameView
{
Title = "Message \u2502 Enter=send \u2502 Ctrl+N=newline \u2502 Tab=complete \u2502 Ctrl+K=search",
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;
_inputField.ContentsChanged += OnInputContentsChanged;
_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;
_usersList.Accepting += OnUsersListAccepting;
_usersFrame.Add(_usersList);
Add(_usersFrame);
// Status bar at the very bottom — custom drawing for colored connection state
_statusLabel = new Label
{
Text = "",
X = 0,
Y = Pos.AnchorEnd(1),
Width = Dim.Fill(),
Height = 1
};
_statusLabel.SetScheme(SchemeManager.GetScheme("Menu"));
_statusLabel.DrawingContent += OnStatusBarDrawContent;
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