using Insight.Remote.Shared.Abstractions;
using Insight.Remote.Shared.Enums;
using Insight.Remote.Shared.Native.Windows;
using Microsoft.Extensions.Logging;
using System.Threading.Channels;

namespace Insight.Remote.Windows.Services;

internal class WinInputProvider : IInputProvider
{
    private volatile bool _inputBlocked;

    private readonly Channel<Action> _inputs = Channel.CreateUnbounded<Action>(new UnboundedChannelOptions { AllowSynchronousContinuations = false });

    private readonly IScreenProvider _screenProvider;
    private readonly ILogger<WinInputProvider> _logger;

    public WinInputProvider(IScreenProvider screenProvider, ILogger<WinInputProvider> logger)
    {
        _screenProvider = screenProvider;
        _logger = logger;
    }

    public async Task InitAsync(CancellationToken cancellationToken)
    {
        try
        {
            while (cancellationToken.IsCancellationRequested is false)
            {
                await ThreadedInputProcessing(cancellationToken);
                await Task.Delay(1000, cancellationToken);
            }
        }
        catch (TaskCanceledException) { }
        catch (Exception ex)
        {
            _logger.LogError(ex.ToString());
        }
    }

    private async Task ThreadedInputProcessing(CancellationToken cancellationToken)
    {
        var semaphore = new SemaphoreSlim(0, 1);

        var thread = new Thread(async () =>
        {
            //_logger.LogInformation("New input processing thread started on thread {threadId}", Thread.CurrentThread.ManagedThreadId);

            while (cancellationToken.IsCancellationRequested is false)
            {
                try
                {
                    if (await _inputs.Reader.WaitToReadAsync(cancellationToken) is false) return;

                    // Thread likely has hooks in current desktop. SendKeys will create one with no way to unhook it
                    // Start a new thread for processing input => break
                    if (Win32Interop.SwitchToInputDesktop() is false)
                    {
                        _logger.LogWarning("Desktop switch failed during input processing.");
                        break;
                    }

                    var input = await _inputs.Reader.ReadAsync(cancellationToken);
                    if (_inputBlocked) continue;

                    input();
                }
                catch (OperationCanceledException) { }
                catch (Exception ex)
                {
                    _logger.LogError(ex.Message);
                }
            }

            //_logger.LogInformation("Stopping input processing on thread {threadId}", Environment.CurrentManagedThreadId);
            semaphore.Release();
        });

        thread.SetApartmentState(ApartmentState.STA);
        thread.Start();

        try
        {
            await semaphore.WaitAsync(cancellationToken);
        }
        catch (TaskCanceledException) { }
        catch (Exception ex)
        {
            _logger.LogError(ex.ToString());
        }
    }

    public async Task SendKeyDownAsync(string key, CancellationToken cancellationToken)
    {
        if (ConvertJavaScriptKeyToVirtualKey(key, out var keyCode) is false || keyCode is null) return;

        try
        {
            await _inputs.Writer.WriteAsync(() =>
            {
                var union = new User32.InputUnion()
                {
                    ki = new User32.KEYBDINPUT()
                    {
                        wVk = keyCode.Value,
                        wScan = (User32.ScanCodeShort)User32.MapVirtualKeyEx((uint)keyCode.Value, User32.VkMapType.MAPVK_VK_TO_VSC, User32.GetKeyboardLayout()),
                        time = 0,
                        dwExtraInfo = User32.GetMessageExtraInfo()
                    }
                };

                var input = new User32.INPUT() { type = User32.InputType.KEYBOARD, U = union };
                _ = User32.SendInput(1, new User32.INPUT[] { input }, User32.INPUT.Size);
            }, cancellationToken);
        }
        catch (TaskCanceledException) { }
        catch (Exception ex)
        {
            _logger.LogError(ex.ToString());
        }
    }

