using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Windows.Forms;
using Microsoft.Win32;

namespace VRio.App;

/// <summary>
/// A background keyboard input source built on the Win32 <b>Raw Input</b> API.
/// Where the WinForms focus path (<c>ProcessCmdKey</c>/<c>OnKeyUp</c>) only sees
/// keys while vRIO is the foreground window and can't tell one keyboard from
/// another, Raw Input carries a per-device handle with every keystroke and —
/// with the <c>RIDEV_INPUTSINK</c> flag — keeps delivering <c>WM_INPUT</c> even
/// when vRIO is in the background. That lets the user nominate one physical
/// keyboard as a dedicated cockpit panel: its keys drive the RIO controls while
/// the sim (or RIOJoy) holds focus, and other keyboards are ignored.
///
/// <para>This is the input-side twin of <see cref="KeyboardLampMirror"/>: that
/// class <em>writes</em> a chosen keyboard's LEDs while the game has focus, this
/// one <em>reads</em> a chosen keyboard's keys under the same condition. Raw
/// Input observes without intercepting — the keystroke still reaches whatever
/// app has focus — so the selected keyboard should be one dedicated to the
/// panel, not the one you also type on.</para>
///
/// <para>Registration is scoped to when a device is actually selected
/// (<see cref="SetTarget"/>): picking "all keyboards" unregisters and hands
/// input back to the focus path, so vRIO isn't pumping <c>WM_INPUT</c> for every
/// keystroke system-wide when the feature is idle. Not thread-safe;
/// <see cref="ProcessInput"/> and the events fire on the UI thread that owns the
/// window handle, which is exactly where <see cref="Input.InputRouter"/> wants
/// to be called.</para>
/// </summary>
public sealed class RawKeyboardSource : IDisposable
{
    // --- Win32 Raw Input interop ---------------------------------------------

