using System;
using System.Collections.Generic;
using System.Linq;
using RGB.NET.Core;
using Serilog;

namespace Artemis.Core.Services;

internal class InputService : IInputService
{
    private readonly ILogger _logger;
    private readonly IDeviceService _deviceService;
    private ArtemisDevice? _firstKeyboard;
    private ArtemisDevice? _firstMouse;
    private int _keyboardCount;
    private int _mouseCount;

    public InputService(ILogger logger, IDeviceService deviceService)
    {
        _logger = logger;
        _deviceService = deviceService;

        _deviceService.DeviceAdded += DeviceServiceOnDevicesModified;
        _deviceService.DeviceRemoved += DeviceServiceOnDevicesModified;
        BustIdentifierCache();
    }

    protected virtual void OnKeyboardKeyUpDown(ArtemisKeyboardKeyUpDownEventArgs e)
    {
        KeyboardKeyUpDown?.Invoke(this, e);
    }

    protected virtual void OnKeyboardKeyDown(ArtemisKeyboardKeyEventArgs e)
    {
        KeyboardKeyDown?.Invoke(this, e);
    }

    protected virtual void OnKeyboardKeyUp(ArtemisKeyboardKeyEventArgs e)
    {
        KeyboardKeyUp?.Invoke(this, e);
    }

    protected virtual void OnKeyboardToggleStatusChanged(ArtemisKeyboardToggleStatusArgs e)
    {
        KeyboardToggleStatusChanged?.Invoke(this, e);
    }

    protected virtual void OnMouseButtonUpDown(ArtemisMouseButtonUpDownEventArgs e)
    {
        MouseButtonUpDown?.Invoke(this, e);
    }

    protected virtual void OnMouseButtonDown(ArtemisMouseButtonEventArgs e)
    {
        MouseButtonDown?.Invoke(this, e);
    }

    protected virtual void OnMouseButtonUp(ArtemisMouseButtonEventArgs e)
    {
        MouseButtonUp?.Invoke(this, e);
    }

    protected virtual void OnMouseScroll(ArtemisMouseScrollEventArgs e)
    {
        MouseScroll?.Invoke(this, e);
    }

    protected virtual void OnMouseMove(ArtemisMouseMoveEventArgs e)
    {
        MouseMove?.Invoke(this, e);
    }

    protected virtual void OnDeviceIdentified()
    {
        DeviceIdentified?.Invoke(this, EventArgs.Empty);
    }

    /// <inheritdoc />
    public void Dispose()
    {
        while (_inputProviders.Any())
        {
            InputProvider provider = _inputProviders.First();
            RemoveInputProvider(provider);
            provider.Dispose();
        }
    }

    public event EventHandler<ArtemisKeyboardKeyUpDownEventArgs>? KeyboardKeyUpDown;
    public event EventHandler<ArtemisKeyboardKeyEventArgs>? KeyboardKeyDown;
    public event EventHandler<ArtemisKeyboardKeyEventArgs>? KeyboardKeyUp;
    public event EventHandler<ArtemisKeyboardToggleStatusArgs>? KeyboardToggleStatusChanged;

    public event EventHandler<ArtemisMouseButtonUpDownEventArgs>? MouseButtonUpDown;
    public event EventHandler<ArtemisMouseButtonEventArgs>? MouseButtonDown;
    public event EventHandler<ArtemisMouseButtonEventArgs>? MouseButtonUp;
    public event EventHandler<ArtemisMouseScrollEventArgs>? MouseScroll;
    public event EventHandler<ArtemisMouseMoveEventArgs>? MouseMove;
    public event EventHandler? DeviceIdentified;

    #region Providers

    private readonly List<InputProvider> _inputProviders = new();

    public KeyboardToggleStatus KeyboardToggleStatus { get; private set; } = new(false, false, false);

    public void AddInputProvider(InputProvider inputProvider)
    {
        inputProvider.IdentifierReceived += InputProviderOnIdentifierReceived;
        inputProvider.KeyboardDataReceived += InputProviderOnKeyboardDataReceived;
        inputProvider.KeyboardToggleStatusReceived += InputProviderOnKeyboardToggleStatusReceived;
        inputProvider.MouseButtonDataReceived += InputProviderOnMouseButtonDataReceived;
        inputProvider.MouseScrollDataReceived += InputProviderOnMouseScrollDataReceived;
        inputProvider.MouseMoveDataReceived += InputProviderOnMouseMoveDataReceived;
        _inputProviders.Add(inputProvider);

        inputProvider.OnKeyboardToggleStatusRequested();
    }


