using System.Runtime.InteropServices;

namespace VoiceCat.App.Native;

/// <summary>
/// Thin wrapper over the Win32 Raw Input API (user32) used to observe the push-to-talk key
/// system-wide — i.e. while another application is focused.
///
/// We deliberately use Raw Input (<c>RegisterRawInputDevices</c> + <c>WM_INPUT</c> with
/// <c>RIDEV_INPUTSINK</c>) rather than a low-level keyboard hook (<c>SetWindowsHookEx</c> /
/// <c>WH_KEYBOARD_LL</c>). A low-level hook is the textbook keylogger pattern and is exactly
/// what antivirus heuristics flag — especially for an unsigned, statically linked MinGW binary
/// like ours. Raw Input involves no DLL injection and no global hook: the OS simply posts
/// <c>WM_INPUT</c> messages to our own window's message queue (it is the same input path games
/// use), so it does not trip those heuristics. It also passes keystrokes through to the focused
/// app rather than swallowing them.
///
/// Only the keyboard device is registered; <see cref="TryParseKey"/> filters to the single PTT
/// virtual-key code one level up (MainForm).
/// </summary>
internal static partial class RawInput
{
    // ── Constants ──────────────────────────────────────────────────────────────────────────
    public const int WM_INPUT = 0x00FF;

    private const uint RID_INPUT = 0x10000003;     // GetRawInputData: get the raw data
    private const uint RIM_TYPEKEYBOARD = 1;       // RAWINPUTHEADER.dwType for a keyboard
    private const uint RIDEV_INPUTSINK = 0x00000100; // receive input even when not in foreground
    private const uint RIDEV_REMOVE = 0x00000001;  // stop receiving input from the device
    private const ushort RI_KEY_BREAK = 0x01;      // RAWKEYBOARD.Flags bit set on key-up

    private const ushort HID_USAGE_PAGE_GENERIC = 0x01;
    private const ushort HID_USAGE_GENERIC_KEYBOARD = 0x06;

    // ── Structs (must match the Win32 layout exactly) ──────────────────────────────────────
    [StructLayout(LayoutKind.Sequential)]
    private struct RAWINPUTDEVICE
    {
        public ushort usUsagePage;
        public ushort usUsage;
        public uint dwFlags;
        public IntPtr hwndTarget;
    }

    [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;
    }

    // ── P/Invoke (source-generated via LibraryImport, matching VoiceCat.Interop) ────────────
    [LibraryImport("user32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static partial bool RegisterRawInputDevices(
        ref RAWINPUTDEVICE pRawInputDevices, uint uiNumDevices, uint cbSize);

    [LibraryImport("user32.dll")]
    private static partial uint GetRawInputData(
        IntPtr hRawInput, uint uiCommand, IntPtr pData, ref uint pcbSize, uint cbSizeHeader);

    [LibraryImport("user32.dll")]
    private static partial short GetAsyncKeyState(int vKey);

    // ── Public helpers ─────────────────────────────────────────────────────────────────────

    /// <summary>Register a background keyboard sink targeting <paramref name="hwnd"/>. With
    /// RIDEV_INPUTSINK the window receives WM_INPUT even when it is not in the foreground.</summary>
    public static bool RegisterKeyboardSink(IntPtr hwnd)
    {
        var rid = new RAWINPUTDEVICE
        {
            usUsagePage = HID_USAGE_PAGE_GENERIC,
            usUsage = HID_USAGE_GENERIC_KEYBOARD,
            dwFlags = RIDEV_INPUTSINK,
            hwndTarget = hwnd,
        };
        return RegisterRawInputDevices(ref rid, 1, (uint)Marshal.SizeOf<RAWINPUTDEVICE>());
    }

    /// <summary>Tear down the keyboard sink. For RIDEV_REMOVE the target handle must be NULL.</summary>
    public static bool UnregisterKeyboardSink()
    {
        var rid = new RAWINPUTDEVICE
        {
            usUsagePage = HID_USAGE_PAGE_GENERIC,
            usUsage = HID_USAGE_GENERIC_KEYBOARD,
            dwFlags = RIDEV_REMOVE,
            hwndTarget = IntPtr.Zero,
        };
        return RegisterRawInputDevices(ref rid, 1, (uint)Marshal.SizeOf<RAWINPUTDEVICE>());
    }

    /// <summary>Decode a WM_INPUT message's <paramref name="hRawInput"/> (the message's LParam).
    /// Returns false for anything that isn't a keyboard event. On success, <paramref name="vkey"/>
    /// is the virtual-key code and <paramref name="keyUp"/> distinguishes a release from a press
    /// (auto-repeat arrives as repeated presses).</summary>
    public static bool TryParseKey(IntPtr hRawInput, out ushort vkey, out bool keyUp)
    {
        vkey = 0;
        keyUp = false;

        uint headerSize = (uint)Marshal.SizeOf<RAWINPUTHEADER>();
        uint size = 0;
        // First call (pData == NULL) returns 0 on success and fills `size` with the buffer length.
        if (GetRawInputData(hRawInput, RID_INPUT, IntPtr.Zero, ref size, headerSize) != 0 || size == 0)
            return false;

        IntPtr buf = Marshal.AllocHGlobal((int)size);
        try
        {
            if (GetRawInputData(hRawInput, RID_INPUT, buf, ref size, headerSize) != size)
                return false;

            var header = Marshal.PtrToStructure<RAWINPUTHEADER>(buf);
            if (header.dwType != RIM_TYPEKEYBOARD)
                return false;

            // The keyboard payload immediately follows the (8-byte-aligned) header.
            var kb = Marshal.PtrToStructure<RAWKEYBOARD>(buf + (int)headerSize);
            vkey = kb.VKey;
            keyUp = (kb.Flags & RI_KEY_BREAK) != 0;
            return true;
        }
        finally
        {
            Marshal.FreeHGlobal(buf);
        }
    }

    /// <summary>True if the given virtual-key code is currently held down. Used as a watchdog to
    /// catch a missed key-up (e.g. across an RDP / lock-screen focus switch) so PTT can't stick.</summary>
    public static bool IsKeyDown(int vKey) => (GetAsyncKeyState(vKey) & 0x8000) != 0;
}
