using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronIO;
using Crestron.SimplSharp.Net.Http;
using Crestron.SimplSharp.WebScripting;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using PepperDash.Core;
using PepperDash.Core.Logging;
using PepperDash.Essentials.AppServer;
using PepperDash.Essentials.AppServer.Messengers;
using PepperDash.Essentials.Core;
using PepperDash.Essentials.Core.Config;
using PepperDash.Essentials.Core.CrestronIO;
using PepperDash.Essentials.Core.DeviceInfo;
using PepperDash.Essentials.Core.DeviceTypeInterfaces;
using PepperDash.Essentials.Core.Lighting;
using PepperDash.Essentials.Core.Monitoring;
using PepperDash.Essentials.Core.Queues;
using PepperDash.Essentials.Core.Routing;
using PepperDash.Essentials.Core.Shades;
using PepperDash.Essentials.Core.Web;
using PepperDash.Essentials.Devices.Common.AudioCodec;
using PepperDash.Essentials.Devices.Common.Cameras;
using PepperDash.Essentials.Devices.Common.Displays;
using PepperDash.Essentials.Devices.Common.Lighting;
using PepperDash.Essentials.Devices.Common.SoftCodec;
using PepperDash.Essentials.Devices.Common.VideoCodec;
using PepperDash.Essentials.Room.MobileControl;
using PepperDash.Essentials.RoomBridges;
using PepperDash.Essentials.Services;
using PepperDash.Essentials.WebApiHandlers;
using PepperDash.Essentials.WebSocketServer;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using WebSocketSharp;

namespace PepperDash.Essentials
{
    /// <summary>
    /// Represents a MobileControlSystemController
    /// </summary>
    public class MobileControlSystemController : EssentialsDevice, IMobileControl
    {
        private bool _initialized = false;
        private const long ServerReconnectInterval = 5000;
        private const long PingInterval = 25000;

        private readonly Dictionary<string, List<IMobileControlAction>> _actionDictionary =
            new Dictionary<string, List<IMobileControlAction>>(
                StringComparer.InvariantCultureIgnoreCase
            );

        public Dictionary<string, List<IMobileControlAction>> ActionDictionary => _actionDictionary;

        private readonly GenericQueue _receiveQueue;
        private readonly List<MobileControlBridgeBase> _roomBridges =
            new List<MobileControlBridgeBase>();

        private readonly Dictionary<string, IMobileControlMessenger> _messengers =
            new Dictionary<string, IMobileControlMessenger>();

        private readonly Dictionary<string, IMobileControlMessenger> _defaultMessengers =
            new Dictionary<string, IMobileControlMessenger>();

        private readonly GenericQueue _transmitToServerQueue;

        private readonly GenericQueue _transmitToClientsQueue;

        private bool _disableReconnect;
        private WebSocket _wsClient2;

        /// <summary>
        /// Gets or sets the ApiService
        /// </summary>
        public MobileControlApiService ApiService { get; private set; }

        public List<MobileControlBridgeBase> RoomBridges => _roomBridges;

        private readonly MobileControlWebsocketServer _directServer;

        public MobileControlWebsocketServer DirectServer => _directServer;

        private readonly CCriticalSection _wsCriticalSection = new CCriticalSection();

        /// <summary>
        /// Gets or sets the SystemUrl
        /// </summary>
        public string SystemUrl; //set only from SIMPL Bridge!

        public bool Connected => _wsClient2 != null && _wsClient2.IsAlive;

        private IEssentialsRoomCombiner _roomCombiner;

        public string SystemUuid
        {
            get
            {
                // Check to see if the SystemUuid value is populated. If not populated from configuration, check for value from SIMPL bridge.
                if (
                    !string.IsNullOrEmpty(ConfigReader.ConfigObject.SystemUuid)
                    && ConfigReader.ConfigObject.SystemUuid != "missing url"
                )
                {
                    return ConfigReader.ConfigObject.SystemUuid;
                }

                this.LogWarning(
                    "No system_url value defined in config.  Checking for value from SIMPL Bridge."
                );

                if (!string.IsNullOrEmpty(SystemUrl))
                {
                    this.LogError(
                        "No system_url value defined in config or SIMPL Bridge.  Unable to connect to Mobile Control."
                    );
                    return string.Empty;
                }

                var result = Regex.Match(SystemUrl, @"https?:\/\/.*\/systems\/(.*)\/#.*");
                string uuid = result.Groups[1].Value;
                return uuid;
            }
        }

        /// <summary>
        /// Gets or sets the ApiOnlineAndAuthorized
        /// </summary>
        public BoolFeedback ApiOnlineAndAuthorized { get; private set; }

        /// <summary>
        /// Used for tracking HTTP debugging
        /// </summary>
        private bool _httpDebugEnabled;

        private bool _isAuthorized;

        /// <summary>
        /// Tracks if the system is authorized to the API server
        /// </summary>
        public bool IsAuthorized
        {
            get { return _isAuthorized; }
            private set
            {
                if (value == _isAuthorized)
                    return;

                _isAuthorized = value;
                ApiOnlineAndAuthorized.FireUpdate();
            }
        }

        private DateTime _lastAckMessage;

        public DateTime LastAckMessage => _lastAckMessage;

        private CTimer _pingTimer;

        private CTimer _serverReconnectTimer;
        private LogLevel _wsLogLevel = LogLevel.Error;

        /// <summary>
        ///
        /// </summary>
        /// <param name="key"></param>
        /// <param name="name"></param>
        /// <param name="config"></param>
        public MobileControlSystemController(string key, string name, MobileControlConfig config)
            : base(key, name)
        {
            Config = config;

            // The queue that will collect the incoming messages in the order they are received
            //_receiveQueue = new ReceiveQueue(key, ParseStreamRx);
            _receiveQueue = new GenericQueue(
                key + "-rxqueue",
                Crestron.SimplSharpPro.CrestronThread.Thread.eThreadPriority.HighPriority,
                25
            );

            // The queue that will collect the outgoing messages in the order they are received
            _transmitToServerQueue = new GenericQueue(
                key + "-txqueue",
                Crestron.SimplSharpPro.CrestronThread.Thread.eThreadPriority.HighPriority,
                25
            );

            if (Config.DirectServer != null && Config.DirectServer.EnableDirectServer)
            {
                _directServer = new MobileControlWebsocketServer(
                    Key + "-directServer",
                    Config.DirectServer.Port,
                    this
                );
                DeviceManager.AddDevice(_directServer);

                _transmitToClientsQueue = new GenericQueue(
                    key + "-clienttxqueue",
                    Crestron.SimplSharpPro.CrestronThread.Thread.eThreadPriority.HighPriority,
                    25
                );
            }

            Host = config.ServerUrl;
            if (!Host.StartsWith("http"))
            {
                Host = "https://" + Host;
            }

            ApiService = new MobileControlApiService(Host);

            this.LogInformation(
                "Mobile UI controller initializing for server:{0}",
                config.ServerUrl
            );

            if (Global.Platform == eDevicePlatform.Appliance)
            {
                AddConsoleCommands();
            }

            AddPreActivationAction(() => LinkSystemMonitorToAppServer());

            AddPreActivationAction(() => SetupDefaultDeviceMessengers());

            AddPreActivationAction(() => SetupDefaultRoomMessengers());

            AddPreActivationAction(() => AddWebApiPaths());

            AddPreActivationAction(() =>
            {
                _roomCombiner = DeviceManager.AllDevices.OfType<IEssentialsRoomCombiner>().FirstOrDefault();

                if (_roomCombiner == null)
                    return;

                _roomCombiner.RoomCombinationScenarioChanged += OnRoomCombinationScenarioChanged;
            });

            CrestronEnvironment.ProgramStatusEventHandler +=
                CrestronEnvironment_ProgramStatusEventHandler;

            ApiOnlineAndAuthorized = new BoolFeedback(() =>
            {
                if (_wsClient2 == null)
                    return false;

                return _wsClient2.IsAlive && IsAuthorized;
            });
        }