    public void RemoveInputProvider(InputProvider inputProvider)
    {
        if (!_inputProviders.Contains(inputProvider))
            return;

        _inputProviders.Remove(inputProvider);
        inputProvider.IdentifierReceived -= InputProviderOnIdentifierReceived;
        inputProvider.KeyboardDataReceived -= InputProviderOnKeyboardDataReceived;
        inputProvider.KeyboardToggleStatusReceived -= InputProviderOnKeyboardToggleStatusReceived;
        inputProvider.MouseButtonDataReceived -= InputProviderOnMouseButtonDataReceived;
        inputProvider.MouseScrollDataReceived -= InputProviderOnMouseScrollDataReceived;
        inputProvider.MouseMoveDataReceived -= InputProviderOnMouseMoveDataReceived;
    }

    #endregion

    #region Identification

    private readonly Dictionary<Tuple<InputProvider, object>, ArtemisDevice> _deviceCache = new();
    private List<ArtemisDevice> _devices = new();
    private ArtemisDevice? _identifyingDevice;

    public void IdentifyDevice(ArtemisDevice device)
    {
        if (device.DeviceType != RGBDeviceType.Keyboard && device.DeviceType != RGBDeviceType.Mouse)
            throw new ArtemisCoreException($"Cannot initialize input-identification for a device of type {device.DeviceType}. \r\n" +
                                           "Only keyboard and mouse is supported.");

        _identifyingDevice = device;
        _logger.Debug("Start identifying device {device}", device);
    }

    public void StopIdentify()
    {
        if (_identifyingDevice == null)
            return;

        _logger.Debug("Stop identifying device {device}", _identifyingDevice);
        _identifyingDevice = null;
        _deviceService.SaveDevices();

        BustIdentifierCache();
    }

    // TODO: Move the OnIdentifierReceived logic into here and get rid of OnIdentifierReceived, this and OnIdentifierReceived are always called in combination with each other
    public ArtemisDevice? GetDeviceByIdentifier(InputProvider provider, object identifier, InputDeviceType type)
    {
        if (provider == null) throw new ArgumentNullException(nameof(provider));
        if (identifier == null) throw new ArgumentNullException(nameof(identifier));

        // We will almost always only have zero or one of each
        if (type == InputDeviceType.Keyboard)
        {
            if (_keyboardCount == 0)
                return null;
            if (_keyboardCount == 1)
                return _firstKeyboard;
        }

        if (type == InputDeviceType.Mouse)
        {
            if (_mouseCount == 0)
                return null;
            if (_mouseCount == 1)
                return _firstMouse;
        }

        // Try cache first
        ArtemisDevice? cacheMatch = GetDeviceFromCache(provider, identifier);
        if (cacheMatch != null)
            return cacheMatch;

        ArtemisDevice? match = _devices.FirstOrDefault(m => m.InputIdentifiers.Any(i => Equals(i.InputProvider, provider.ProviderName) && Equals(i.Identifier, identifier)));

        // If a match was found cache it to speed up the next event and return the match
        if (match != null)
        {
            AddDeviceToCache(match, provider, identifier);
            return match;
        }

        if (type == InputDeviceType.Keyboard)
            return _firstKeyboard;
        if (type == InputDeviceType.Mouse)
            return _firstMouse;
        return null;
    }

    public void BustIdentifierCache()
    {
        _deviceCache.Clear();
        _devices = _deviceService.EnabledDevices.Where(d => d.InputIdentifiers.Any()).ToList();
    }

    private void AddDeviceToCache(ArtemisDevice match, InputProvider provider, object identifier)
    {
        _deviceCache.TryAdd(new Tuple<InputProvider, object>(provider, identifier), match);
    }

    private ArtemisDevice? GetDeviceFromCache(InputProvider provider, object identifier)
    {
        _deviceCache.TryGetValue(new Tuple<InputProvider, object>(provider, identifier), out ArtemisDevice? device);
        return device;
    }
    
    private void InputProviderOnIdentifierReceived(object? sender, InputProviderIdentifierEventArgs e)
    {
        // Don't match if there is no device or if the device type differs from the event device type
        if (_identifyingDevice == null ||
            (_identifyingDevice.DeviceType == RGBDeviceType.Keyboard && e.DeviceType == InputDeviceType.Mouse) ||
            (_identifyingDevice.DeviceType == RGBDeviceType.Mouse && e.DeviceType == InputDeviceType.Keyboard))
            return;
        if (!(sender is InputProvider inputProvider))
            return;

        // Remove existing identification
        _identifyingDevice.InputIdentifiers.RemoveAll(i => i.InputProvider == inputProvider.ProviderName);
        _identifyingDevice.InputIdentifiers.Add(new ArtemisDeviceInputIdentifier(inputProvider.ProviderName, e.Identifier));

        StopIdentify();
        OnDeviceIdentified();
    }

