/*++
RioGamepadXP — a WDM HID minidriver for Windows XP that presents a virtual
joystick (6 axes, hat, 96 buttons) and accepts input reports from the RIOJoy
tray app over a sideband control device. The XP-era replacement for the modern
KMDF+VHF RioGamepad driver, exposing the same 25-byte report contract
(see Public.h) so the user-mode feeder is unchanged apart from the open path.

Architecture
------------
There is exactly one virtual device, so state is global (guarded by a spinlock):

  * HID side: we register as a HID minidriver (HidRegisterMinidriver). hidclass
    creates and owns the HID FDO and drives PnP/Power; it calls our
    IRP_MJ_INTERNAL_DEVICE_CONTROL handler to fetch the HID/report descriptors,
    device attributes, and input reports. We run in POLLED mode
    (DevicesArePolled = TRUE): hidclass paces IOCTL_HID_READ_REPORT and we
    complete each synchronously with the current cached report. This avoids
    pending-IRP + cancel-routine machinery entirely (the usual crash surface in
    a virtual HID driver).

  * Sideband: a raw control device (\Device\RioGamepadXP + a \DosDevices symlink)
    that the app opens by name. On IOCTL_RIO_SUBMIT_REPORT we copy the 25 report
    bytes into the cache. hidclass, on its next poll, hands them to the OS.

Dispatch ownership: HidRegisterMinidriver replaces our driver object's
CREATE/CLOSE/DEVICE_CONTROL (and PnP/Power/etc.) entries with hidclass thunks
that assume a HID FDO. Our control device shares the same driver object, so we
save hidclass's pointers and reinstall thin wrappers that route the control
device's IRPs to us and forward everything else to hidclass.
--*/

#include <ntddk.h>
#include <hidport.h>
#include <initguid.h>   // realize DEFINE_GUID storage from Public.h
#include "Public.h"
#include "ReportDescriptor.h"

//
// Global single-instance state.
//
static PDEVICE_OBJECT   g_ControlDevice = NULL;
static KSPIN_LOCK       g_ReportLock;
static UCHAR            g_Report[RIO_REPORT_SIZE];

// hidclass-installed dispatch pointers we wrap.
static PDRIVER_DISPATCH g_HidCreate = NULL;
static PDRIVER_DISPATCH g_HidClose = NULL;
static PDRIVER_DISPATCH g_HidDeviceControl = NULL;

static UNICODE_STRING   g_SymlinkName;

//
// The HID descriptor that advertises our single report descriptor. bcdHID 1.11.
//
#include <pshpack1.h>
typedef struct _RIO_HID_DESCRIPTOR {
    UCHAR   bLength;
    UCHAR   bDescriptorType;
    USHORT  bcdHID;
    UCHAR   bCountry;
    UCHAR   bNumDescriptors;
    UCHAR   bReportType;
    USHORT  wReportLength;
} RIO_HID_DESCRIPTOR;
#include <poppack.h>

static const RIO_HID_DESCRIPTOR g_HidDescriptor =
{
    sizeof(RIO_HID_DESCRIPTOR),
    HID_HID_DESCRIPTOR_TYPE,        // 0x21
    0x0111,                         // HID 1.11
    0x00,                           // not localized
    1,                              // one report descriptor
    HID_REPORT_DESCRIPTOR_TYPE,     // 0x22
    sizeof(g_RioReportDescriptor)
};

//
// Reset the cached report to the neutral rest state: axes centered (16383 =
// 0x3FFF little-endian), hat null (0x0F), all buttons released.
//
static VOID
RioResetReport(VOID)
{
    ULONG i;
    KIRQL irql;

    KeAcquireSpinLock(&g_ReportLock, &irql);
    RtlZeroMemory(g_Report, sizeof(g_Report));
    for (i = 0; i < 6; i++)         // 6 axes, 2 bytes each, centered
    {
        g_Report[i * 2] = 0xFF;
        g_Report[i * 2 + 1] = 0x3F;
    }
    g_Report[12] = 0x0F;            // hat centered/null
    KeReleaseSpinLock(&g_ReportLock, irql);
}

//
// Complete an IRP with a status and information count.
//
static NTSTATUS
RioComplete(PIRP Irp, NTSTATUS Status, ULONG_PTR Information)
{
    Irp->IoStatus.Status = Status;
    Irp->IoStatus.Information = Information;
    IoCompleteRequest(Irp, IO_NO_INCREMENT);
    return Status;
}

