WingMan – Rev 17

Subversion Repositories:
Rev:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using Gma.System.MouseKeyHook;
using MQTTnet.Extensions.ManagedClient;
using MQTTnet.Server;
using WingMan.Bindings;
using WingMan.Communication;
using WingMan.Lobby;
using WingMan.Properties;
using WingMan.Utilities;

namespace WingMan
{
    public partial class WingManForm : Form
    {
        public WingManForm()
        {
            InitializeComponent();

            FormTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
            FormCancellationTokenSource = new CancellationTokenSource();

            MqttCommunication = new MqttCommunication(FormTaskScheduler, FormCancellationTokenSource.Token);
            MqttCommunication.OnClientAuthenticationFailed += OnMqttClientAuthenticationFailed;
            MqttCommunication.OnClientConnectionFailed += OnMqttClientConnectionFailed;
            MqttCommunication.OnClientDisconnected += OnMqttClientDisconnected;
            MqttCommunication.OnClientConnected += OnMqttClientConnected;
            MqttCommunication.OnServerAuthenticationFailed += OnMqttServerAuthenticationFailed;
            MqttCommunication.OnServerStopped += OnMqttServerStopped;
            MqttCommunication.OnServerStarted += OnMqttServerStarted;
            MqttCommunication.OnServerClientConnected += OnMqttServerClientConnected;
            MqttCommunication.OnServerClientDisconnected += OnMqttServerClientDisconnected;

            LocalKeyBindings = new LocalKeyBindings(new List<KeyBinding>());
            RemoteKeyBindings = new RemoteKeyBindings(new ObservableCollection<RemoteKeyBinding>());

            LocalListBoxBindingSource = new BindingSource
            {
                DataSource = LocalKeyBindings.Bindings
            };
            LocalBindingsListBox.DisplayMember = "DisplayName";
            LocalBindingsListBox.ValueMember = "Keys";
            LocalBindingsListBox.DataSource = LocalListBoxBindingSource;

            KeyBindingsExchange = new KeyBindingsExchange
            {
                ExchangeBindings = new List<KeyBindingExchange>()
            };

            RemoteBindingsComboBoxSource = new BindingSource
            {
                DataSource = KeyBindingsExchange.ExchangeBindings
            };
            RemoteBindingsComboBox.DisplayMember = "Nick";
            RemoteBindingsComboBox.ValueMember = "KeyBindings";
            RemoteBindingsComboBox.DataSource = RemoteBindingsComboBoxSource;

            // Start lobby message synchronizer.
            LobbyMessageSynchronizer = new LobbyMessageSynchronizer(MqttCommunication, FormTaskScheduler,
                FormCancellationTokenSource.Token);
            LobbyMessageSynchronizer.OnLobbyMessageReceived += OnLobbyMessageReceived;

            // Start mouse key bindings synchronizer.
            KeyBindingsSynchronizer = new KeyBindingsSynchronizer(LocalKeyBindings, MqttCommunication,
                FormTaskScheduler, FormCancellationTokenSource.Token);
            KeyBindingsSynchronizer.OnMouseKeyBindingsSynchronized += OnMouseKeyBindingsSynchronized;

            // Start mouse key interceptor.
            KeyInterceptor = new KeyInterceptor(RemoteKeyBindings, MqttCommunication, FormTaskScheduler,
                FormCancellationTokenSource.Token);
            KeyInterceptor.OnMouseKeyBindingMatched += OnMouseKeyBindingMatched;

            // Start mouse key simulator.
            KeySimulator = new KeySimulator(LocalKeyBindings, MqttCommunication, FormTaskScheduler,
                FormCancellationTokenSource.Token);
            KeySimulator.OnMouseKeyBindingExecuting += OnMouseKeyBindingExecuting;
        }

        private static CancellationTokenSource FormCancellationTokenSource { get; set; }

        private static TaskScheduler FormTaskScheduler { get; set; }

        private static IKeyboardMouseEvents MouseKeyApplicationHook { get; set; }

        private List<string> MouseKeyCombo { get; set; }