    private void DeviceServiceOnDevicesModified(object? sender, DeviceEventArgs args)
    {
        _firstKeyboard = _deviceService.Devices.FirstOrDefault(d => d.DeviceType == RGBDeviceType.Keyboard);
        _firstMouse = _deviceService.Devices.FirstOrDefault(d => d.DeviceType == RGBDeviceType.Mouse);
        _keyboardCount = _deviceService.Devices.Count(d => d.DeviceType == RGBDeviceType.Keyboard);
        _mouseCount = _deviceService.Devices.Count(d => d.DeviceType == RGBDeviceType.Mouse);

        BustIdentifierCache();
    }

    #endregion

    #region Keyboard

    private readonly Dictionary<ArtemisDevice, HashSet<KeyboardKey>> _pressedKeys = new();
    private readonly Dictionary<ArtemisDevice, KeyboardModifierKey> _keyboardModifier = new();
    private KeyboardModifierKey _globalModifiers;

    private void InputProviderOnKeyboardDataReceived(object? sender, InputProviderKeyboardEventArgs e)
    {
        KeyboardModifierKey keyboardModifierKey = UpdateModifierKeys(e.Device, e.Key, e.IsDown);

        // if UpdatePressedKeys is true, the key is already pressed, then we skip this event
        if (UpdatePressedKeys(e.Device, e.Key, e.IsDown))
            return;

        // Get the LED
        bool foundLedId = InputKeyUtilities.TryGetLedIdFromKeyboardKey(e.Key, out LedId ledId);
        // If we find a backslash but the keyboard is ISO, we need to use the non-US backslash.
        // The scancode is the same, but the LED ID is different
        if (ledId == LedId.Keyboard_Backslash && e.Device?.PhysicalLayout == KeyboardLayoutType.ISO)
        {
            ledId = LedId.Keyboard_NonUsTilde;
        }

        ArtemisLed? led = null;
        if (foundLedId && e.Device != null)
            led = e.Device.GetLed(ledId, true);

        // Create the UpDown event args because it can be used for every event
        ArtemisKeyboardKeyUpDownEventArgs eventArgs = new(e.Device, led, e.Key, keyboardModifierKey, e.IsDown);
        OnKeyboardKeyUpDown(eventArgs);
        if (e.IsDown)
            OnKeyboardKeyDown(eventArgs);
        else
            OnKeyboardKeyUp(eventArgs);

        // _logger.Verbose("Keyboard data: LED ID: {ledId}, key: {key}, is down: {isDown}, modifiers: {modifiers}, device: {device} ", ledId, e.Key, e.IsDown, keyboardModifierKey, e.Device);
    }

    private void InputProviderOnKeyboardToggleStatusReceived(object? sender, InputProviderKeyboardToggleEventArgs e)
    {
        KeyboardToggleStatus old = KeyboardToggleStatus;
        if (KeyboardToggleStatus.CapsLock == e.KeyboardToggleStatus.CapsLock &&
            KeyboardToggleStatus.NumLock == e.KeyboardToggleStatus.NumLock &&
            KeyboardToggleStatus.ScrollLock == e.KeyboardToggleStatus.ScrollLock)
            return;

        KeyboardToggleStatus = e.KeyboardToggleStatus;
        OnKeyboardToggleStatusChanged(new ArtemisKeyboardToggleStatusArgs(old, KeyboardToggleStatus));
    }

    private bool UpdatePressedKeys(ArtemisDevice? device, KeyboardKey key, bool isDown)
    {
        if (device != null)
        {
            // Ensure the device is in the dictionary
            _pressedKeys.TryAdd(device, new HashSet<KeyboardKey>());
            // Get the hash set of the device
            HashSet<KeyboardKey> pressedDeviceKeys = _pressedKeys[device];
            // See if the key is already pressed
            bool alreadyPressed = pressedDeviceKeys.Contains(key);

            // Prevent triggering already down keys again. When a key is held, we don't want to spam events like Windows does
            if (isDown && alreadyPressed)
                return true;

            // Either add or remove the key depending on its status
            if (isDown && !alreadyPressed)
                pressedDeviceKeys.Add(key);
            else if (!isDown && alreadyPressed)
                pressedDeviceKeys.Remove(key);
        }

        return false;
    }