        private void SetupDefaultRoomMessengers()
        {
            this.LogVerbose("Setting up room messengers");

            foreach (var room in DeviceManager.AllDevices.OfType<IEssentialsRoom>())
            {
                this.LogVerbose(
                    "Setting up room messengers for room: {key}",
                    room.Key
                );

                var messenger = new MobileControlEssentialsRoomBridge(room);

                messenger.AddParent(this);

                _roomBridges.Add(messenger);

                AddDefaultDeviceMessenger(messenger);

                this.LogVerbose(
                    "Attempting to set up default room messengers for room: {0}",
                    room.Key
                );

                if (room is IRoomEventSchedule)
                {
                    this.LogInformation("Setting up event schedule messenger for room: {key}", room.Key);

                    var scheduleMessenger = new RoomEventScheduleMessenger(
                        $"{room.Key}-schedule-{Key}",
                        string.Format("/room/{0}", room.Key),
                        room as IRoomEventSchedule
                    );

                    AddDefaultDeviceMessenger(scheduleMessenger);
                }

                if (room is ITechPassword)
                {
                    this.LogInformation("Setting up tech password messenger for room: {key}", room.Key);

                    var techPasswordMessenger = new ITechPasswordMessenger(
                        $"{room.Key}-techPassword-{Key}",
                        string.Format("/room/{0}", room.Key),
                        room as ITechPassword
                    );

                    AddDefaultDeviceMessenger(techPasswordMessenger);
                }

                if (room is IShutdownPromptTimer)
                {
                    this.LogInformation("Setting up shutdown prompt timer messenger for room: {key}", this, room.Key);

                    var shutdownPromptTimerMessenger = new IShutdownPromptTimerMessenger(
                        $"{room.Key}-shutdownPromptTimer-{Key}",
                        string.Format("/room/{0}", room.Key),
                        room as IShutdownPromptTimer
                    );

                    AddDefaultDeviceMessenger(shutdownPromptTimerMessenger);
                }

                if (room is ILevelControls levelControls)
                {
                    this.LogInformation("Setting up level controls messenger for room: {key}", this, room.Key);

                    var levelControlsMessenger = new ILevelControlsMessenger(
                        $"{room.Key}-levelControls-{Key}",
                        $"/room/{room.Key}",
                        levelControls
                    );

                    AddDefaultDeviceMessenger(levelControlsMessenger);
                }
            }
        }