        private LocalKeyBindings LocalKeyBindings { get; }

        private RemoteKeyBindings RemoteKeyBindings { get; }

        private BindingSource LocalListBoxBindingSource { get; }

        private BindingSource RemoteBindingsComboBoxSource { get; }

        private KeyBindingsExchange KeyBindingsExchange { get; }

        public MqttCommunication MqttCommunication { get; set; }

        public LobbyMessageSynchronizer LobbyMessageSynchronizer { get; set; }

        public KeyBindingsSynchronizer KeyBindingsSynchronizer { get; set; }

        public KeyInterceptor KeyInterceptor { get; set; }

        public KeySimulator KeySimulator { get; set; }

        /// <summary>
        ///     Clean up any resources being used.
        /// </summary>
        /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
        protected override void Dispose(bool disposing)
        {
            FormCancellationTokenSource?.Dispose();
            FormCancellationTokenSource = null;

            if (disposing && components != null)
            {
                components.Dispose();
                components = null;
            }

            base.Dispose(disposing);
        }

        private void OnMouseKeyBindingExecuting(object sender, KeyBindingExecutingEventArgs args)
        {
            ActivityTextBox.AppendText(
                $"{Strings.Executing_binding_from_remote_client} : {args.Nick} : {args.Name}{Environment.NewLine}");
        }

        private void OnMouseKeyBindingMatched(object sender, KeyBindingMatchedEventArgs args)
        {
            ActivityTextBox.AppendText(
                $"{Strings.Matched_remote_key_binding} : {args.Nick} : {args.Name} : {string.Join(" + ", args.KeyCombo)}{Environment.NewLine}");
        }

        private void OnMqttServerClientDisconnected(object sender, MqttClientDisconnectedEventArgs e)
        {
            ActivityTextBox.AppendText(
                $"{Strings.Client_disconnected}{Environment.NewLine}");
        }

        private void OnMqttServerClientConnected(object sender, MqttClientConnectedEventArgs e)
        {
            ActivityTextBox.AppendText(
                $"{Strings.Client_connected}{Environment.NewLine}");
        }

        private void OnMqttServerStarted(object sender, EventArgs e)
        {
            ActivityTextBox.AppendText(
                $"{Strings.Server_started}{Environment.NewLine}");
        }

        private void OnMqttServerStopped(object sender, EventArgs e)
        {
            ActivityTextBox.AppendText(
                $"{Strings.Server_stopped}{Environment.NewLine}");
        }

        private void OnMqttClientConnected(object sender, MQTTnet.Client.MqttClientConnectedEventArgs e)
        {
            ActivityTextBox.AppendText(
                $"{Strings.Client_connected}{Environment.NewLine}");
        }

        private void OnMqttClientDisconnected(object sender, MQTTnet.Client.MqttClientDisconnectedEventArgs e)
        {
            ActivityTextBox.AppendText(
                $"{Strings.Client_disconnected}{Environment.NewLine}");
        }

        private void OnMqttClientConnectionFailed(object sender, MqttManagedProcessFailedEventArgs e)
        {
            ActivityTextBox.AppendText(
                $"{Strings.Client_connection_failed}{Environment.NewLine}");
        }

        private void OnMqttServerAuthenticationFailed(object sender, MqttAuthenticationFailureEventArgs e)
        {
            ActivityTextBox.AppendText(
                $"{Strings.Failed_to_authenticate_client} : {e.Exception}{Environment.NewLine}");
        }

        private void OnMqttClientAuthenticationFailed(object sender, MqttAuthenticationFailureEventArgs e)
        {
            ActivityTextBox.AppendText(
                $"{Strings.Failed_to_authenticate_with_server} : {e.Exception}{Environment.NewLine}");
        }

