using System.Runtime.InteropServices;
using RioJoy.Core.Mapping;
using MouseButtonId = RioJoy.Core.Mapping.MouseButton;

namespace RioJoy.Core.Output;

/// <summary>
/// <see cref="IInputSink"/> backed by the Win32 <c>SendInput</c> API. Keys are
/// sent by <b>scancode</b> (VK → scancode via <c>MapVirtualKey</c>), matching the
/// legacy <c>Key</c> function (riovjoy2.cpp#L2055), so games that read raw
/// scancodes (most do) see the keypress. Targets the interactive session, which
/// is why RIOJoy is a tray app rather than a service (see docs/PLAN.md risks).
/// </summary>
public sealed class SendInputSink : IInputSink
{
    public void KeyDown(byte virtualKey, bool extended) => SendKey(virtualKey, extended, keyUp: false);

    public void KeyUp(byte virtualKey, bool extended) => SendKey(virtualKey, extended, keyUp: true);

    public void MouseMove(int dx, int dy)
    {
        var input = new INPUT
        {
            type = INPUT_MOUSE,
            U = { mi = { dx = dx, dy = dy, dwFlags = MOUSEEVENTF_MOVE } },
        };
        Send(input);
    }

    public void MouseButton(MouseButton button, bool down)
    {
        uint flags = (button, down) switch
        {
            (MouseButtonId.Left, true) => MOUSEEVENTF_LEFTDOWN,
            (MouseButtonId.Left, false) => MOUSEEVENTF_LEFTUP,
            (MouseButtonId.Right, true) => MOUSEEVENTF_RIGHTDOWN,
            (MouseButtonId.Right, false) => MOUSEEVENTF_RIGHTUP,
            _ => 0,
        };

        var input = new INPUT
        {
            type = INPUT_MOUSE,
            U = { mi = { dwFlags = flags } },
        };
        Send(input);
    }

    private static void SendKey(byte virtualKey, bool extended, bool keyUp)
    {
        uint flags = KEYEVENTF_SCANCODE;
        if (keyUp) flags |= KEYEVENTF_KEYUP;
        if (extended) flags |= KEYEVENTF_EXTENDEDKEY;

        var input = new INPUT
        {
            type = INPUT_KEYBOARD,
            U =
            {
                ki = new KEYBDINPUT
                {
                    wVk = 0,
                    wScan = (ushort)MapVirtualKey(virtualKey, MAPVK_VK_TO_VSC),
                    dwFlags = flags,
                },
            },
        };
        Send(input);
    }

    private static void Send(INPUT input) =>
        SendInput(1, new[] { input }, Marshal.SizeOf(typeof(INPUT)));

    // --- Win32 interop --------------------------------------------------------

    private const uint INPUT_MOUSE = 0;
    private const uint INPUT_KEYBOARD = 1;

    private const uint KEYEVENTF_EXTENDEDKEY = 0x0001;
    private const uint KEYEVENTF_KEYUP = 0x0002;
    private const uint KEYEVENTF_SCANCODE = 0x0008;

    private const uint MOUSEEVENTF_MOVE = 0x0001;
    private const uint MOUSEEVENTF_LEFTDOWN = 0x0002;
    private const uint MOUSEEVENTF_LEFTUP = 0x0004;
    private const uint MOUSEEVENTF_RIGHTDOWN = 0x0008;
    private const uint MOUSEEVENTF_RIGHTUP = 0x0010;

    private const uint MAPVK_VK_TO_VSC = 0;

    [StructLayout(LayoutKind.Sequential)]
    private struct INPUT
    {
        public uint type;
        public InputUnion U;
    }

    [StructLayout(LayoutKind.Explicit)]
    private struct InputUnion
    {
        [FieldOffset(0)] public MOUSEINPUT mi;
        [FieldOffset(0)] public KEYBDINPUT ki;
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct MOUSEINPUT
    {
        public int dx;
        public int dy;
        public uint mouseData;
        public uint dwFlags;
        public uint time;
        public nuint dwExtraInfo;
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct KEYBDINPUT
    {
        public ushort wVk;
        public ushort wScan;
        public uint dwFlags;
        public uint time;
        public nuint dwExtraInfo;
    }

    [DllImport("user32.dll", SetLastError = true)]
    private static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);

    [DllImport("user32.dll")]
    private static extern uint MapVirtualKey(uint uCode, uint uMapType);
}