        /// <summary>
        /// Set up the messengers for each device type
        /// </summary>
        private void SetupDefaultDeviceMessengers()
        {
            bool messengerAdded = false;

            var allDevices = DeviceManager.AllDevices.Where((d) => !(d is IEssentialsRoom));

            this.LogInformation(
                "All Devices that aren't rooms count: {0}",
                allDevices?.Count()
            );

            var count = allDevices.Count();

            foreach (var device in allDevices)
            {
                try
                {
                    this.LogVerbose(
                        "Attempting to set up device messengers for {deviceKey}",
                        device.Key
                    );

                    // StatusMonitorBase which is prop of ICommunicationMonitor is not a PepperDash.Core.Device, but is in the device array
                    if (device is ICommunicationMonitor)
                    {
                        this.LogVerbose(
                            "Checking if {deviceKey} implements ICommunicationMonitor",
                            device.Key
                        );

                        if (!(device is ICommunicationMonitor commMonitor))
                        {
                            this.LogDebug(
                                "{deviceKey} does not implement ICommunicationMonitor. Skipping CommunicationMonitorMessenger",
                                device.Key
                            );

                            this.LogDebug("Created all messengers for {deviceKey}. Devices Left: {deviceCount}", device.Key, --count);

                            continue;
                        }

                        this.LogDebug(
                            "Adding CommunicationMonitorMessenger for {deviceKey}",
                            device.Key
                        );

                        var commMessenger = new ICommunicationMonitorMessenger(
                            $"{device.Key}-commMonitor-{Key}",
                            string.Format("/device/{0}", device.Key),
                            commMonitor
                        );

                        AddDefaultDeviceMessenger(commMessenger);

                        messengerAdded = true;
                    }

                    if (device is CameraBase cameraDevice)
                    {
                        this.LogVerbose(
                            "Adding CameraBaseMessenger for {deviceKey}",
                            device.Key
                        );

                        var cameraMessenger = new CameraBaseMessenger(
                            $"{device.Key}-cameraBase-{Key}",
                            cameraDevice,
                            $"/device/{device.Key}"
                        );

                        AddDefaultDeviceMessenger(cameraMessenger);

                        messengerAdded = true;
                    }

                    if (device is BlueJeansPc)
                    {
                        this.LogVerbose(
                            "Adding IRunRouteActionMessnger for {deviceKey}",
                            device.Key
                        );

                        var routeMessenger = new RunRouteActionMessenger(
                            $"{device.Key}-runRouteAction-{Key}",
                            device as BlueJeansPc,
                            $"/device/{device.Key}"
                        );

                        AddDefaultDeviceMessenger(routeMessenger);

                        messengerAdded = true;
                    }

                    if (device is ITvPresetsProvider)
                    {
                        this.LogVerbose(
                            "Trying to cast to ITvPresetsProvider for {deviceKey}",
                            device.Key
                        );

                        var presetsDevice = device as ITvPresetsProvider;


                        this.LogVerbose(
                            "Adding ITvPresetsProvider for {deviceKey}",
                            device.Key
                        );

                        var presetsMessenger = new DevicePresetsModelMessenger(
                            $"{device.Key}-presets-{Key}",
                            $"/device/{device.Key}",
                            presetsDevice
                        );

                        AddDefaultDeviceMessenger(presetsMessenger);

                        messengerAdded = true;
                    }


                    if (device is DisplayBase)
                    {
                        this.LogVerbose("Adding actions for device: {0}", device.Key);

                        var dbMessenger = new DisplayBaseMessenger(
                            $"{device.Key}-displayBase-{Key}",
                            $"/device/{device.Key}",
                            device as DisplayBase
                        );

                        AddDefaultDeviceMessenger(dbMessenger);

                        messengerAdded = true;
                    }

                    if (device is TwoWayDisplayBase twoWayDisplay)
                    {
                        this.LogVerbose(
                            "Adding TwoWayDisplayBase for {deviceKey}",
                            device.Key
                        );
                        var twoWayDisplayMessenger = new TwoWayDisplayBaseMessenger(
                            $"{device.Key}-twoWayDisplay-{Key}",
                            string.Format("/device/{0}", device.Key),
                            twoWayDisplay
                        );
                        AddDefaultDeviceMessenger(twoWayDisplayMessenger);

                        messengerAdded = true;
                    }

                    if (device is IBasicVolumeWithFeedback)
                    {
                        var deviceKey = device.Key;
                        this.LogVerbose(
                            "Adding IBasicVolumeControlWithFeedback for {deviceKey}",
                            deviceKey
                        );

                        var volControlDevice = device as IBasicVolumeWithFeedback;
                        var messenger = new DeviceVolumeMessenger(
                            $"{device.Key}-volume-{Key}",
                            string.Format("/device/{0}", deviceKey),
                            volControlDevice
                        );
                        AddDefaultDeviceMessenger(messenger);

                        messengerAdded = true;
                    }

                    if (device is ILightingScenes || device is LightingBase)
                    {
                        var deviceKey = device.Key;

                        this.LogVerbose(
                            "Adding LightingBaseMessenger for {deviceKey}",
                            deviceKey
                        );

                        var lightingDevice = device as ILightingScenes;
                        var messenger = new ILightingScenesMessenger(
                            $"{device.Key}-lighting-{Key}",
                            lightingDevice,
                            string.Format("/device/{0}", deviceKey)
                        );
                        AddDefaultDeviceMessenger(messenger);

                        messengerAdded = true;
                    }

                    if (device is IShadesOpenCloseStop)
                    {
                        var deviceKey = device.Key;
                        var shadeDevice = device as IShadesOpenCloseStop;

                        this.LogVerbose(
                            "Adding ShadeBaseMessenger for {deviceKey}",
                            deviceKey
                        );

                        var messenger = new IShadesOpenCloseStopMessenger(
                            $"{device.Key}-shades-{Key}",
                            shadeDevice,
                            string.Format("/device/{0}", deviceKey)
                        );
                        AddDefaultDeviceMessenger(messenger);

                        messengerAdded = true;
                    }

                    if (device is VideoCodecBase codec)
                    {
                        this.LogVerbose(
                            "Adding VideoCodecBaseMessenger for {deviceKey}", codec.Key);

                        var messenger = new VideoCodecBaseMessenger(
                            $"{codec.Key}-videoCodec-{Key}",
                            codec,
                            $"/device/{codec.Key}"
                        );

                        AddDefaultDeviceMessenger(messenger);

                        messengerAdded = true;
                    }

                    if (device is AudioCodecBase audioCodec)
                    {
                        this.LogVerbose(
                            "Adding AudioCodecBaseMessenger for {deviceKey}", audioCodec.Key
                        );

                        var messenger = new AudioCodecBaseMessenger(
                            $"{audioCodec.Key}-audioCodec-{Key}",
                            audioCodec,
                            $"/device/{audioCodec.Key}"
                        );

                        AddDefaultDeviceMessenger(messenger);

                        messengerAdded = true;
                    }

                    if (device is ISetTopBoxControls stbDevice)
                    {
                        this.LogVerbose(
                            "Adding ISetTopBoxControlMessenger for {deviceKey}"
                        );                        

                        var messenger = new ISetTopBoxControlsMessenger(
                            $"{device.Key}-stb-{Key}",
                            $"/device/{device.Key}",
                            stbDevice
                        );

                        AddDefaultDeviceMessenger(messenger);

                        messengerAdded = true;
                    }

                    if (device is IChannel channelDevice)
                    {
                        this.LogVerbose(
                            "Adding IChannelMessenger for {deviceKey}", device.Key
                        );                        

                        var messenger = new IChannelMessenger(
                            $"{device.Key}-channel-{Key}",
                            $"/device/{device.Key}",
                            channelDevice
                        );

                        AddDefaultDeviceMessenger(messenger);

                        messengerAdded = true;
                    }

                    if (device is IColor colorDevice)
                    {
                        this.LogVerbose("Adding IColorMessenger for {deviceKey}", device.Key);                        

                        var messenger = new IColorMessenger(
                            $"{device.Key}-color-{Key}",
                            $"/device/{device.Key}",
                            colorDevice
                        );

                        AddDefaultDeviceMessenger(messenger);

                        messengerAdded = true;
                    }

                    if (device is IDPad dPadDevice)
                    {
                        this.LogVerbose("Adding IDPadMessenger for {deviceKey}", device.Key);                        

                        var messenger = new IDPadMessenger(
                            $"{device.Key}-dPad-{Key}",
                            $"/device/{device.Key}",
                            dPadDevice
                        );

                        AddDefaultDeviceMessenger(messenger);

                        messengerAdded = true;
                    }

                    if (device is INumericKeypad nkDevice)
                    {
                        this.LogVerbose("Adding INumericKeyapdMessenger for {deviceKey}", device.Key);                        

                        var messenger = new INumericKeypadMessenger(
                            $"{device.Key}-numericKeypad-{Key}",
                            $"/device/{device.Key}",
                            nkDevice
                        );

                        AddDefaultDeviceMessenger(messenger);

                        messengerAdded = true;
                    }

                    if (device is IHasPowerControl pcDevice)
                    {
                        this.LogVerbose("Adding IHasPowerControlMessenger for {deviceKey}", device.Key);                        

                        var messenger = new IHasPowerMessenger(
                            $"{device.Key}-powerControl-{Key}",
                            $"/device/{device.Key}",
                            pcDevice
                        );

                        AddDefaultDeviceMessenger(messenger);

                        messengerAdded = true;
                    }

                    if (device is IHasPowerControlWithFeedback powerControl)
                    {
                        var deviceKey = device.Key;
                        this.LogVerbose(
                            "Adding IHasPowerControlWithFeedbackMessenger for {deviceKey}",
                            deviceKey
                        );

                        var messenger = new IHasPowerControlWithFeedbackMessenger(
                            $"{device.Key}-powerFeedback-{Key}",
                            string.Format("/device/{0}", deviceKey),
                            powerControl
                        );
                        AddDefaultDeviceMessenger(messenger);
                        messengerAdded = true;
                    }

                    if (device is ITransport transportDevice)
                    {
                        this.LogVerbose(
                            "Adding ITransportMessenger for {deviceKey}", device.Key
                        );                        

                        var messenger = new ITransportMessenger(
                            $"{device.Key}-transport-{Key}",
                            $"/device/{device.Key}",
                            transportDevice
                        );

                        AddDefaultDeviceMessenger(messenger);

                        messengerAdded = true;
                    }

                    if (device is IHasCurrentSourceInfoChange csiChange)
                    {
                        this.LogVerbose("Adding IHasCurrentSourceInfoMessenger for {deviceKey}", device.Key);

                        var messenger = new IHasCurrentSourceInfoMessenger(
                            $"{device.Key}-currentSource-{Key}",
                            $"/device/{device.Key}",
                            csiChange
                        );

                        AddDefaultDeviceMessenger(messenger);

                        messengerAdded = true;
                    }

                    if (device is ISwitchedOutput switchedDevice)
                    {
                        this.LogVerbose(
                            "Adding ISwitchedOutputMessenger for {deviceKey}", device.Key
                        );

                        var messenger = new ISwitchedOutputMessenger(
                            $"{device.Key}-switchedOutput-{Key}",
                            switchedDevice,
                            $"/device/{device.Key}"
                        );

                        AddDefaultDeviceMessenger(messenger);

                        messengerAdded = true;
                    }

                    if (device is IDeviceInfoProvider provider)
                    {
                        this.LogVerbose("Adding IHasDeviceInfoMessenger for {deviceKey}", device.Key
                        );

                        var messenger = new DeviceInfoMessenger(
                            $"{device.Key}-deviceInfo-{Key}",
                            $"/device/{device.Key}",
                            provider
                        );

                        AddDefaultDeviceMessenger(messenger);

                        messengerAdded = true;
                    }

                    if (device is ILevelControls levelControls)
                    {
                        this.LogVerbose(
                            "Adding LevelControlsMessenger for {deviceKey}", device.Key
                        );

                        var messenger = new ILevelControlsMessenger(
                            $"{device.Key}-levelControls-{Key}",
                            $"/device/{device.Key}",
                            levelControls
                        );

                        AddDefaultDeviceMessenger(messenger);

                        messengerAdded = true;
                    }

                    if (device is IHasInputs<string> stringInputs)
                    {
                        this.LogVerbose("Adding InputsMessenger<string> for {deviceKey}", device.Key);

                        var messenger = new IHasInputsMessenger<string>(
                            $"{device.Key}-inputs-{Key}",
                            $"/device/{device.Key}",
                            stringInputs
                        );

                        AddDefaultDeviceMessenger(messenger);

                        messengerAdded = true;
                    }

                    if (device is IHasInputs<byte> byteInputs)
                    {
                        this.LogVerbose("Adding InputsMessenger for {deviceKey}", device.Key);

                        var messenger = new IHasInputsMessenger<byte>(
                            $"{device.Key}-inputs-{Key}",
                            $"/device/{device.Key}",
                            byteInputs
                        );

                        AddDefaultDeviceMessenger(messenger);

                        messengerAdded = true;
                    }

                    if (device is IHasInputs<int> intInputs)
                    {
                        this.LogVerbose("Adding InputsMessenger for {deviceKey}", device.Key);

                        var messenger = new IHasInputsMessenger<int>(
                            $"{device.Key}-inputs-{Key}",
                            $"/device/{device.Key}",
                            intInputs
                        );

                        AddDefaultDeviceMessenger(messenger);

                        messengerAdded = true;
                    }

                    if (device is IMatrixRouting matrix)
                    {
                        this.LogVerbose(
                            "Adding IMatrixRoutingMessenger for {deviceKey}",
                            device.Key
                        );

                        var messenger = new IMatrixRoutingMessenger(
                            $"{device.Key}-matrixRouting",
                            $"/device/{device.Key}",
                            matrix
                        );

                        AddDefaultDeviceMessenger(messenger);

                        messengerAdded = true;
                    }

                    if (device is ITemperatureSensor tempSensor)
                    {
                        this.LogVerbose(
                            "Adding ITemperatureSensor for {deviceKey}",
                            device.Key
                        );

                        var messenger = new ITemperatureSensorMessenger(
                            $"{device.Key}-tempSensor",
                            tempSensor,
                            $"/device/{device.Key}"
                        );

                        AddDefaultDeviceMessenger(messenger);

                        messengerAdded = true;
                    }

                    if (device is IHumiditySensor humSensor)
                    {
                        this.LogVerbose(
                            "Adding IHumiditySensor for {deviceKey}",
                            device.Key
                        );

                        var messenger = new IHumiditySensorMessenger(
                            $"{device.Key}-humiditySensor",
                            humSensor,
                            $"/device/{device.Key}"
                        );

                        AddDefaultDeviceMessenger(messenger);

                        messengerAdded = true;
                    }

                    if (device is IEssentialsRoomCombiner roomCombiner)
                    {
                        this.LogVerbose(
                            "Adding IEssentialsRoomCombinerMessenger for {deviceKey}", device.Key
                        );

                        var messenger = new IEssentialsRoomCombinerMessenger(
                            $"{device.Key}-roomCombiner-{Key}",
                            $"/device/{device.Key}",
                            roomCombiner
                        );

                        AddDefaultDeviceMessenger(messenger);

                        messengerAdded = true;
                    }

                    if (device is IProjectorScreenLiftControl screenLiftControl)
                    {
                        this.LogVerbose("Adding IProjectorScreenLiftControlMessenger for {deviceKey}", device.Key
                        );

                        var messenger = new IProjectorScreenLiftControlMessenger(
                            $"{device.Key}-screenLiftControl-{Key}",
                            $"/device/{device.Key}",
                            screenLiftControl
                        );

                        AddDefaultDeviceMessenger(messenger);

                        messengerAdded = true;
                    }

                    if (device is IDspPresets dspPresets)
                    {
                        this.LogVerbose("Adding IDspPresetsMessenger for {deviceKey}", device.Key
                        );

                        var messenger = new IDspPresetsMessenger(
                            $"{device.Key}-dspPresets-{Key}",
                            $"/device/{device.Key}",
                            dspPresets
                        );

                        AddDefaultDeviceMessenger(messenger);

                        messengerAdded = true;
                    }

                    this.LogVerbose("Trying to cast to generic device for device: {key}", device.Key);

                    if (device is EssentialsDevice)
                    {
                        if (!(device is EssentialsDevice genericDevice) || messengerAdded)
                        {
                            this.LogVerbose(
                                "Skipping GenericMessenger for {deviceKey}. Messenger(s) Added: {messengersAdded}.",
                                device.Key,
                                messengerAdded
                            );
                            this.LogDebug(
                                "AllDevices Completed a device. Devices Left: {count}",
                                --count
                            );
                            continue;
                        }

                        this.LogDebug(
                            "Adding GenericMessenger for {deviceKey}",
                            this,
                            genericDevice?.Key
                        );

                        AddDefaultDeviceMessenger(
                            new GenericMessenger(
                                genericDevice.Key + "-" + Key + "-generic",
                                genericDevice,
                                string.Format("/device/{0}", genericDevice.Key)
                            )
                        );
                    }
                    else
                    {
                        this.LogVerbose(
                            "Not Essentials Device. Skipping GenericMessenger for {deviceKey}",
                            device.Key
                        );
                    }
                    this.LogDebug(
                        "AllDevices Completed a device. Devices Left: {count}",
                        --count
                    );
                }

                catch (Exception ex)
                {
                    this.LogException(ex, "Exception setting up default device messengers");
                }
            }
        }