    public async Task SendKeyUpAsync(string key, CancellationToken cancellationToken)
    {
        if (ConvertJavaScriptKeyToVirtualKey(key, out var keyCode) is false || keyCode is null) return;

        try
        {
            await _inputs.Writer.WriteAsync(() =>
            {
                var union = new User32.InputUnion()
                {
                    ki = new User32.KEYBDINPUT()
                    {
                        wVk = keyCode.Value,
                        wScan = (User32.ScanCodeShort)User32.MapVirtualKeyEx((uint)keyCode.Value, User32.VkMapType.MAPVK_VK_TO_VSC, User32.GetKeyboardLayout()),
                        time = 0,
                        dwFlags = User32.KEYEVENTF.KEYUP,
                        dwExtraInfo = User32.GetMessageExtraInfo()
                    }
                };
                var input = new User32.INPUT() { type = User32.InputType.KEYBOARD, U = union };
                _ = User32.SendInput(1, new User32.INPUT[] { input }, User32.INPUT.Size);
            }, cancellationToken);
        }
        catch (TaskCanceledException) { }
        catch (Exception ex)
        {
            _logger.LogError(ex.ToString());
        }
    }

    public async Task SendMouseButtonActionAsync(int button, ButtonAction buttonAction, double percentX, double percentY, CancellationToken cancellationToken)
    {
        try
        {
            await _inputs.Writer.WriteAsync(() =>
            {
                User32.MOUSEEVENTF mouseEvent;
                switch (button)
                {
                    case 0:
                        switch (buttonAction)
                        {
                            case ButtonAction.Down:
                                mouseEvent = User32.MOUSEEVENTF.LEFTDOWN;
                                break;
                            case ButtonAction.Up:
                                mouseEvent = User32.MOUSEEVENTF.LEFTUP;
                                break;
                            default:
                                return;
                        }
                        break;
                    case 1:
                        switch (buttonAction)
                        {
                            case ButtonAction.Down:
                                mouseEvent = User32.MOUSEEVENTF.MIDDLEDOWN;
                                break;
                            case ButtonAction.Up:
                                mouseEvent = User32.MOUSEEVENTF.MIDDLEUP;
                                break;
                            default:
                                return;
                        }
                        break;
                    case 2:
                        switch (buttonAction)
                        {
                            case ButtonAction.Down:
                                mouseEvent = User32.MOUSEEVENTF.RIGHTDOWN;
                                break;
                            case ButtonAction.Up:
                                mouseEvent = User32.MOUSEEVENTF.RIGHTUP;
                                break;
                            default:
                                return;
                        }
                        break;
                    default:
                        return;
                }

                var xyPercent = GetAbsolutePercentFromRelativePercent(percentX, percentY, _screenProvider);

                // Coordinates must be normalized.  The bottom-right coordinate is mapped to 65535.
                var normalizedX = xyPercent.Item1 * 65535D;
                var normalizedY = xyPercent.Item2 * 65535D;
                var union = new User32.InputUnion() { mi = new User32.MOUSEINPUT() { dwFlags = User32.MOUSEEVENTF.ABSOLUTE | mouseEvent | User32.MOUSEEVENTF.VIRTUALDESK, dx = (int)normalizedX, dy = (int)normalizedY, time = 0, mouseData = 0, dwExtraInfo = User32.GetMessageExtraInfo() } };
                var input = new User32.INPUT() { type = User32.InputType.MOUSE, U = union };
                _ = User32.SendInput(1, new User32.INPUT[] { input }, User32.INPUT.Size);
            }, cancellationToken);
        }
        catch (TaskCanceledException) { }
        catch (Exception ex)
        {
            _logger.LogError(ex.ToString());
        }
    }