        private void OnMouseKeyBindingsSynchronized(object sender, KeyBindingsSynchronizerEventArgs e)
        {
            ActivityTextBox.AppendText(
                $"{Strings.Synchronized_bindings_with_client} : {e.Bindings.Nick} : {e.Bindings.KeyBindings.Count}{Environment.NewLine}");

            var exchangeBindings = KeyBindingsExchange.ExchangeBindings.FirstOrDefault(exchangeBinding =>
                string.Equals(exchangeBinding.Nick, e.Bindings.Nick, StringComparison.Ordinal));

            // If the nick does not exist then add it.
            if (exchangeBindings == null)
            {
                KeyBindingsExchange.ExchangeBindings.Add(e.Bindings);
                RemoteBindingsComboBoxSource.ResetBindings(false);
                UpdateRemoteItems();
                return;
            }

            // If the bindings for the nick have not changed then do not update.
            if (exchangeBindings.KeyBindings.SequenceEqual(e.Bindings.KeyBindings))
            {
                RemoteBindingsComboBoxSource.ResetBindings(false);
                UpdateRemoteItems();
                return;
            }

            // Update the bindings.
            exchangeBindings.KeyBindings = e.Bindings.KeyBindings;
            RemoteBindingsComboBoxSource.ResetBindings(false);
            UpdateRemoteItems();
        }

        private void UpdateRemoteItems()
        {
            var exchangeBindings = (List<KeyBinding>) RemoteBindingsComboBox.SelectedValue;
            if (exchangeBindings == null)
                return;

            var replaceMouseBindings = new ObservableCollection<RemoteKeyBinding>();
            foreach (var remoteBinding in RemoteKeyBindings.Bindings)
            {
                if (!exchangeBindings.Any(binding =>
                    string.Equals(binding.Name, remoteBinding.Name, StringComparison.Ordinal)))
                    continue;

                replaceMouseBindings.Add(remoteBinding);
            }

            RemoteKeyBindings.Bindings = replaceMouseBindings;

            RemoteBindingsListBox.Items.Clear();
            RemoteBindingsListBox.DisplayMember = "Name";
            RemoteBindingsListBox.ValueMember = "Name";
            var bindings = exchangeBindings.Select(binding => (object) binding.Name).ToArray();
            if (bindings.Length == 0)
                return;

            RemoteBindingsListBox.Items.AddRange(bindings);
        }

        private void OnLobbyMessageReceived(object sender, LobbyMessageReceivedEventArgs e)
        {
            LobbyTextBox.AppendText($"{e.Nick} : {e.Message}{Environment.NewLine}");
        }

        private void AddressTextBoxClick(object sender, EventArgs e)
        {
            Address.BackColor = Color.Empty;
        }

        private void PortTextBoxClick(object sender, EventArgs e)
        {
            Port.BackColor = Color.Empty;
        }

        private async void HostButtonClickAsync(object sender, EventArgs e)
        {
            // Stop the MQTT server if it is running.
            if (MqttCommunication.Running)
            {
                await MqttCommunication.Stop();
                HostButton.BackColor = Color.Empty;

                // Enable controls.
                ConnectButton.Enabled = true;
                Address.Enabled = true;
                Port.Enabled = true;
                Nick.Enabled = true;
                Password.Enabled = true;
                return;
            }

            if (!ValidateAddressPort(out var ipAddress, out var port, out var nick, out var password))
                return;

            // Start the MQTT server.
            if (!await MqttCommunication
                .Start(MqttCommunicationType.Server, ipAddress, port, nick, password))
            {
                ActivityTextBox.AppendText(
                    $"{Strings.Failed_starting_server}{Environment.NewLine}");
                return;
            }

            HostButton.BackColor = Color.Aquamarine;

            // Disable controls
            ConnectButton.Enabled = false;
            Address.Enabled = false;
            Port.Enabled = false;
            Nick.Enabled = false;
            Password.Enabled = false;
        }