        private void AddWebApiPaths()
        {
            var apiServer = DeviceManager
                .AllDevices.OfType<EssentialsWebApi>()
                .FirstOrDefault(d => d.Key == "essentialsWebApi");

            if (apiServer == null)
            {
                this.LogWarning("No API Server available");
                return;
            }

            // TODO: Add routes for the rest of the MC console commands
            var routes = new List<HttpCwsRoute>
            {
                new HttpCwsRoute($"device/{Key}/authorize")
                {
                    Name = "MobileControlAuthorize",
                    RouteHandler = new MobileAuthRequestHandler(this)
                },
                new HttpCwsRoute($"device/{Key}/info")
                {
                    Name = "MobileControlInformation",
                    RouteHandler = new MobileInfoHandler(this)
                },
                new HttpCwsRoute($"device/{Key}/actionPaths")
                {
                    Name = "MobileControlActionPaths",
                    RouteHandler = new ActionPathsHandler(this)
                }
            };

            apiServer.AddRoute(routes);
        }

        private void AddConsoleCommands()
        {
            CrestronConsole.AddNewConsoleCommand(
                AuthorizeSystem,
                "mobileauth",
                "Authorizes system to talk to Mobile Control server",
                ConsoleAccessLevelEnum.AccessOperator
            );
            CrestronConsole.AddNewConsoleCommand(
                s => ShowInfo(),
                "mobileinfo",
                "Shows information for current mobile control session",
                ConsoleAccessLevelEnum.AccessOperator
            );
            CrestronConsole.AddNewConsoleCommand(
                s =>
                {
                    s = s.Trim();
                    if (!string.IsNullOrEmpty(s))
                    {
                        _httpDebugEnabled = (s.Trim() != "0");
                    }
                    CrestronConsole.ConsoleCommandResponse(
                        "HTTP Debug {0}",
                        _httpDebugEnabled ? "Enabled" : "Disabled"
                    );
                },
                "mobilehttpdebug",
                "1 enables more verbose HTTP response debugging",
                ConsoleAccessLevelEnum.AccessOperator
            );
            CrestronConsole.AddNewConsoleCommand(
                TestHttpRequest,
                "mobilehttprequest",
                "Tests an HTTP get to URL given",
                ConsoleAccessLevelEnum.AccessOperator
            );

            CrestronConsole.AddNewConsoleCommand(
                PrintActionDictionaryPaths,
                "mobileshowactionpaths",
                "Prints the paths in the Action Dictionary",
                ConsoleAccessLevelEnum.AccessOperator
            );
            CrestronConsole.AddNewConsoleCommand(
                s =>
                {
                    _disableReconnect = false;

                    CrestronConsole.ConsoleCommandResponse(
                        $"Connecting to MC API server"
                    );

                    ConnectWebsocketClient();
                },
                "mobileconnect",
                "Forces connect of websocket",
                ConsoleAccessLevelEnum.AccessOperator
            );

            CrestronConsole.AddNewConsoleCommand(
                s =>
                {
                    _disableReconnect = true;

                    CleanUpWebsocketClient();

                    CrestronConsole.ConsoleCommandResponse(
                        $"Disonnected from MC API server"
                    );
                },
                "mobiledisco",
                "Disconnects websocket",
                ConsoleAccessLevelEnum.AccessOperator
            );

            CrestronConsole.AddNewConsoleCommand(
                ParseStreamRx,
                "mobilesimulateaction",
                "Simulates a message from the server",
                ConsoleAccessLevelEnum.AccessOperator
            );

            CrestronConsole.AddNewConsoleCommand(
                SetWebsocketDebugLevel,
                "mobilewsdebug",
                "Set Websocket debug level",
                ConsoleAccessLevelEnum.AccessProgrammer
            );
        }

        /// <summary>
        /// Gets or sets the Config
        /// </summary>
        public MobileControlConfig Config { get; private set; }

        /// <summary>
        /// Gets or sets the Host
        /// </summary>
        public string Host { get; private set; }

        public string ClientAppUrl => Config.ClientAppUrl;

        private void OnRoomCombinationScenarioChanged(
            object sender,
            EventArgs eventArgs
        )
        {
            SendMessageObject(new MobileControlMessage { Type = "/system/roomCombinationChanged" });
        }

        /// <summary>
        /// CheckForDeviceMessenger method
        /// </summary>
        public bool CheckForDeviceMessenger(string key)
        {
            return _messengers.ContainsKey(key);
        }