    private KeyboardModifierKey UpdateModifierKeys(ArtemisDevice? device, KeyboardKey key, in bool isDown)
    {
        KeyboardModifierKey modifiers = _globalModifiers;
        if (device != null)
            _keyboardModifier.TryGetValue(device, out modifiers);

        if (key == KeyboardKey.LeftAlt || key == KeyboardKey.RightAlt)
        {
            if (isDown)
                modifiers |= KeyboardModifierKey.Alt;
            else
                modifiers &= ~KeyboardModifierKey.Alt;
        }
        else if (key == KeyboardKey.LeftCtrl || key == KeyboardKey.RightCtrl)
        {
            if (isDown)
                modifiers |= KeyboardModifierKey.Control;
            else
                modifiers &= ~KeyboardModifierKey.Control;
        }
        else if (key == KeyboardKey.LeftShift || key == KeyboardKey.RightShift)
        {
            if (isDown)
                modifiers |= KeyboardModifierKey.Shift;
            else
                modifiers &= ~KeyboardModifierKey.Shift;
        }
        else if (key == KeyboardKey.LeftWin || key == KeyboardKey.RightWin)
        {
            if (isDown)
                modifiers |= KeyboardModifierKey.Windows;
            else
                modifiers &= ~KeyboardModifierKey.Windows;
        }

        if (device != null)
            _keyboardModifier[device] = modifiers;
        else
            _globalModifiers = modifiers;

        return modifiers;
    }

    public void ReleaseAll()
    {
        foreach ((ArtemisDevice? device, HashSet<KeyboardKey>? keys) in _pressedKeys.ToList())
        {
            foreach (KeyboardKey keyboardKey in keys)
                InputProviderOnKeyboardDataReceived(this, new InputProviderKeyboardEventArgs(device, keyboardKey, false));
        }
    }

    public bool IsKeyDown(KeyboardKey key)
    {
        return _pressedKeys.Any(p => p.Value.Contains(key));
    }

    #endregion

    #region Mouse

    private readonly HashSet<MouseButton> _pressedButtons = new();


    private void InputProviderOnMouseButtonDataReceived(object? sender, InputProviderMouseButtonEventArgs e)
    {
        bool foundLedId = InputKeyUtilities.TryGetLedIdFromMouseButton(e.Button, out LedId ledId);
        ArtemisLed? led = null;
        if (foundLedId && e.Device != null)
            led = e.Device.Leds.FirstOrDefault(l => l.RgbLed.Id == ledId);

        // Create the UpDown event args because it can be used for every event
        ArtemisMouseButtonUpDownEventArgs eventArgs = new(e.Device, led, e.Button, e.IsDown);
        OnMouseButtonUpDown(eventArgs);
        if (e.IsDown)
        {
            if (!_pressedButtons.Contains(e.Button))
                _pressedButtons.Add(e.Button);
            OnMouseButtonDown(eventArgs);
        }
        else
        {
            if (_pressedButtons.Contains(e.Button))
                _pressedButtons.Remove(e.Button);
            OnMouseButtonUp(eventArgs);
        }

        // _logger.Verbose("Mouse button data: LED ID: {ledId}, button: {button}, is down: {isDown}, device: {device} ", ledId, e.Button, e.IsDown, e.Device);
    }

    private void InputProviderOnMouseScrollDataReceived(object? sender, InputProviderMouseScrollEventArgs e)
    {
        OnMouseScroll(new ArtemisMouseScrollEventArgs(e.Device, e.Direction, e.Delta));
        // _logger.Verbose("Mouse scroll data: Direction: {direction}, delta: {delta}, device: {device} ", e.Direction, e.Delta, e.Device);
    }

    private void InputProviderOnMouseMoveDataReceived(object? sender, InputProviderMouseMoveEventArgs e)
    {
        OnMouseMove(new ArtemisMouseMoveEventArgs(e.Device, e.CursorX, e.CursorY, e.DeltaX, e.DeltaY));
        // _logger.Verbose("Mouse move data: XY: {X},{Y} - delta XY: {deltaX},{deltaY} - device: {device} ", e.CursorX, e.CursorY, e.DeltaX, e.DeltaY, e.Device);
    }

    public bool IsButtonDown(MouseButton button)
    {
        return _pressedButtons.Contains(button);
    }

    #endregion
}