        private bool ValidateAddressPort(out IPAddress address, out int port, out string nick, out string password)
        {
            address = IPAddress.Any;
            port = 0;
            nick = string.Empty;
            password = string.Empty;

            if (string.IsNullOrEmpty(Address.Text) &&
                string.IsNullOrEmpty(Port.Text) &&
                string.IsNullOrEmpty(Nick.Text) &&
                string.IsNullOrEmpty(Password.Text))
            {
                Address.BackColor = Color.LightPink;
                Port.BackColor = Color.LightPink;
                Nick.BackColor = Color.LightPink;
                Password.BackColor = Color.LightPink;
                return false;
            }

            if (!IPAddress.TryParse(Address.Text, out address))
            {
                Address.BackColor = Color.LightPink;
                return false;
            }

            if (!uint.TryParse(Port.Text, out var uPort))
            {
                Port.BackColor = Color.LightPink;
                return false;
            }

            port = (int) uPort;

            if (string.IsNullOrEmpty(Nick.Text))
            {
                Nick.BackColor = Color.LightPink;
                return false;
            }

            nick = Nick.Text;

            if (string.IsNullOrEmpty(Password.Text))
            {
                Password.BackColor = Color.LightPink;
                return false;
            }

            password = AES.ExpandKey(Password.Text);

            Address.BackColor = Color.Empty;
            Port.BackColor = Color.Empty;
            Nick.BackColor = Color.Empty;
            Password.BackColor = Color.Empty;

            return true;
        }

        private async void ConnectButtonClickAsync(object sender, EventArgs e)
        {
            if (MqttCommunication.Running)
            {
                await MqttCommunication.Stop();
                ConnectButton.Text = Strings.Connect;
                ConnectButton.BackColor = Color.Empty;

                Address.Enabled = true;
                Port.Enabled = true;
                Nick.Enabled = true;
                Password.Enabled = true;
                HostButton.Enabled = true;
                return;
            }

            if (!ValidateAddressPort(out var ipAddress, out var port, out var nick, out var password))
                return;

            if (!await MqttCommunication
                .Start(MqttCommunicationType.Client, ipAddress, port, nick, password))
            {
                ActivityTextBox.AppendText(
                    $"{Strings.Failed_starting_client}{Environment.NewLine}");
                return;
            }

            ConnectButton.Text = Strings.Disconnect;
            ConnectButton.BackColor = Color.Aquamarine;

            HostButton.Enabled = false;
            Address.Enabled = false;
            Port.Enabled = false;
            Nick.Enabled = false;
            Password.Enabled = false;
        }

        private async void LobbySayTextBoxKeyDown(object sender, KeyEventArgs e)
        {
            if (e.KeyCode != Keys.Enter)
                return;

            await LobbyMessageSynchronizer.Broadcast(LobbySayTextBox.Text);

            LobbySayTextBox.Text = string.Empty;
        }

        private void LocalAddBindingButtonClick(object sender, EventArgs e)
        {
            if (string.IsNullOrEmpty(LocalNameTextBox.Text))
            {
                LocalNameTextBox.BackColor = Color.LightPink;
                return;
            }

            // Only unique names allowed.
            if (LocalKeyBindings.Bindings.Any(binding =>
                string.Equals(binding.Name, LocalNameTextBox.Text, StringComparison.Ordinal)))
            {
                LocalNameTextBox.BackColor = Color.LightPink;
                LocalBindingsListBox.BackColor = Color.LightPink;
                return;
            }

            LocalNameTextBox.BackColor = Color.Empty;
            LocalBindingsListBox.BackColor = Color.Empty;

            ShowOverlayPanel();

            MouseKeyCombo = new List<string>();

            MouseKeyApplicationHook = Hook.GlobalEvents();
            MouseKeyApplicationHook.KeyUp += LocalMouseKeyHookOnKeyUp;
            MouseKeyApplicationHook.KeyDown += LocalMouseKeyHookOnKeyDown;
        }

        private void ShowOverlayPanel()
        {
            OverlayPanel.BringToFront();
            OverlayPanel.Visible = true;
            OverlayPanel.Invalidate();
        }

        private async void LocalMouseKeyHookOnKeyUp(object sender, KeyEventArgs e)
        {
            LocalKeyBindings.Bindings.Add(new KeyBinding(LocalNameTextBox.Text, MouseKeyCombo));

            LocalListBoxBindingSource.ResetBindings(false);

            MouseKeyApplicationHook.KeyDown -= LocalMouseKeyHookOnKeyDown;
            MouseKeyApplicationHook.KeyUp -= LocalMouseKeyHookOnKeyUp;

            MouseKeyApplicationHook.Dispose();

            LocalNameTextBox.Text = string.Empty;
            HideOverlayPanel();

            await SaveLocalMouseKeyBindings();
        }