//
// IRP_MJ_INTERNAL_DEVICE_CONTROL on the HID FDO: hidclass asks us for the
// descriptors / attributes / input reports. These IOCTLs are METHOD_NEITHER;
// hidclass supplies the output buffer at Irp->UserBuffer with the length in
// Parameters.DeviceIoControl.OutputBufferLength.
//
static NTSTATUS
RioInternalDeviceControl(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
    PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
    NTSTATUS status = STATUS_SUCCESS;
    ULONG_PTR info = 0;
    ULONG outLen = stack->Parameters.DeviceIoControl.OutputBufferLength;
    PVOID out = Irp->UserBuffer;

    UNREFERENCED_PARAMETER(DeviceObject);

    switch (stack->Parameters.DeviceIoControl.IoControlCode)
    {
    case IOCTL_HID_GET_DEVICE_DESCRIPTOR:
        if (outLen < sizeof(g_HidDescriptor)) { status = STATUS_BUFFER_TOO_SMALL; break; }
        RtlCopyMemory(out, &g_HidDescriptor, sizeof(g_HidDescriptor));
        info = sizeof(g_HidDescriptor);
        break;

    case IOCTL_HID_GET_REPORT_DESCRIPTOR:
        if (outLen < sizeof(g_RioReportDescriptor)) { status = STATUS_BUFFER_TOO_SMALL; break; }
        RtlCopyMemory(out, g_RioReportDescriptor, sizeof(g_RioReportDescriptor));
        info = sizeof(g_RioReportDescriptor);
        break;

    case IOCTL_HID_GET_DEVICE_ATTRIBUTES:
    {
        PHID_DEVICE_ATTRIBUTES attr = (PHID_DEVICE_ATTRIBUTES)out;
        if (outLen < sizeof(HID_DEVICE_ATTRIBUTES)) { status = STATUS_BUFFER_TOO_SMALL; break; }
        RtlZeroMemory(attr, sizeof(HID_DEVICE_ATTRIBUTES));
        attr->Size = sizeof(HID_DEVICE_ATTRIBUTES);
        attr->VendorID = RIO_VENDOR_ID;
        attr->ProductID = RIO_PRODUCT_ID;
        attr->VersionNumber = RIO_VERSION;
        info = sizeof(HID_DEVICE_ATTRIBUTES);
        break;
    }

    case IOCTL_HID_READ_REPORT:
    {
        KIRQL irql;
        if (outLen < RIO_REPORT_SIZE) { status = STATUS_BUFFER_TOO_SMALL; break; }
        KeAcquireSpinLock(&g_ReportLock, &irql);
        RtlCopyMemory(out, g_Report, RIO_REPORT_SIZE);
        KeReleaseSpinLock(&g_ReportLock, irql);
        info = RIO_REPORT_SIZE;
        break;
    }

    case IOCTL_HID_WRITE_REPORT:
    case IOCTL_HID_SET_FEATURE:
    case IOCTL_HID_GET_FEATURE:
        // No output reports / feature reports on this device.
        status = STATUS_NOT_SUPPORTED;
        break;

    case IOCTL_HID_GET_STRING:
        // DirectInput takes the friendly name from the registry OEMName value
        // (set at install), not from a HID string, so an empty success is fine.
        status = STATUS_SUCCESS;
        info = 0;
        break;

    default:
        status = STATUS_NOT_SUPPORTED;
        break;
    }

    return RioComplete(Irp, status, info);
}

//
// hidclass calls this (our saved AddDevice) after it has created the HID FDO.
// Nothing to attach — hidclass owns the stack — so just succeed.
//
static NTSTATUS
RioAddDevice(PDRIVER_OBJECT DriverObject, PDEVICE_OBJECT FunctionalDeviceObject)
{
    UNREFERENCED_PARAMETER(DriverObject);
    UNREFERENCED_PARAMETER(FunctionalDeviceObject);
    return STATUS_SUCCESS;
}

//
// Sideband handlers (control device only). CREATE/CLOSE just succeed.
//
static NTSTATUS
RioControlCreateClose(PIRP Irp)
{
    return RioComplete(Irp, STATUS_SUCCESS, 0);
}

//
// IOCTL_RIO_SUBMIT_REPORT (METHOD_BUFFERED): copy the 25 report bytes into the
// cache; hidclass hands them out on its next poll.
//
static NTSTATUS
RioControlDeviceControl(PIRP Irp)
{
    PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
    NTSTATUS status;

    if (stack->Parameters.DeviceIoControl.IoControlCode == IOCTL_RIO_SUBMIT_REPORT)
    {
        if (stack->Parameters.DeviceIoControl.InputBufferLength != RIO_REPORT_SIZE)
        {
            status = STATUS_INVALID_BUFFER_SIZE;
        }
        else
        {
            KIRQL irql;
            KeAcquireSpinLock(&g_ReportLock, &irql);
            RtlCopyMemory(g_Report, Irp->AssociatedIrp.SystemBuffer, RIO_REPORT_SIZE);
            KeReleaseSpinLock(&g_ReportLock, irql);
            status = STATUS_SUCCESS;
        }
    }
    else
    {
        status = STATUS_INVALID_DEVICE_REQUEST;
    }

    return RioComplete(Irp, status, 0);
}

//
// Dispatch wrappers: route the control device's IRPs to us, forward the HID
// FDO's IRPs to the saved hidclass handlers.
//
static NTSTATUS
RioDispatchCreate(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
    if (DeviceObject == g_ControlDevice)
        return RioControlCreateClose(Irp);
    return g_HidCreate(DeviceObject, Irp);
}