        /// <summary>
        /// AddDeviceMessenger method
        /// </summary>
        public void AddDeviceMessenger(IMobileControlMessenger messenger)
        {
            if (_messengers.ContainsKey(messenger.Key))
            {
                this.LogWarning("Messenger with key {messengerKey) already added", messenger.Key);
                return;
            }

            if (messenger is IDelayedConfiguration simplMessenger)
            {
                simplMessenger.ConfigurationIsReady += Bridge_ConfigurationIsReady;
            }

            if (messenger is MobileControlBridgeBase roomBridge)
            {
                _roomBridges.Add(roomBridge);
            }

            this.LogVerbose(
                "Adding messenger with key {messengerKey} for path {messengerPath}",
                messenger.Key,
                messenger.MessagePath
            );

            _messengers.Add(messenger.Key, messenger);

            if (_initialized)
            {
                this.LogDebug("Registering messenger {messengerKey} AFTER initialization", messenger.Key);
                messenger.RegisterWithAppServer(this);
            }
        }

        private void AddDefaultDeviceMessenger(IMobileControlMessenger messenger)
        {
            if (_defaultMessengers.ContainsKey(messenger.Key))
            {
                this.LogWarning(
                    "Default messenger with key {messengerKey} already added",
                    messenger.Key
                );
                return;
            }

            if (messenger is IDelayedConfiguration simplMessenger)
            {
                simplMessenger.ConfigurationIsReady += Bridge_ConfigurationIsReady;
            }
            this.LogVerbose(
                "Adding default messenger with key {messengerKey} for path {messengerPath}",
                messenger.Key,
                messenger.MessagePath
            );

            _defaultMessengers.Add(messenger.Key, messenger);

            if (_initialized)
            {
                RegisterMessengerWithServer(messenger);
            }
        }

        private void RegisterMessengerWithServer(IMobileControlMessenger messenger)
        {
            this.LogVerbose(
                "Registering messenger with key {messengerKey} for path {messengerPath}",
                messenger.Key,
                messenger.MessagePath
            );

            messenger.RegisterWithAppServer(this);
        }

        /// <summary>
        /// Initialize method
        /// </summary>
        /// <inheritdoc />
        public override void Initialize()
        {
            foreach (var messenger in _messengers)
            {
                try
                {
                    RegisterMessengerWithServer(messenger.Value);
                }
                catch (Exception ex)
                {
                    this.LogException(ex, "Exception registering custom messenger {messengerKey}", messenger.Key);
                    continue;
                }
            }

            foreach (var messenger in _defaultMessengers)
            {
                try
                {
                    RegisterMessengerWithServer(messenger.Value);
                }
                catch (Exception ex)
                {
                    this.LogException(ex, "Exception registering default messenger {messengerKey}", messenger.Key);
                    continue;
                }
            }

            var simplMessengers = _messengers.OfType<IDelayedConfiguration>().ToList();

            if (simplMessengers.Count > 0)
            {
                return;
            }

            _initialized = true;

            RegisterSystemToServer();
        }

        #region IMobileControl Members

        /// <summary>
        /// GetAppServer method
        /// </summary>
        public static IMobileControl GetAppServer()
        {
            try
            {
                var appServer =
                    DeviceManager.GetDevices().SingleOrDefault(s => s is IMobileControl)
                    as MobileControlSystemController;
                return appServer;
            }
            catch (Exception e)
            {
                Debug.LogMessage(e, "Unable to find MobileControlSystemController in Devices");
                return null;
            }
        }

        /// <summary>
        /// Generates the url and creates the websocket client
        /// </summary>
        private bool CreateWebsocket()
        {
            if (_wsClient2 != null)
            {
                _wsClient2.Close();
                _wsClient2 = null;
            }

            if (string.IsNullOrEmpty(SystemUuid))
            {
                this.LogError(
                    "System UUID not defined. Unable to connect to Mobile Control"
                );
                return false;
            }

            var wsHost = Host.Replace("http", "ws");
            var url = string.Format("{0}/system/join/{1}", wsHost, SystemUuid);

            _wsClient2 = new WebSocket(url)
            {
                Log =
                {
                    Output = (data, s) =>
                        this.LogDebug(
                            "Message from websocket: {message}",
                            data
                        )
                }
            };

            _wsClient2.SslConfiguration.EnabledSslProtocols =
                System.Security.Authentication.SslProtocols.Tls11
                | System.Security.Authentication.SslProtocols.Tls12;

            _wsClient2.OnMessage += HandleMessage;
            _wsClient2.OnOpen += HandleOpen;
            _wsClient2.OnError += HandleError;
            _wsClient2.OnClose += HandleClose;

            return true;
        }

        /// <summary>
        /// LinkSystemMonitorToAppServer method
        /// </summary>
        public void LinkSystemMonitorToAppServer()
        {
            if (CrestronEnvironment.DevicePlatform != eDevicePlatform.Appliance)
            {
                this.LogWarning(
                    "System Monitor does not exist for this platform. Skipping..."
                );
                return;
            }

            if (!(DeviceManager.GetDeviceForKey("systemMonitor") is SystemMonitorController sysMon))
            {
                return;
            }

            var key = sysMon.Key + "-" + Key;
            var messenger = new SystemMonitorMessenger(key, sysMon, "/device/systemMonitor");

            AddDeviceMessenger(messenger);
        }

        #endregion

        private void SetWebsocketDebugLevel(string cmdparameters)
        {
            if (CrestronEnvironment.ProgramCompatibility == eCrestronSeries.Series4)
            {
                this.LogInformation(
                    "Setting websocket log level not currently allowed on 4 series."
                );
                return; // Web socket log level not currently allowed in series4
            }

            if (string.IsNullOrEmpty(cmdparameters))
            {
                this.LogInformation("Current Websocket debug level: {webSocketDebugLevel}", _wsLogLevel);
                return;
            }

            if (cmdparameters.ToLower().Contains("help") || cmdparameters.ToLower().Contains("?"))
            {
                CrestronConsole.ConsoleCommandResponse(
                    $"valid options are:\r\n{LogLevel.Trace}\r\n{LogLevel.Debug}\r\n{LogLevel.Info}\r\n{LogLevel.Warn}\r\n{LogLevel.Error}\r\n{LogLevel.Fatal}\r\n"
                );
            }

            try
            {
                var debugLevel = (LogLevel)Enum.Parse(typeof(LogLevel), cmdparameters, true);

                _wsLogLevel = debugLevel;

                if (_wsClient2 != null)
                {
                    _wsClient2.Log.Level = _wsLogLevel;
                }

                CrestronConsole.ConsoleCommandResponse($"Websocket log level set to {debugLevel}");
            }
            catch
            {
                CrestronConsole.ConsoleCommandResponse(
                    $"{cmdparameters} is not a valid debug level. Valid options are:\r\n{LogLevel.Trace}\r\n{LogLevel.Debug}\r\n{LogLevel.Info}\r\n{LogLevel.Warn}\r\n{LogLevel.Error}\r\n{LogLevel.Fatal}\r\n"
                );

            }
        }

        /// <summary>
        /// Sends message to server to indicate the system is shutting down
        /// </summary>
        /// <param name="programEventType"></param>
        private void CrestronEnvironment_ProgramStatusEventHandler(
            eProgramStatusEventType programEventType
        )
        {
            if (
                programEventType != eProgramStatusEventType.Stopping
                || _wsClient2 == null
                || !_wsClient2.IsAlive
            )
            {
                return;
            }

            _disableReconnect = true;

            StopServerReconnectTimer();
            CleanUpWebsocketClient();
        }

        /// <summary>
        /// PrintActionDictionaryPaths method
        /// </summary>
        public void PrintActionDictionaryPaths(object o)
        {
            CrestronConsole.ConsoleCommandResponse("ActionDictionary Contents:\r\n");

            foreach (var (messengerKey, actionPath) in GetActionDictionaryPaths())
            {
                CrestronConsole.ConsoleCommandResponse($"<{messengerKey}> {actionPath}\r\n");
            }
        }