        private void HideOverlayPanel()
        {
            OverlayPanel.SendToBack();
            OverlayPanel.Visible = false;
            OverlayPanel.Invalidate();
        }

        private void LocalMouseKeyHookOnKeyDown(object sender, KeyEventArgs e)
        {
            e.SuppressKeyPress = true;

            KeyConversion.KeysToString.TryGetValue((byte) e.KeyCode, out var key);

            MouseKeyCombo.Add(key);
        }

        private void LocalNameTextBoxClick(object sender, EventArgs e)
        {
            LocalNameTextBox.BackColor = Color.Empty;
        }

        private async void LocalBindingsRemoveButtonClick(object sender, EventArgs e)
        {
            var helmBinding = (KeyBinding) LocalBindingsListBox.SelectedItem;
            if (helmBinding == null)
                return;

            LocalKeyBindings.Bindings.Remove(helmBinding);
            LocalListBoxBindingSource.ResetBindings(false);

            await SaveLocalMouseKeyBindings();
        }

        private async void LobbySayButtonClick(object sender, EventArgs e)
        {
            await LobbyMessageSynchronizer.Broadcast(LobbySayTextBox.Text);

            LobbySayTextBox.Text = string.Empty;
        }

        private void RemoteBindingsComboBoxSelectionChangeCompleted(object sender, EventArgs e)
        {
            UpdateRemoteItems();
        }

        private async void WingManFormOnLoad(object sender, EventArgs e)
        {
            await LoadLocalMouseKeyBindings();

            await LoadRemoteMouseKeyBindings();
        }

        private void RemoteBindingsBindButtonClicked(object sender, EventArgs e)
        {
            if (string.IsNullOrEmpty((string) RemoteBindingsListBox.SelectedItem))
            {
                RemoteBindingsListBox.BackColor = Color.LightPink;
                return;
            }

            RemoteBindingsListBox.BackColor = Color.Empty;

            ShowOverlayPanel();

            MouseKeyCombo = new List<string>();

            MouseKeyApplicationHook = Hook.GlobalEvents();
            MouseKeyApplicationHook.KeyUp += RemoteMouseKeyHookOnKeyUp;
            MouseKeyApplicationHook.KeyDown += RemoteMouseKeyHookOnKeyDown;
        }

        private void RemoteBindingsUnbindButtonClicked(object sender, EventArgs e)
        {
            var item = (string) RemoteBindingsListBox.SelectedItem;
            if (string.IsNullOrEmpty(item))
            {
                RemoteBindingsListBox.BackColor = Color.LightPink;
                return;
            }

            RemoteBindingsListBox.BackColor = Color.Empty;

            var remoteKeyBinding = RemoteKeyBindings.Bindings.FirstOrDefault(binding =>
                string.Equals(binding.Name, item, StringComparison.Ordinal));

            if (remoteKeyBinding == null)
                return;

            RemoteKeyBindings.Bindings.Remove(remoteKeyBinding);
            RemoteBindingsBindToBox.Text = string.Empty;
        }

        private void RemoteMouseKeyHookOnKeyDown(object sender, KeyEventArgs e)
        {
            e.SuppressKeyPress = true;

            KeyConversion.KeysToString.TryGetValue((byte) e.KeyCode, out var key);

            MouseKeyCombo.Add(key);
        }

        private async void RemoteMouseKeyHookOnKeyUp(object sender, KeyEventArgs e)
        {
            RemoteKeyBindings.Bindings.Add(new RemoteKeyBinding(RemoteBindingsComboBox.Text,
                (string) RemoteBindingsListBox.SelectedItem, MouseKeyCombo));

            MouseKeyApplicationHook.KeyDown -= RemoteMouseKeyHookOnKeyDown;
            MouseKeyApplicationHook.KeyUp -= RemoteMouseKeyHookOnKeyUp;

            MouseKeyApplicationHook.Dispose();

            RemoteBindingsBindToBox.Text = string.Join(" + ", MouseKeyCombo);
            HideOverlayPanel();

            await SaveRemoteMouseKeyBindings();
        }