    public async Task SendMouseMoveAsync(double percentX, double percentY, CancellationToken cancellationToken)
    {
        try
        {
            await _inputs.Writer.WriteAsync(() =>
            {
                var xyPercent = GetAbsolutePercentFromRelativePercent(percentX, percentY, _screenProvider);
                // Coordinates must be normalized.  The bottom-right coordinate is mapped to 65535.
                var normalizedX = xyPercent.Item1 * 65535D;
                var normalizedY = xyPercent.Item2 * 65535D;
                var union = new User32.InputUnion() { mi = new User32.MOUSEINPUT() { dwFlags = User32.MOUSEEVENTF.ABSOLUTE | User32.MOUSEEVENTF.MOVE | User32.MOUSEEVENTF.VIRTUALDESK, dx = (int)normalizedX, dy = (int)normalizedY, time = 0, mouseData = 0, dwExtraInfo = User32.GetMessageExtraInfo() } };
                var input = new User32.INPUT() { type = User32.InputType.MOUSE, U = union };
                _ = User32.SendInput(1, new User32.INPUT[] { input }, User32.INPUT.Size);
            }, cancellationToken);
        }
        catch (TaskCanceledException) { }
        catch (Exception ex)
        {
            _logger.LogError(ex.ToString());
        }
    }

    public async Task SendMouseWheelAsync(int deltaY, CancellationToken cancellationToken)
    {
        try
        {
            await _inputs.Writer.WriteAsync(() =>
            {
                if (deltaY < 0)
                {
                    deltaY = -120;
                }
                else if (deltaY > 0)
                {
                    deltaY = 120;
                }
                var union = new User32.InputUnion() { mi = new User32.MOUSEINPUT() { dwFlags = User32.MOUSEEVENTF.WHEEL, dx = 0, dy = 0, time = 0, mouseData = deltaY, dwExtraInfo = User32.GetMessageExtraInfo() } };
                var input = new User32.INPUT() { type = User32.InputType.MOUSE, U = union };
                _ = User32.SendInput(1, new User32.INPUT[] { input }, User32.INPUT.Size);
            }, cancellationToken);
        }
        catch (TaskCanceledException) { }
        catch (Exception ex)
        {
            _logger.LogError(ex.ToString());
        }
    }

    public async Task SendTextAsync(string transferText, CancellationToken cancellationToken)
    {
        try
        {
            await _inputs.Writer.WriteAsync(() =>
            {
                SendKeys.SendWait(transferText);
            }, cancellationToken);
        }
        catch (TaskCanceledException) { }
        catch (Exception ex)
        {
            _logger.LogError(ex.ToString());
        }
    }

    public async Task SetKeyStatesUpAsync(CancellationToken cancellationToken)
    {
        try
        {
            await _inputs.Writer.WriteAsync(() =>
            {
                foreach (User32.VirtualKey key in Enum.GetValues(typeof(User32.VirtualKey)))
                {
                    try
                    {
                        var state = User32.GetKeyState(key);
                        if (state == -127)
                        {
                            var union = new User32.InputUnion()
                            {
                                ki = new User32.KEYBDINPUT()
                                {
                                    wVk = key,
                                    wScan = 0,
                                    time = 0,
                                    dwFlags = User32.KEYEVENTF.KEYUP,
                                    dwExtraInfo = User32.GetMessageExtraInfo()
                                }
                            };
                            var input = new User32.INPUT() { type = User32.InputType.KEYBOARD, U = union };
                            _ = User32.SendInput(1, new User32.INPUT[] { input }, User32.INPUT.Size);
                        }
                    }
                    catch { }
                }
            }, cancellationToken);
        }
        catch (TaskCanceledException) { }
        catch (Exception ex)
        {
            _logger.LogError(ex.ToString());
        }
    }

    public async Task ToggleBlockInputAsync(bool toggleOn, CancellationToken cancellationToken)
    {
        try
        {
            await _inputs.Writer.WriteAsync(() =>
            {
                _inputBlocked = toggleOn;
                var result = User32.BlockInput(toggleOn);

                _logger.LogInformation("Result of ToggleBlockInput set to {toggleOn}: {result}", toggleOn, result);
            }, cancellationToken);
        }
        catch (TaskCanceledException) { }
        catch (Exception ex)
        {
            _logger.LogError(ex.ToString());
        }
    }