        public List<(string, string)> GetActionDictionaryPaths()
        {
            var paths = new List<(string, string)>();

            foreach (var item in _actionDictionary)
            {
                var messengers = item.Value.Select(a => a.Messenger).Cast<MessengerBase>();
                foreach (var messenger in messengers)
                {
                    foreach (var actionPath in messenger.GetActionPaths())
                    {
                        paths.Add((messenger.Key, $"{item.Key}{actionPath}"));
                    }
                }
            }

            return paths;
        }

        /// <summary>
        /// Adds an action to the dictionary
        /// </summary>
        /// <param name="key">The path of the API command</param>
        /// <param name="action">The action to be triggered by the commmand</param>
        public void AddAction<T>(T messenger, Action<string, string, JToken> action)
            where T : IMobileControlMessenger
        {
            if (
                _actionDictionary.TryGetValue(
                    messenger.MessagePath,
                    out List<IMobileControlAction> actionList
                )
            )
            {
                if (
                    actionList.Any(a =>
                        a.Messenger.GetType() == messenger.GetType()
                        && a.Messenger.DeviceKey == messenger.DeviceKey
                    )
                )
                {
                    this.LogWarning("Messenger of type {messengerType} already exists. Skipping actions for {messengerKey}", messenger.GetType().Name, messenger.Key);
                    return;
                }

                actionList.Add(new MobileControlAction(messenger, action));
                return;
            }

            actionList = new List<IMobileControlAction>
            {
                new MobileControlAction(messenger, action)
            };

            _actionDictionary.Add(messenger.MessagePath, actionList);
        }

        /// <summary>
        /// Removes an action from the dictionary
        /// </summary>
        /// <param name="key"></param>
        /// <summary>
        /// RemoveAction method
        /// </summary>
        public void RemoveAction(string key)
        {
            if (_actionDictionary.ContainsKey(key))
            {
                _actionDictionary.Remove(key);
            }
        }

        public MobileControlBridgeBase GetRoomBridge(string key)
        {
            return _roomBridges.FirstOrDefault((r) => r.RoomKey.Equals(key));
        }

        /// <summary>
        /// GetRoomMessenger method
        /// </summary>
        public IMobileControlRoomMessenger GetRoomMessenger(string key)
        {
            return _roomBridges.FirstOrDefault((r) => r.RoomKey.Equals(key));
        }

        /// <summary>
        ///
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Bridge_ConfigurationIsReady(object sender, EventArgs e)
        {
            this.LogDebug("Bridge ready.  Registering");

            // send the configuration object to the server

            if (_wsClient2 == null)
            {
                RegisterSystemToServer();
            }
            else if (!_wsClient2.IsAlive)
            {
                ConnectWebsocketClient();
            }
            else
            {
                SendInitialMessage();
            }
        }

        /// <summary>
        ///
        /// </summary>
        /// <param name="o"></param>
        private void ReconnectToServerTimerCallback(object o)
        {
            this.LogDebug("Attempting to reconnect to server...");

            ConnectWebsocketClient();
        }

        /// <summary>
        /// Verifies system connection with servers
        /// </summary>
        private void AuthorizeSystem(string code)
        {
            if (
                string.IsNullOrEmpty(SystemUuid)
                || SystemUuid.Equals("missing url", StringComparison.OrdinalIgnoreCase)
            )
            {
                CrestronConsole.ConsoleCommandResponse(
                    "System does not have a UUID. Please ensure proper configuration is loaded and restart."
                );
                return;
            }
            if (string.IsNullOrEmpty(code))
            {
                CrestronConsole.ConsoleCommandResponse(
                    "Please enter a grant code to authorize a system"
                );
                return;
            }
            if (string.IsNullOrEmpty(Config.ServerUrl))
            {
                CrestronConsole.ConsoleCommandResponse(
                    "Mobile control API address is not set. Check portal configuration"
                );
                return;
            }

            var authTask = ApiService.SendAuthorizationRequest(Host, code, SystemUuid);

            authTask.ContinueWith(t =>
            {
                var response = t.Result;

                if (response.Authorized)
                {
                    this.LogDebug("System authorized, sending config.");
                    RegisterSystemToServer();
                    return;
                }

                this.LogInformation(response.Reason);
            });
        }

        /// <summary>
        /// Dumps info in response to console command.
        /// </summary>
        private void ShowInfo()
        {
            var url = Config != null ? Host : "No config";
            string name;
            string code;
            if (_roomBridges != null && _roomBridges.Count > 0)
            {
                name = _roomBridges[0].RoomName;
                code = _roomBridges[0].UserCode;
            }
            else
            {
                name = "No config";
                code = "Not available";
            }
            var conn = _wsClient2 == null ? "No client" : (_wsClient2.IsAlive ? "Yes" : "No");

            var secSinceLastAck = DateTime.Now - _lastAckMessage;

            if (Config.EnableApiServer)
            {
                CrestronConsole.ConsoleCommandResponse(
                    @"Mobile Control Edge Server API Information:

	Server address: {0}
	System Name: {1}
    System URL: {2}
	System UUID: {3}
	System User code: {4}
	Connected?: {5}
    Seconds Since Last Ack: {6}",
                    url,
                    name,
                    ConfigReader.ConfigObject.SystemUrl,
                    SystemUuid,
                    code,
                    conn,
                    secSinceLastAck.Seconds
                );
            }
            else
            {
                CrestronConsole.ConsoleCommandResponse(
                    @"
Mobile Control Edge Server API Information:
    Not Enabled in Config.
"
                );
            }

            if (
                Config.DirectServer != null
                && Config.DirectServer.EnableDirectServer
                && _directServer != null
            )
            {
                CrestronConsole.ConsoleCommandResponse(
                    @"
Mobile Control Direct Server Information:
    User App URL: {0}
    Server port: {1}
",
                    string.Format("{0}[insert_client_token]", _directServer.UserAppUrlPrefix),
                    _directServer.Port
                );

                CrestronConsole.ConsoleCommandResponse(
                    @"
    UI Client Info:
    Tokens Defined: {0}
    Clients Connected: {1}
",
                    _directServer.UiClients.Count,
                    _directServer.ConnectedUiClientsCount
                );

                var clientNo = 1;
                foreach (var clientContext in _directServer.UiClients)
                {
                    var isAlive = false;
                    var duration = "Not Connected";

                    if (clientContext.Value.Client != null)
                    {
                        isAlive = clientContext.Value.Client.Context.WebSocket.IsAlive;
                        duration = clientContext.Value.Client.ConnectedDuration.ToString();
                    }

                    CrestronConsole.ConsoleCommandResponse(
                        @"
Client {0}:
Room Key: {1}
Touchpanel Key: {6}
Token: {2}
Client URL: {3}
Connected: {4}
Duration: {5}
",
                        clientNo,
                        clientContext.Value.Token.RoomKey,
                        clientContext.Key,
                        string.Format("{0}{1}", _directServer.UserAppUrlPrefix, clientContext.Key),
                        isAlive,
                        duration,
                        clientContext.Value.Token.TouchpanelKey
                    );
                    clientNo++;
                }
            }
            else
            {
                CrestronConsole.ConsoleCommandResponse(
                    @"
Mobile Control Direct Server Infromation:
    Not Enabled in Config."
                );
            }
        }

        /// <summary>
        /// RegisterSystemToServer method
        /// </summary>
        public void RegisterSystemToServer()
        {

            if (!Config.EnableApiServer)
            {
                this.LogInformation(
                    "ApiServer disabled via config.  Cancelling attempt to register to server."
                );
                return;
            }

            var result = CreateWebsocket();

            if (!result)
            {
                this.LogFatal("Unable to create websocket.");
                return;
            }

            ConnectWebsocketClient();
        }

