using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
using RioJoy.Core.Calibration;
using RioJoy.Core.Hid;
using RioJoy.Core.Mapping;

namespace RioJoy.Core.Output;

/// <summary>
/// <see cref="IJoystickSink"/> that feeds the RioGamepad virtual HID driver: it
/// opens the driver's device interface, maintains the current
/// <see cref="RioHidReport"/>, and submits the whole report on every change via
/// <c>DeviceIoControl(IOCTL_RIO_SUBMIT_REPORT)</c>. Replaces
/// <see cref="NullJoystickSink"/> once the driver is installed (Phase 1/3b).
/// </summary>
public sealed class HidFeederJoystickSink : IJoystickSink, IDisposable
{
    // Must match driver/RioGamepad/Public.h.
    private static readonly Guid InterfaceGuid =
        new(0xb6a3f1c2, 0x7e84, 0x4d2a, 0x9c, 0x1f, 0x2a, 0x5e, 0x8d, 0x3b, 0x60, 0x71);

    // CTL_CODE(FILE_DEVICE_UNKNOWN=0x22, 0x800, METHOD_BUFFERED=0, FILE_WRITE_ACCESS=0x2).
    private const uint IoctlSubmitReport = (0x22u << 16) | (0x2u << 14) | (0x800u << 2);

    private readonly SafeFileHandle _device;
    private readonly RioHidReport _report = new();
    private readonly object _gate = new();

    private HidFeederJoystickSink(SafeFileHandle device) => _device = device;

    /// <summary>
    /// Try to open the virtual joystick driver. Returns <see langword="false"/> if
    /// the driver is not installed/present, so callers can fall back to
    /// <see cref="NullJoystickSink"/>.
    /// </summary>
    public static bool TryCreate(out HidFeederJoystickSink? sink)
    {
        sink = null;
#if NET40
        // Windows XP: RioGamepadXP is a HID minidriver whose sideband control
        // device can't carry a device interface (no PDO), so it's opened by its
        // fixed symbolic-link name rather than via SetupDi + interface GUID.
        // Contract is otherwise identical (see driver/RioGamepadXP/Public.h).
        string? path = @"\\.\RioGamepadXP";
#else
        string? path = FindDevicePath();
#endif
        if (path is null)
            return false;

        SafeFileHandle handle = CreateFileW(
            path,
            GENERIC_WRITE,
            FILE_SHARE_READ | FILE_SHARE_WRITE,
            IntPtr.Zero,
            OPEN_EXISTING,
            0,
            IntPtr.Zero);

        if (handle.IsInvalid)
        {
            handle.Dispose();
            return false;
        }

        sink = new HidFeederJoystickSink(handle);
        sink.Submit(); // push the centered rest state
        return true;
    }

    public void SetButton(int button, bool pressed)
    {
        lock (_gate)
        {
            _report.SetButton(button, pressed);
            Submit();
        }
    }

    public void SetHat(RioHat position)
    {
        lock (_gate)
        {
            _report.SetHat(position);
            Submit();
        }
    }

    public void SetAxis(JoyAxis axis, int value)
    {
        lock (_gate)
        {
            _report.SetAxis(axis, value);
            Submit();
        }
    }

    private void Submit()
    {
        byte[] buffer = _report.ToArray();
        if (!DeviceIoControl(_device, IoctlSubmitReport, buffer, (uint)buffer.Length,
                IntPtr.Zero, 0, out _, IntPtr.Zero))
        {
            // Device may have been removed; surface for the caller's error handling.
            throw new InvalidOperationException(
                $"IOCTL_RIO_SUBMIT_REPORT failed (Win32 {Marshal.GetLastWin32Error()}).");
        }
    }

    public void Dispose() => _device.Dispose();

    // --- SetupAPI device-interface resolution --------------------------------