        private void RemoteBindingsListBoxSelectedValueChanged(object sender, EventArgs e)
        {
            RemoteBindingsBindToBox.Text = "";

            var name = (string) RemoteBindingsListBox.SelectedItem;
            if (string.IsNullOrEmpty(name))
                return;

            var nick = RemoteBindingsComboBox.Text;
            if (string.IsNullOrEmpty(nick))
                return;

            foreach (var binding in RemoteKeyBindings.Bindings)
            {
                if (!string.Equals(binding.Nick, nick) || !string.Equals(binding.Name, name))
                    continue;

                RemoteBindingsBindToBox.Text = string.Join(" + ", binding.Keys);
                break;
            }
        }

        #region Saving and loading

        private async Task SaveLocalMouseKeyBindings()
        {
            try
            {
                using (var memoryStream = new MemoryStream())
                {
                    LocalKeyBindings.XmlSerializer.Serialize(memoryStream, LocalKeyBindings);

                    memoryStream.Position = 0L;

                    using (var fileStream = new FileStream("LocalKeyBindings.xml", FileMode.Create))
                    {
                        await memoryStream.CopyToAsync(fileStream);
                    }
                }
            }
            catch (Exception)
            {
                ActivityTextBox.AppendText(
                    $"{Strings.Failed_saving_local_bindings}{Environment.NewLine}");
            }
        }

        private async Task LoadLocalMouseKeyBindings()
        {
            try
            {
                using (var fileStream = new FileStream("LocalKeyBindings.xml", FileMode.Open))
                {
                    using (var memoryStream = new MemoryStream())
                    {
                        await fileStream.CopyToAsync(memoryStream);

                        memoryStream.Position = 0L;

                        var loadedBindings =
                            (LocalKeyBindings) LocalKeyBindings.XmlSerializer.Deserialize(memoryStream);

                        foreach (var binding in loadedBindings.Bindings) LocalKeyBindings.Bindings.Add(binding);

                        LocalListBoxBindingSource.ResetBindings(false);
                    }
                }
            }
            catch (Exception)
            {
                ActivityTextBox.AppendText(
                    $"{Strings.Failed_loading_local_bindings}{Environment.NewLine}");
            }
        }

        private async Task SaveRemoteMouseKeyBindings()
        {
            try
            {
                using (var memoryStream = new MemoryStream())
                {
                    RemoteKeyBindings.XmlSerializer.Serialize(memoryStream, RemoteKeyBindings);

                    memoryStream.Position = 0L;

                    using (var fileStream = new FileStream("RemoteKeyBindings.xml", FileMode.Create))
                    {
                        await memoryStream.CopyToAsync(fileStream);
                    }
                }
            }
            catch (Exception)
            {
                ActivityTextBox.AppendText(
                    $"{Strings.Failed_saving_remote_bindings}{Environment.NewLine}");
            }
        }

        private async Task LoadRemoteMouseKeyBindings()
        {
            try
            {
                using (var fileStream = new FileStream("RemoteKeyBindings.xml", FileMode.Open))
                {
                    using (var memoryStream = new MemoryStream())
                    {
                        await fileStream.CopyToAsync(memoryStream);

                        memoryStream.Position = 0L;

                        var loadedBindings =
                            (RemoteKeyBindings) RemoteKeyBindings.XmlSerializer.Deserialize(memoryStream);

                        foreach (var binding in loadedBindings.Bindings) RemoteKeyBindings.Bindings.Add(binding);
                    }
                }
            }
            catch (Exception)
            {
                ActivityTextBox.AppendText(
                    $"{Strings.Failed_loading_remote_bindings}{Environment.NewLine}");
            }
        }

        #endregion
    }
}