        /// <summary>
        /// Connects the Websocket Client
        /// </summary>
        private void ConnectWebsocketClient()
        {
            try
            {
                _wsCriticalSection.Enter();

                // set to 99999 to let things work on 4-Series
                if (
                    (CrestronEnvironment.ProgramCompatibility & eCrestronSeries.Series4)
                    == eCrestronSeries.Series4
                )
                {
                    _wsClient2.Log.Level = (LogLevel)99999;
                }
                else if (
                    (CrestronEnvironment.ProgramCompatibility & eCrestronSeries.Series3)
                    == eCrestronSeries.Series3
                )
                {
                    _wsClient2.Log.Level = _wsLogLevel;
                }

                //This version of the websocket client is TLS1.2 ONLY

                //Fires OnMessage event when PING is received.
                _wsClient2.EmitOnPing = true;

                this.LogDebug(
                    "Connecting mobile control client to {mobileControlUrl}",
                    _wsClient2.Url
                );

                TryConnect();
            }
            finally
            {
                _wsCriticalSection.Leave();
            }
        }

        /// <summary>
        /// Attempts to connect the websocket
        /// </summary>
        private void TryConnect()
        {
            try
            {
                IsAuthorized = false;
                _wsClient2.Connect();
            }
            catch (InvalidOperationException)
            {
                this.LogError(
                    "Maximum retries exceeded. Restarting websocket"
                );
                HandleConnectFailure();
            }
            catch (IOException ex)
            {
                this.LogException(ex, "IO Exception on connect");
                HandleConnectFailure();
            }
            catch (Exception ex)
            {
                this.LogException(
                    ex,
                    "Error on Websocket Connect"
                );
                HandleConnectFailure();
            }
        }

        /// <summary>
        /// Gracefully handles conect failures by reconstructing the ws client and starting the reconnect timer
        /// </summary>
        private void HandleConnectFailure()
        {
            _wsClient2 = null;

            var wsHost = Host.Replace("http", "ws");
            var url = string.Format("{0}/system/join/{1}", wsHost, SystemUuid);
            _wsClient2 = new WebSocket(url)
            {
                Log =
                {
                    Output = (data, s) =>
                        this.LogDebug(
                            "Message from websocket: {message}",
                            data
                        )
                }
            };

            _wsClient2.OnMessage -= HandleMessage;
            _wsClient2.OnOpen -= HandleOpen;
            _wsClient2.OnError -= HandleError;
            _wsClient2.OnClose -= HandleClose;

            _wsClient2.OnMessage += HandleMessage;
            _wsClient2.OnOpen += HandleOpen;
            _wsClient2.OnError += HandleError;
            _wsClient2.OnClose += HandleClose;

            StartServerReconnectTimer();
        }

        /// <summary>
        ///
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void HandleOpen(object sender, EventArgs e)
        {
            StopServerReconnectTimer();
            StartPingTimer();
            this.LogInformation("Mobile Control API connected");
            SendMessageObject(new MobileControlMessage { Type = "hello" });
        }

        /// <summary>
        ///
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void HandleMessage(object sender, MessageEventArgs e)
        {
            if (e.IsPing)
            {
                _lastAckMessage = DateTime.Now;
                IsAuthorized = true;
                ResetPingTimer();
                return;
            }

            if (e.IsText && e.Data.Length > 0)
            {
                _receiveQueue.Enqueue(new ProcessStringMessage(e.Data, ParseStreamRx));
            }
        }

        /// <summary>
        ///
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void HandleError(object sender, ErrorEventArgs e)
        {
            this.LogError("Websocket error {0}", e.Message);

            IsAuthorized = false;
            StartServerReconnectTimer();
        }

        /// <summary>
        ///
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void HandleClose(object sender, CloseEventArgs e)
        {
            this.LogDebug(
                "Websocket close {code} {reason}, clean={wasClean}",
                e.Code,
                e.Reason,
                e.WasClean
            );
            IsAuthorized = false;
            StopPingTimer();

            // Start the reconnect timer only if disableReconnect is false and the code isn't 4200. 4200 indicates system is not authorized;
            if (_disableReconnect || e.Code == 4200)
            {
                return;
            }

            StartServerReconnectTimer();
        }

        /// <summary>
        /// After a "hello" from the server, sends config and stuff
        /// </summary>
        private void SendInitialMessage()
        {
            this.LogInformation("Sending initial join message");

            var touchPanels = DeviceManager
                .AllDevices.OfType<IMobileControlTouchpanelController>()
                .Where(tp => !tp.UseDirectServer)
                .Select(
                    (tp) =>
                    {
                        return new { touchPanelKey = tp.Key, roomKey = tp.DefaultRoomKey };
                    }
                );

            var msg = new MobileControlMessage
            {
                Type = "join",
                Content = JToken.FromObject(
                    new { config = GetConfigWithPluginVersion(), touchPanels }
                )
            };

            SendMessageObject(msg);
        }

        /// <summary>
        /// GetConfigWithPluginVersion method
        /// </summary>
        public MobileControlEssentialsConfig GetConfigWithPluginVersion()
        {
            // Populate the application name and version number
            var confObject = new MobileControlEssentialsConfig(ConfigReader.ConfigObject);

            confObject.Info.RuntimeInfo.AppName = Assembly.GetExecutingAssembly().GetName().Name;

            var essentialsVersion = Global.AssemblyVersion;
            confObject.Info.RuntimeInfo.AssemblyVersion = essentialsVersion;


            //            // Set for local testing
            //            confObject.RuntimeInfo.PluginVersion = "4.0.0-localBuild";

            // Populate the plugin version
            var pluginVersion = Assembly
                .GetExecutingAssembly()
                .GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false);


            if (pluginVersion[0] is AssemblyInformationalVersionAttribute fullVersionAtt)
            {
                var pluginInformationalVersion = fullVersionAtt.InformationalVersion;

                confObject.RuntimeInfo.PluginVersion = pluginInformationalVersion;
                confObject.RuntimeInfo.EssentialsVersion = Global.AssemblyVersion;
                confObject.RuntimeInfo.PepperDashCoreVersion = PluginLoader.PepperDashCoreAssembly.Version;
                confObject.RuntimeInfo.EssentialsPlugins = PluginLoader.EssentialsPluginAssemblies;
            }
            return confObject;
        }

        /// <summary>
        /// SetClientUrl method
        /// </summary>
        public void SetClientUrl(string path, string roomKey = null)
        {
            var message = new MobileControlMessage
            {
                Type = string.IsNullOrEmpty(roomKey) ? $"/event/system/setUrl" : $"/event/room/{roomKey}/setUrl",
                Content = JToken.FromObject(new MobileControlSimpleContent<string> { Value = path })
            };

            SendMessageObject(message);
        }

        /// <summary>
        /// Sends any object type to server
        /// </summary>
        /// <param name="o"></param>
        /// <summary>
        /// SendMessageObject method
        /// </summary>
        public void SendMessageObject(IMobileControlMessage o)
        {

            if (Config.EnableApiServer)
            {

                _transmitToServerQueue.Enqueue(new TransmitMessage(o, _wsClient2));

            }

            if (
                Config.DirectServer != null
                && Config.DirectServer.EnableDirectServer
                && _directServer != null
            )
            {
                _transmitToClientsQueue.Enqueue(new MessageToClients(o, _directServer));
            }

        }


        /// <summary>
        /// SendMessageObjectToDirectClient method
        /// </summary>
        public void SendMessageObjectToDirectClient(object o)
        {
            if (
                Config.DirectServer != null
                && Config.DirectServer.EnableDirectServer
                && _directServer != null
            )
            {
                _transmitToClientsQueue.Enqueue(new MessageToClients(o, _directServer));
            }
        }


        /// <summary>
        /// Disconnects the Websocket Client and stops the heartbeat timer
        /// </summary>
        private void CleanUpWebsocketClient()
        {
            if (_wsClient2 == null)
            {
                return;
            }

            this.LogDebug("Disconnecting websocket");

            _wsClient2.Close();
        }

        private void ResetPingTimer()
        {
            // This tells us we're online with the API and getting pings
            _pingTimer.Reset(PingInterval);
        }

        private void StartPingTimer()
        {
            StopPingTimer();
            _pingTimer = new CTimer(PingTimerCallback, null, PingInterval);
        }

        private void StopPingTimer()
        {
            if (_pingTimer == null)
            {
                return;
            }

            _pingTimer.Stop();
            _pingTimer.Dispose();
            _pingTimer = null;
        }