static NTSTATUS
RioDispatchClose(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
    if (DeviceObject == g_ControlDevice)
        return RioControlCreateClose(Irp);
    return g_HidClose(DeviceObject, Irp);
}

static NTSTATUS
RioDispatchDeviceControl(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
    if (DeviceObject == g_ControlDevice)
        return RioControlDeviceControl(Irp);
    return g_HidDeviceControl(DeviceObject, Irp);
}

//
// Unload: drop the symlink and control device. hidclass tears down the HID FDO.
//
static VOID
RioUnload(PDRIVER_OBJECT DriverObject)
{
    UNREFERENCED_PARAMETER(DriverObject);

    if (g_SymlinkName.Buffer != NULL)
        IoDeleteSymbolicLink(&g_SymlinkName);
    if (g_ControlDevice != NULL)
        IoDeleteDevice(g_ControlDevice);
}

//
// Create the sideband control device and its DOS-device symbolic link.
//
static NTSTATUS
RioCreateControlDevice(PDRIVER_OBJECT DriverObject)
{
    NTSTATUS status;
    UNICODE_STRING deviceName;

    RtlInitUnicodeString(&deviceName, RIO_XP_DEVICE_NAME);
    RtlInitUnicodeString(&g_SymlinkName, RIO_XP_SYMLINK_NAME);

    status = IoCreateDevice(
        DriverObject,
        0,                       // no device extension; state is global
        &deviceName,
        FILE_DEVICE_UNKNOWN,
        FILE_DEVICE_SECURE_OPEN,
        FALSE,                   // not exclusive
        &g_ControlDevice);
    if (!NT_SUCCESS(status))
        return status;

    g_ControlDevice->Flags |= DO_BUFFERED_IO;   // IOCTL_RIO_SUBMIT_REPORT is METHOD_BUFFERED

    status = IoCreateSymbolicLink(&g_SymlinkName, &deviceName);
    if (!NT_SUCCESS(status))
    {
        IoDeleteDevice(g_ControlDevice);
        g_ControlDevice = NULL;
        g_SymlinkName.Buffer = NULL;
        return status;
    }

    g_ControlDevice->Flags &= ~DO_DEVICE_INITIALIZING;
    return STATUS_SUCCESS;
}

NTSTATUS
DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
    NTSTATUS status;
    HID_MINIDRIVER_REGISTRATION reg;

    KeInitializeSpinLock(&g_ReportLock);
    RioResetReport();

    //
    // Our own dispatch entries. hidclass replaces the HID-related ones during
    // registration; we re-wrap CREATE/CLOSE/DEVICE_CONTROL afterwards.
    //
    DriverObject->MajorFunction[IRP_MJ_INTERNAL_DEVICE_CONTROL] = RioInternalDeviceControl;
    DriverObject->MajorFunction[IRP_MJ_CREATE] = RioDispatchCreate;
    DriverObject->MajorFunction[IRP_MJ_CLOSE] = RioDispatchClose;
    DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = RioDispatchDeviceControl;
    DriverObject->DriverExtension->AddDevice = RioAddDevice;
    DriverObject->DriverUnload = RioUnload;

    RtlZeroMemory(&reg, sizeof(reg));
    reg.Revision = HID_REVISION;
    reg.DriverObject = DriverObject;
    reg.RegistryPath = RegistryPath;
    reg.DeviceExtensionSize = 0;    // per-device state is global (single instance)
    reg.DevicesArePolled = TRUE;    // polled reads → no pending-IRP/cancel machinery

    status = HidRegisterMinidriver(&reg);
    if (!NT_SUCCESS(status))
        return status;

    //
    // hidclass has now overwritten CREATE/CLOSE/DEVICE_CONTROL (and PnP/Power/
    // etc.) with its own thunks. Save the ones we need to forward, then reinstall
    // our wrappers so the control device's IRPs come back to us.
    //
    g_HidCreate = DriverObject->MajorFunction[IRP_MJ_CREATE];
    g_HidClose = DriverObject->MajorFunction[IRP_MJ_CLOSE];
    g_HidDeviceControl = DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL];

    DriverObject->MajorFunction[IRP_MJ_CREATE] = RioDispatchCreate;
    DriverObject->MajorFunction[IRP_MJ_CLOSE] = RioDispatchClose;
    DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = RioDispatchDeviceControl;

    //
    // hidclass also replaces DriverUnload; chain our teardown ahead of nothing
    // (hidclass's unload runs via its own bookkeeping). Reassert ours.
    //
    DriverObject->DriverUnload = RioUnload;

    status = RioCreateControlDevice(DriverObject);
    if (!NT_SUCCESS(status))
    {
        // The HID device can still work without the sideband, but the app can't
        // feed it, so fail registration cleanly.
        return status;
    }

    return STATUS_SUCCESS;
}