    private bool ConvertJavaScriptKeyToVirtualKey(string key, out User32.VirtualKey? result)
    {
        result = key switch
        {
            "Down" or "ArrowDown" => User32.VirtualKey.DOWN,
            "Up" or "ArrowUp" => User32.VirtualKey.UP,
            "Left" or "ArrowLeft" => User32.VirtualKey.LEFT,
            "Right" or "ArrowRight" => User32.VirtualKey.RIGHT,
            "Enter" => User32.VirtualKey.RETURN,
            "Esc" or "Escape" => User32.VirtualKey.ESCAPE,
            "Alt" => User32.VirtualKey.MENU,
            "Control" => User32.VirtualKey.CONTROL,
            "Shift" => User32.VirtualKey.SHIFT,
            "PAUSE" => User32.VirtualKey.PAUSE,
            "BREAK" => User32.VirtualKey.PAUSE,
            "Backspace" => User32.VirtualKey.BACK,
            "Tab" => User32.VirtualKey.TAB,
            "CapsLock" => User32.VirtualKey.CAPITAL,
            "Delete" => User32.VirtualKey.DELETE,
            "Home" => User32.VirtualKey.HOME,
            "End" => User32.VirtualKey.END,
            "PageUp" => User32.VirtualKey.PRIOR,
            "PageDown" => User32.VirtualKey.NEXT,
            "NumLock" => User32.VirtualKey.NUMLOCK,
            "Insert" => User32.VirtualKey.INSERT,
            "ScrollLock" => User32.VirtualKey.SCROLL,
            "F1" => User32.VirtualKey.F1,
            "F2" => User32.VirtualKey.F2,
            "F3" => User32.VirtualKey.F3,
            "F4" => User32.VirtualKey.F4,
            "F5" => User32.VirtualKey.F5,
            "F6" => User32.VirtualKey.F6,
            "F7" => User32.VirtualKey.F7,
            "F8" => User32.VirtualKey.F8,
            "F9" => User32.VirtualKey.F9,
            "F10" => User32.VirtualKey.F10,
            "F11" => User32.VirtualKey.F11,
            "F12" => User32.VirtualKey.F12,
            "Meta" => User32.VirtualKey.LWIN,
            "ContextMenu" => User32.VirtualKey.MENU,
            _ => key.Length == 1 ? (User32.VirtualKey)User32.VkKeyScan(Convert.ToChar(key)) : null
        };

        if (result is not null) return true;

        _logger.LogWarning("Unable to parse key input: {key}.", key);
        return false;
    }

    private static Tuple<double, double> GetAbsolutePercentFromRelativePercent(double percentX, double percentY, IScreenProvider capturer)
    {
        var absoluteX = capturer.CurrentScreenBounds.Width * percentX + capturer.CurrentScreenBounds.Left - capturer.GetVirtualScreenBounds().Left;
        var absoluteY = capturer.CurrentScreenBounds.Height * percentY + capturer.CurrentScreenBounds.Top - capturer.GetVirtualScreenBounds().Top;
        return new Tuple<double, double>(absoluteX / capturer.GetVirtualScreenBounds().Width, absoluteY / capturer.GetVirtualScreenBounds().Height);
    }

    private static Tuple<double, double> GetAbsolutePointFromRelativePercent(double percentX, double percentY, IScreenProvider capturer)
    {
        var absoluteX = capturer.CurrentScreenBounds.Width * percentX + capturer.CurrentScreenBounds.Left;
        var absoluteY = capturer.CurrentScreenBounds.Height * percentY + capturer.CurrentScreenBounds.Top;
        return new Tuple<double, double>(absoluteX, absoluteY);
    }
}