        private void PingTimerCallback(object o)
        {
            this.LogDebug(

                "Ping timer expired. Closing websocket"
            );

            try
            {
                _wsClient2.Close();
            }
            catch (Exception ex)
            {
                this.LogException(ex,
                    "Exception closing websocket"
                );

                HandleConnectFailure();
            }
        }

        /// <summary>
        ///
        /// </summary>
        private void StartServerReconnectTimer()
        {
            StopServerReconnectTimer();
            _serverReconnectTimer = new CTimer(
                ReconnectToServerTimerCallback,
                ServerReconnectInterval
            );
            this.LogDebug("Reconnect Timer Started.");
        }

        /// <summary>
        /// Does what it says
        /// </summary>
        private void StopServerReconnectTimer()
        {
            if (_serverReconnectTimer == null)
            {
                return;
            }
            _serverReconnectTimer.Stop();
            _serverReconnectTimer = null;
        }

        /// <summary>
        /// Resets reconnect timer and updates usercode
        /// </summary>
        /// <param name="content"></param>
        private void HandleHeartBeat(JToken content)
        {
            SendMessageObject(new MobileControlMessage { Type = "/system/heartbeatAck" });

            var code = content["userCode"];
            if (code == null)
            {
                return;
            }

            foreach (var b in _roomBridges)
            {
                b.SetUserCode(code.Value<string>());
            }
        }

        private void HandleClientJoined(JToken content)
        {
            var clientId = content["clientId"].Value<string>();
            var roomKey = content["roomKey"].Value<string>();

            if (_roomCombiner == null)
            {
                var message = new MobileControlMessage
                {
                    Type = "/system/roomKey",
                    ClientId = clientId,
                    Content = roomKey
                };

                SendMessageObject(message);
                return;
            }

            if (!_roomCombiner.CurrentScenario.UiMap.ContainsKey(roomKey))
            {
                this.LogWarning(
                    "Unable to find correct roomKey for {roomKey} in current scenario. Returning {roomKey} as roomKey", roomKey);

                var message = new MobileControlMessage
                {
                    Type = "/system/roomKey",
                    ClientId = clientId,
                    Content = roomKey
                };

                SendMessageObject(message);
                return;
            }

            var newRoomKey = _roomCombiner.CurrentScenario.UiMap[roomKey];

            var newMessage = new MobileControlMessage
            {
                Type = "/system/roomKey",
                ClientId = clientId,
                Content = newRoomKey
            };

            SendMessageObject(newMessage);
        }

        private void HandleUserCode(JToken content, Action<string, string> action = null)
        {
            var code = content["userCode"];

            JToken qrChecksum;

            try
            {
                qrChecksum = content.SelectToken("qrChecksum", false);
            }
            catch
            {
                qrChecksum = new JValue(string.Empty);
            }

            if (code == null)
            {
                return;
            }

            if (action == null)
            {
                foreach (var bridge in _roomBridges)
                {
                    bridge.SetUserCode(code.Value<string>(), qrChecksum.Value<string>());
                }

                return;
            }

            action(code.Value<string>(), qrChecksum.Value<string>());
        }

        /// <summary>
        /// HandleClientMessage method
        /// </summary>
        public void HandleClientMessage(string message)
        {
            _receiveQueue.Enqueue(new ProcessStringMessage(message, ParseStreamRx));
        }

        /// <summary>
        ///
        /// </summary>
        private void ParseStreamRx(string messageText)
        {
            if (string.IsNullOrEmpty(messageText))
            {
                return;
            }

            if (!messageText.Contains("/system/heartbeat"))
            {
                this.LogDebug(
                    "Message RX: {messageText}",
                    messageText
                );
            }

            try
            {
                var message = JsonConvert.DeserializeObject<MobileControlMessage>(messageText);

                switch (message.Type)
                {
                    case "hello":
                        SendInitialMessage();
                        break;
                    case "/system/heartbeat":
                        HandleHeartBeat(message.Content);
                        break;
                    case "/system/userCode":
                        HandleUserCode(message.Content);
                        break;
                    case "/system/clientJoined":
                        HandleClientJoined(message.Content);
                        break;
                    case "/system/reboot":
                        SystemMonitorController.ProcessorReboot();
                        break;
                    case "/system/programReset":
                        SystemMonitorController.ProgramReset(InitialParametersClass.ApplicationNumber);
                        break;
                    case "raw":
                        var wrapper = message.Content.ToObject<DeviceActionWrapper>();
                        DeviceJsonApi.DoDeviceAction(wrapper);
                        break;
                    case "close":
                        this.LogDebug("Received close message from server");
                        break;
                    default:
                        // Incoming message example
                        // /room/roomA/status
                        // /room/roomAB/status

                        // ActionDictionary Keys example
                        // /room/roomA
                        // /room/roomAB

                        // Can't do direct comparison because it will match /room/roomA with /room/roomA/xxx instead of /room/roomAB/xxx
                        var handlers = _actionDictionary.Where(kv => message.Type.StartsWith(kv.Key + "/")).SelectMany(kv => kv.Value).ToList(); // adds trailing slash to ensure above case is handled


                        if (handlers.Count == 0)
                        {
                            this.LogInformation("-- Warning: Incoming message has no registered handler {type}", message.Type);
                            break;
                        }                        

                        foreach (var handler in handlers)
                        {
                            Task.Run(
                                () =>
                                    handler.Action(message.Type, message.ClientId, message.Content)
                            );
                        }

                        break;
                }
            }
            catch (Exception err)
            {
                this.LogException(
                    err,
                    "Unable to parse {message}",
                    messageText
                );
            }
        }

        /// <summary>
        ///
        /// </summary>
        /// <param name="s"></param>
        private void TestHttpRequest(string s)
        {
            {
                s = s.Trim();
                if (string.IsNullOrEmpty(s))
                {
                    PrintTestHttpRequestUsage();
                    return;
                }
                var tokens = s.Split(' ');
                if (tokens.Length < 2)
                {
                    CrestronConsole.ConsoleCommandResponse("Too few paramaters\r");
                    PrintTestHttpRequestUsage();
                    return;
                }

                try
                {
                    var url = tokens[1];
                    switch (tokens[0].ToLower())
                    {
                        case "get":
                            {
                                var resp = new HttpClient().Get(url);
                                CrestronConsole.ConsoleCommandResponse("RESPONSE:\r{0}\r\r", resp);
                            }
                            break;
                        case "post":
                            {
                                var resp = new HttpClient().Post(url, new byte[] { });
                                CrestronConsole.ConsoleCommandResponse("RESPONSE:\r{0}\r\r", resp);
                            }
                            break;
                        default:
                            CrestronConsole.ConsoleCommandResponse("Only get or post supported\r");
                            PrintTestHttpRequestUsage();
                            break;
                    }
                }
                catch (HttpException e)
                {
                    CrestronConsole.ConsoleCommandResponse("Exception in request:\r");
                    CrestronConsole.ConsoleCommandResponse(
                        "Response URL: {0}\r",
                        e.Response.ResponseUrl
                    );
                    CrestronConsole.ConsoleCommandResponse(
                        "Response Error Code: {0}\r",
                        e.Response.Code
                    );
                    CrestronConsole.ConsoleCommandResponse(
                        "Response body: {0}\r",
                        e.Response.ContentString
                    );
                }
            }
        }

        private void PrintTestHttpRequestUsage()
        {
            CrestronConsole.ConsoleCommandResponse("Usage: mobilehttprequest:N get/post url\r");
        }
    }

    /// <summary>
    /// Represents a ClientSpecificUpdateRequest
    /// </summary>
    public class ClientSpecificUpdateRequest
    {
        public ClientSpecificUpdateRequest(Action<string> action)
        {
            ResponseMethod = action;
        }

        /// <summary>
        /// Gets or sets the ResponseMethod
        /// </summary>
        public Action<string> ResponseMethod { get; private set; }
    }

    /// <summary>
    /// Represents a UserCodeChanged
    /// </summary>
    public class UserCodeChanged
    {
        public Action<string, string> UpdateUserCode { get; private set; }

        public UserCodeChanged(Action<string, string> updateMethod)
        {
            UpdateUserCode = updateMethod;
        }
    }
}