    private static string? FindDevicePath()
    {
        Guid guid = InterfaceGuid;
        IntPtr devInfo = SetupDiGetClassDevs(ref guid, IntPtr.Zero, IntPtr.Zero,
            DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
        if (devInfo == INVALID_HANDLE_VALUE)
            return null;

        try
        {
            var ifData = new SP_DEVICE_INTERFACE_DATA();
            ifData.cbSize = Marshal.SizeOf(typeof(SP_DEVICE_INTERFACE_DATA));

            if (!SetupDiEnumDeviceInterfaces(devInfo, IntPtr.Zero, ref guid, 0, ref ifData))
                return null;

            uint required = 0;
            SetupDiGetDeviceInterfaceDetail(devInfo, ref ifData, IntPtr.Zero, 0, ref required, IntPtr.Zero);
            if (required == 0)
                return null;

            IntPtr detail = Marshal.AllocHGlobal((int)required);
            try
            {
                // SP_DEVICE_INTERFACE_DETAIL_DATA.cbSize: 8 on x64, 6 on x86.
                Marshal.WriteInt32(detail, IntPtr.Size == 8 ? 8 : 6);
                if (!SetupDiGetDeviceInterfaceDetail(devInfo, ref ifData, detail, required, ref required, IntPtr.Zero))
                    return null;

                // DevicePath follows the cbSize DWORD.
                return Marshal.PtrToStringUni(IntPtr.Add(detail, 4));
            }
            finally
            {
                Marshal.FreeHGlobal(detail);
            }
        }
        finally
        {
            SetupDiDestroyDeviceInfoList(devInfo);
        }
    }

    private const int DIGCF_PRESENT = 0x02;
    private const int DIGCF_DEVICEINTERFACE = 0x10;
    private const uint GENERIC_WRITE = 0x40000000;
    private const uint FILE_SHARE_READ = 0x01;
    private const uint FILE_SHARE_WRITE = 0x02;
    private const uint OPEN_EXISTING = 3;
    private static readonly IntPtr INVALID_HANDLE_VALUE = new(-1);

    [StructLayout(LayoutKind.Sequential)]
    private struct SP_DEVICE_INTERFACE_DATA
    {
        public int cbSize;
        public Guid InterfaceClassGuid;
        public int Flags;
        public IntPtr Reserved;
    }

    [DllImport("setupapi.dll", SetLastError = true)]
    private static extern IntPtr SetupDiGetClassDevs(ref Guid ClassGuid, IntPtr Enumerator, IntPtr hwndParent, int Flags);

    [DllImport("setupapi.dll", SetLastError = true)]
    private static extern bool SetupDiEnumDeviceInterfaces(IntPtr DeviceInfoSet, IntPtr DeviceInfoData,
        ref Guid InterfaceClassGuid, uint MemberIndex, ref SP_DEVICE_INTERFACE_DATA DeviceInterfaceData);

    [DllImport("setupapi.dll", SetLastError = true, CharSet = CharSet.Unicode)]
    private static extern bool SetupDiGetDeviceInterfaceDetail(IntPtr DeviceInfoSet,
        ref SP_DEVICE_INTERFACE_DATA DeviceInterfaceData, IntPtr DeviceInterfaceDetailData,
        uint DeviceInterfaceDetailDataSize, ref uint RequiredSize, IntPtr DeviceInfoData);

    [DllImport("setupapi.dll", SetLastError = true)]
    private static extern bool SetupDiDestroyDeviceInfoList(IntPtr DeviceInfoSet);

    [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
    private static extern SafeFileHandle CreateFileW(string lpFileName, uint dwDesiredAccess, uint dwShareMode,
        IntPtr lpSecurityAttributes, uint dwCreationDisposition, uint dwFlagsAndAttributes, IntPtr hTemplateFile);

    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern bool DeviceIoControl(SafeFileHandle hDevice, uint dwIoControlCode,
        byte[] lpInBuffer, uint nInBufferSize, IntPtr lpOutBuffer, uint nOutBufferSize,
        out uint lpBytesReturned, IntPtr lpOverlapped);
}