    [StructLayout(LayoutKind.Sequential)]
    private struct RAWINPUTDEVICE
    {
        public ushort usUsagePage;
        public ushort usUsage;
        public uint dwFlags;
        public IntPtr hwndTarget;
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct RAWINPUTDEVICELIST
    {
        public IntPtr hDevice;
        public uint dwType;
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct RAWINPUTHEADER
    {
        public uint dwType;
        public uint dwSize;
        public IntPtr hDevice;
        public IntPtr wParam;
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct RAWKEYBOARD
    {
        public ushort MakeCode;
        public ushort Flags;
        public ushort Reserved;
        public ushort VKey;
        public uint Message;
        public uint ExtraInformation;
    }

    [DllImport("user32.dll", SetLastError = true)]
    private static extern bool RegisterRawInputDevices(
        [In] RAWINPUTDEVICE[] pRawInputDevices, uint uiNumDevices, uint cbSize);

    [DllImport("user32.dll", SetLastError = true)]
    private static extern uint GetRawInputData(
        IntPtr hRawInput, uint uiCommand, IntPtr pData, ref uint pcbSize, uint cbSizeHeader);

    [DllImport("user32.dll", SetLastError = true)]
    private static extern uint GetRawInputDeviceList(
        [In, Out] RAWINPUTDEVICELIST[]? pRawInputDeviceList, ref uint puiNumDevices, uint cbSize);

    [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
    private static extern uint GetRawInputDeviceInfoW(
        IntPtr hDevice, uint uiCommand, IntPtr pData, ref uint pcbSize);

    private const uint Err = 0xFFFFFFFF;                // sentinel: all three APIs return (UINT)-1 on failure
    private const uint RIDEV_REMOVE = 0x00000001;
    private const uint RIDEV_INPUTSINK = 0x00000100;
    private const uint RIDEV_DEVNOTIFY = 0x00002000;    // WM_INPUT_DEVICE_CHANGE on hot-plug
    private const uint RID_INPUT = 0x10000003;
    private const uint RIDI_DEVICENAME = 0x20000007;
    private const uint RIM_TYPEKEYBOARD = 1;
    private const ushort RI_KEY_BREAK = 0x01;           // set = key up (make/break)
    private const ushort HidUsagePageGeneric = 0x01;
    private const ushort HidUsageKeyboard = 0x06;

    private static readonly int HeaderSize = Marshal.SizeOf<RAWINPUTHEADER>();

    // --- State ---------------------------------------------------------------

    // hDevice → interface path, so the hot per-keystroke path skips the
    // two-call GetRawInputDeviceInfo dance once a device is known.
    private readonly Dictionary<IntPtr, string> _pathByHandle = new();

    private IntPtr _hwnd;
    private string? _target;     // interface path of the keyboard to capture; null = disabled
    private bool _registered;
    private bool _disposed;

    /// <summary>A key on the captured keyboard went down (.NET <see cref="Keys"/> name).</summary>
    public event Action<string>? KeyDown;

    /// <summary>A key on the captured keyboard came up (.NET <see cref="Keys"/> name).</summary>
    public event Action<string>? KeyUp;

    /// <summary>The set of attached keyboards changed (interface path, display name) — for a picker.</summary>
    public event Action<IReadOnlyList<(string Id, string Name)>>? KeyboardsChanged;

    /// <summary>Status/diagnostic lines.</summary>
    public event Action<string>? Logged;

    /// <summary>True while a specific keyboard is being captured (focus path should stand down).</summary>
    public bool IsCapturing => _target is not null;

    /// <summary>Bind to the window whose message loop delivers <c>WM_INPUT</c>.</summary>
    public void Attach(IntPtr hwnd) => _hwnd = hwnd;

    /// <summary>
    /// Capture one keyboard by interface path, or none (<paramref name="deviceId"/>
    /// null) to release input back to the focus path. Registering/unregistering
    /// Raw Input is scoped here so vRIO only taps the global key stream while the
    /// feature is actually in use.
    /// </summary>
    public void SetTarget(string? deviceId)
    {
        if (_target == deviceId)
            return;
        _target = deviceId;
        if (deviceId is null)
            Unregister();
        else
            Register();
    }

    /// <summary>Re-enumerate keyboards and raise <see cref="KeyboardsChanged"/>.</summary>
    public void RefreshKeyboards()
    {
        _pathByHandle.Clear(); // handles are only stable per connection
        KeyboardsChanged?.Invoke(EnumerateKeyboards());
    }

    /// <summary>Handle a <c>WM_INPUT_DEVICE_CHANGE</c>: a keyboard arrived or left.</summary>
    public void HandleDeviceChange() => RefreshKeyboards();

    /// <summary>
    /// Handle a <c>WM_INPUT</c> message (pass its <c>LParam</c>). Silently ignores
    /// input from any keyboard other than the captured one.
    /// </summary>
    public void ProcessInput(IntPtr hRawInput)
    {
        string? target = _target;
        if (target is null)
            return;

        uint size = 0;
        if (GetRawInputData(hRawInput, RID_INPUT, IntPtr.Zero, ref size, (uint)HeaderSize) == Err || size == 0)
            return;

        IntPtr buffer = Marshal.AllocHGlobal((int)size);
        try
        {
            if (GetRawInputData(hRawInput, RID_INPUT, buffer, ref size, (uint)HeaderSize) != size)
                return;

            var header = Marshal.PtrToStructure<RAWINPUTHEADER>(buffer);
            if (header.dwType != RIM_TYPEKEYBOARD)
                return;
            string? path = PathFor(header.hDevice);
            if (!string.Equals(path, target, StringComparison.OrdinalIgnoreCase))
                return;

            var kb = Marshal.PtrToStructure<RAWKEYBOARD>(IntPtr.Add(buffer, HeaderSize));
            ushort vkey = kb.VKey;
            // 0 = no VK, 0xFF = fake key emitted as part of an escaped sequence.
            if (vkey is 0 or 0xFF)
                return;

            string key = ((Keys)vkey).ToString();
            if ((kb.Flags & RI_KEY_BREAK) != 0)
                KeyUp?.Invoke(key);
            else
                KeyDown?.Invoke(key); // repeats while held are dropped by the router
        }
        finally
        {
            Marshal.FreeHGlobal(buffer);
        }
    }

    public void Dispose()
    {
        if (_disposed)
            return;
        _disposed = true;
        _target = null;
        Unregister();
        KeyDown = null;
        KeyUp = null;
        KeyboardsChanged = null;
        Logged = null;
    }

    // --- Registration --------------------------------------------------------

    private void Register()
    {
        if (_registered || _hwnd == IntPtr.Zero)
            return;
        var rid = new[]
        {
            new RAWINPUTDEVICE
            {
                usUsagePage = HidUsagePageGeneric,
                usUsage = HidUsageKeyboard,
                dwFlags = RIDEV_INPUTSINK | RIDEV_DEVNOTIFY, // background delivery + hot-plug notices
                hwndTarget = _hwnd,
            },
        };
        if (RegisterRawInputDevices(rid, 1, (uint)Marshal.SizeOf<RAWINPUTDEVICE>()))
            _registered = true;
        else
            Logged?.Invoke($"Raw keyboard capture unavailable (error {Marshal.GetLastWin32Error()})");
    }

    private void Unregister()
    {
        if (!_registered)
            return;
        // RIDEV_REMOVE must pass a null target window, or the call fails.
        var rid = new[]
        {
            new RAWINPUTDEVICE
            {
                usUsagePage = HidUsagePageGeneric,
                usUsage = HidUsageKeyboard,
                dwFlags = RIDEV_REMOVE,
                hwndTarget = IntPtr.Zero,
            },
        };
        RegisterRawInputDevices(rid, 1, (uint)Marshal.SizeOf<RAWINPUTDEVICE>());
        _registered = false;
    }

    // --- Enumeration / naming ------------------------------------------------

    private List<(string Id, string Name)> EnumerateKeyboards()
    {
        var result = new List<(string, string)>();
        uint listSize = (uint)Marshal.SizeOf<RAWINPUTDEVICELIST>();
        uint count = 0;
        if (GetRawInputDeviceList(null, ref count, listSize) == Err || count == 0)
            return result;

        var devices = new RAWINPUTDEVICELIST[count];
        uint got = GetRawInputDeviceList(devices, ref count, listSize);
        if (got == Err)
            return result;

        var labelCounts = new Dictionary<string, int>();
        for (int i = 0; i < got; i++)
        {
            if (devices[i].dwType != RIM_TYPEKEYBOARD)
                continue;
            string? path = PathFor(devices[i].hDevice);
            if (string.IsNullOrEmpty(path))
                continue;

            string label = FriendlyName(path!);
            // Physical keyboards can surface as more than one collection; a bare
            // count suffix keeps otherwise-identical labels distinguishable.
            if (labelCounts.TryGetValue(label, out int n))
            {
                labelCounts[label] = n + 1;
                label = $"{label} ({n + 1})";
            }
            else
            {
                labelCounts[label] = 1;
            }
            result.Add((path!, label));
        }
        return result;
    }

    private string? PathFor(IntPtr hDevice)
    {
        if (_pathByHandle.TryGetValue(hDevice, out string? cached))
            return cached;
        string? path = DeviceName(hDevice);
        if (path is not null)
            _pathByHandle[hDevice] = path;
        return path;
    }

    private static string? DeviceName(IntPtr hDevice)
    {
        uint chars = 0;
        if (GetRawInputDeviceInfoW(hDevice, RIDI_DEVICENAME, IntPtr.Zero, ref chars) == Err || chars == 0)
            return null;

        IntPtr buffer = Marshal.AllocHGlobal((int)chars * sizeof(char));
        try
        {
            if (GetRawInputDeviceInfoW(hDevice, RIDI_DEVICENAME, buffer, ref chars) == Err)
                return null;
            return Marshal.PtrToStringUni(buffer);
        }
        finally
        {
            Marshal.FreeHGlobal(buffer);
        }
    }

    /// <summary>
    /// Turn a raw device interface path into something a human recognizes: the
    /// device's registry description if we can resolve it, else its VID:PID.
    /// </summary>
    private static string FriendlyName(string interfacePath)
    {
        string? desc = RegistryName(interfacePath);
        if (!string.IsNullOrWhiteSpace(desc))
            return desc!;

        Match m = Regex.Match(interfacePath, @"VID_([0-9A-Fa-f]{4}).*?PID_([0-9A-Fa-f]{4})");
        return m.Success
            ? $"Keyboard {m.Groups[1].Value.ToUpperInvariant()}:{m.Groups[2].Value.ToUpperInvariant()}"
            : "Keyboard";
    }

    private static string? RegistryName(string interfacePath)
    {
        try
        {
            // \\?\HID#VID_x&PID_y#instance#{class-guid}  →  Enum key HID\VID_x&PID_y\instance
            string s = interfacePath;
            if (s.StartsWith(@"\\?\", StringComparison.Ordinal) || s.StartsWith(@"\\.\", StringComparison.Ordinal))
                s = s.Substring(4);
            int guid = s.LastIndexOf("#{", StringComparison.Ordinal);
            if (guid >= 0)
                s = s.Substring(0, guid);
            string instance = s.Replace('#', '\\');

            using RegistryKey? key = Registry.LocalMachine.OpenSubKey(
                @"SYSTEM\CurrentControlSet\Enum\" + instance);
            if (key is null)
                return null;

            string? name = key.GetValue("FriendlyName") as string;
            if (string.IsNullOrWhiteSpace(name))
                name = key.GetValue("DeviceDesc") as string;
            if (string.IsNullOrWhiteSpace(name))
                return null;

            // DeviceDesc is "@driver.inf,%token%;Actual Name" — keep the tail.
            int semi = name!.LastIndexOf(';');
            return semi >= 0 ? name.Substring(semi + 1) : name;
        }
        catch (Exception ex) when (ex is System.Security.SecurityException or UnauthorizedAccessException or IOException)
        {
            return null;
        }
    }
}
