#include "usbd_dinput.h"

#include "usbd_ctlreq.h"
#include <string.h>

#define HID_DESCRIPTOR_TYPE  0x21U
#define HID_REPORT_DESC      0x22U
#define HID_REQ_GET_REPORT   0x01U
#define HID_REQ_GET_IDLE     0x02U
#define HID_REQ_GET_PROTOCOL 0x03U
#define HID_REQ_SET_REPORT   0x09U
#define HID_REQ_SET_IDLE     0x0AU
#define HID_REQ_SET_PROTOCOL 0x0BU

typedef struct
{
  uint8_t altSetting[3];
  uint8_t protocol;
  uint8_t idle;
  uint8_t busy;
} USBD_DINPUT_HandleTypeDef;

static uint8_t DINPUT_Init(USBD_HandleTypeDef *pdev, uint8_t cfgidx);
static uint8_t DINPUT_DeInit(USBD_HandleTypeDef *pdev, uint8_t cfgidx);
static uint8_t DINPUT_Setup(USBD_HandleTypeDef *pdev,
                            USBD_SetupReqTypedef *req);
static uint8_t DINPUT_DataIn(USBD_HandleTypeDef *pdev, uint8_t epnum);
static uint8_t DINPUT_DataOut(USBD_HandleTypeDef *pdev, uint8_t epnum);
static uint8_t *DINPUT_GetCfgDesc(uint16_t *length);
static uint8_t *DINPUT_GetDeviceQualifierDesc(uint16_t *length);
#if (USBD_SUPPORT_USER_STRING_DESC == 1U)
static uint8_t *DINPUT_GetUserString(USBD_HandleTypeDef *pdev,
                                     uint8_t index,
                                     uint16_t *length);
#endif

static uint8_t dinputInBuffer[DINPUT_REPORT_SIZE];
static uint8_t dinputSetupOutBuffer[DINPUT_EP_SIZE];
#if (USBD_SUPPORT_USER_STRING_DESC == 1U)
static uint8_t dinputStringBuffer[32];
#endif

USBD_ClassTypeDef USBD_DINPUT =
{
  DINPUT_Init,
  DINPUT_DeInit,
  DINPUT_Setup,
  NULL,
  NULL,
  DINPUT_DataIn,
  DINPUT_DataOut,
  NULL,
  NULL,
  NULL,
  DINPUT_GetCfgDesc,
  DINPUT_GetCfgDesc,
  DINPUT_GetCfgDesc,
  DINPUT_GetDeviceQualifierDesc,
#if (USBD_SUPPORT_USER_STRING_DESC == 1U)
  DINPUT_GetUserString,
#endif
};

/* Descriptor order and report sizes match the original three-interface unit.
 * IF0 is the only functional interface. IF1/IF2 are inert compatibility
 * shells and their data endpoints are halted in DINPUT_Init(). */
__ALIGN_BEGIN static uint8_t dinputConfigDescriptor[USB_DINPUT_CONFIG_DESC_SIZ] __ALIGN_END =
{
  0x09, USB_DESC_TYPE_CONFIGURATION,
  LOBYTE(USB_DINPUT_CONFIG_DESC_SIZ), HIBYTE(USB_DINPUT_CONFIG_DESC_SIZ),
  0x03, 0x01, 0x00, 0x80, 0x32,

  0x09, USB_DESC_TYPE_INTERFACE,
  0x00, 0x00, 0x01,
  0x03, 0x00, 0x00, 0x05,
  0x09, HID_DESCRIPTOR_TYPE,
  0x11, 0x01, 0x00, 0x01, HID_REPORT_DESC,
  0x51, 0x00,
  0x07, USB_DESC_TYPE_ENDPOINT,
  DINPUT_GAMEPAD_EPIN_ADDR, 0x03,
  LOBYTE(DINPUT_EP_SIZE), HIBYTE(DINPUT_EP_SIZE), 0x01,

  0x09, USB_DESC_TYPE_INTERFACE,
  0x01, 0x00, 0x02,
  0x03, 0x00, 0x00, 0x06,
  0x09, HID_DESCRIPTOR_TYPE,
  0x11, 0x01, 0x00, 0x01, HID_REPORT_DESC,
  0x22, 0x00,
  0x07, USB_DESC_TYPE_ENDPOINT,
  DINPUT_SETUP_EPIN_ADDR, 0x03,
  LOBYTE(DINPUT_EP_SIZE), HIBYTE(DINPUT_EP_SIZE), 0x01,
  0x07, USB_DESC_TYPE_ENDPOINT,
  DINPUT_SETUP_EPOUT_ADDR, 0x03,
  LOBYTE(DINPUT_EP_SIZE), HIBYTE(DINPUT_EP_SIZE), 0x01,

  0x09, USB_DESC_TYPE_INTERFACE,
  0x02, 0x00, 0x01,
  0x03, 0x00, 0x00, 0x07,
  0x09, HID_DESCRIPTOR_TYPE,
  0x11, 0x01, 0x00, 0x01, HID_REPORT_DESC,
  0x15, 0x00,
  0x07, USB_DESC_TYPE_ENDPOINT,
  DINPUT_RAW_EPIN_ADDR, 0x03,
  LOBYTE(DINPUT_EP_SIZE), HIBYTE(DINPUT_EP_SIZE), 0x01,
};

/* 11 buttons, hat and six unsigned 8-bit axes: 9 bytes total. */
__ALIGN_BEGIN static uint8_t gamepadReportDescriptor[] __ALIGN_END =
{
  0x05, 0x01, 0x09, 0x05, 0xA1, 0x01,
  0x05, 0x09, 0x19, 0x01, 0x29, 0x0B,
  0x15, 0x00, 0x25, 0x01, 0x75, 0x01,
  0x95, 0x0B, 0x81, 0x02,
  0x75, 0x01, 0x95, 0x05, 0x81, 0x03,
  0x05, 0x01, 0x09, 0x39, 0x15, 0x00,
  0x25, 0x07, 0x35, 0x00, 0x46, 0x3B, 0x01,
  0x65, 0x14, 0x75, 0x04, 0x95, 0x01,
  0x81, 0x42, 0x75, 0x04, 0x95, 0x01, 0x81, 0x03,
  0x05, 0x01,
  0x09, 0x30, 0x09, 0x31, 0x09, 0x32,
  0x09, 0x33, 0x09, 0x34, 0x09, 0x35,
  0x15, 0x00, 0x26, 0xFF, 0x00,
  0x75, 0x08, 0x95, 0x06, 0x81, 0x02,
  0xC0
};

__ALIGN_BEGIN static uint8_t setupReportDescriptor[] __ALIGN_END =
{
  0x06, 0x00, 0xFF, 0x09, 0x01, 0xA1, 0x01,
  0x09, 0x10, 0x15, 0x00, 0x26, 0xFF, 0x00,
  0x75, 0x08, 0x95, 0x1A, 0x81, 0x02,
  0x09, 0x11, 0x15, 0x00, 0x26, 0xFF, 0x00,
  0x75, 0x08, 0x95, 0x1B, 0x91, 0x02,
  0xC0
};

__ALIGN_BEGIN static uint8_t rawReportDescriptor[] __ALIGN_END =
{
  0x06, 0x00, 0xFF, 0x09, 0x02, 0xA1, 0x01,
  0x09, 0x20, 0x15, 0x00, 0x26, 0xFF, 0x00,
  0x75, 0x08, 0x95, 0x18, 0x81, 0x02,
  0xC0
};

__ALIGN_BEGIN static uint8_t dinputDeviceQualifierDescriptor[USB_LEN_DEV_QUALIFIER_DESC] __ALIGN_END =
{
  USB_LEN_DEV_QUALIFIER_DESC, USB_DESC_TYPE_DEVICE_QUALIFIER,
  0x00, 0x02, 0x00, 0x00, 0x00, USB_MAX_EP0_SIZE, 0x01, 0x00
};

_Static_assert(sizeof(gamepadReportDescriptor) == 81U,
               "DInput gamepad report descriptor length changed");
_Static_assert(sizeof(setupReportDescriptor) == 34U,
               "Setup report descriptor length changed");
_Static_assert(sizeof(rawReportDescriptor) == 21U,
               "Raw report descriptor length changed");

static uint8_t DINPUT_Init(USBD_HandleTypeDef *pdev, uint8_t cfgidx)
{
  USBD_DINPUT_HandleTypeDef *handle;
  UNUSED(cfgidx);

  pdev->pClassData = USBD_malloc(sizeof(USBD_DINPUT_HandleTypeDef));
  if (pdev->pClassData == NULL)
  {
    return USBD_FAIL;
  }
  handle = (USBD_DINPUT_HandleTypeDef *)pdev->pClassData;
  memset(handle, 0, sizeof(*handle));

  USBD_LL_OpenEP(pdev, DINPUT_GAMEPAD_EPIN_ADDR,
                 USBD_EP_TYPE_INTR, DINPUT_EP_SIZE);
  pdev->ep_in[DINPUT_GAMEPAD_EPIN_ADDR & 0x0FU].is_used = 1U;

  USBD_LL_OpenEP(pdev, DINPUT_SETUP_EPIN_ADDR,
                 USBD_EP_TYPE_INTR, DINPUT_EP_SIZE);
  pdev->ep_in[DINPUT_SETUP_EPIN_ADDR & 0x0FU].is_used = 1U;
  USBD_LL_OpenEP(pdev, DINPUT_SETUP_EPOUT_ADDR,
                 USBD_EP_TYPE_INTR, DINPUT_EP_SIZE);
  pdev->ep_out[DINPUT_SETUP_EPOUT_ADDR & 0x0FU].is_used = 1U;
  USBD_LL_OpenEP(pdev, DINPUT_RAW_EPIN_ADDR,
                 USBD_EP_TYPE_INTR, DINPUT_EP_SIZE);
  pdev->ep_in[DINPUT_RAW_EPIN_ADDR & 0x0FU].is_used = 1U;

  /* Stub HID data paths: unused IN endpoints stay at NAK. Setup OUT packets
   * are acknowledged and discarded so the interface can start cleanly. */
  USBD_LL_PrepareReceive(pdev, DINPUT_SETUP_EPOUT_ADDR,
                         dinputSetupOutBuffer, DINPUT_EP_SIZE);
  return USBD_OK;
}

static uint8_t DINPUT_DeInit(USBD_HandleTypeDef *pdev, uint8_t cfgidx)
{
  UNUSED(cfgidx);
  USBD_LL_CloseEP(pdev, DINPUT_GAMEPAD_EPIN_ADDR);
  USBD_LL_CloseEP(pdev, DINPUT_SETUP_EPIN_ADDR);
  USBD_LL_CloseEP(pdev, DINPUT_SETUP_EPOUT_ADDR);
  USBD_LL_CloseEP(pdev, DINPUT_RAW_EPIN_ADDR);
  pdev->ep_in[DINPUT_GAMEPAD_EPIN_ADDR & 0x0FU].is_used = 0U;
  pdev->ep_in[DINPUT_SETUP_EPIN_ADDR & 0x0FU].is_used = 0U;
  pdev->ep_out[DINPUT_SETUP_EPOUT_ADDR & 0x0FU].is_used = 0U;
  pdev->ep_in[DINPUT_RAW_EPIN_ADDR & 0x0FU].is_used = 0U;
  if (pdev->pClassData != NULL)
  {
    USBD_free(pdev->pClassData);
    pdev->pClassData = NULL;
  }
  return USBD_OK;
}

static uint8_t DINPUT_Setup(USBD_HandleTypeDef *pdev,
                            USBD_SetupReqTypedef *req)
{
  USBD_DINPUT_HandleTypeDef *handle =
      (USBD_DINPUT_HandleTypeDef *)pdev->pClassData;
  uint8_t interface = (uint8_t)req->wIndex;
  uint16_t status = 0U;
  uint8_t *descriptor = NULL;
  uint16_t length = 0U;

  if ((handle == NULL) || (interface > 2U))
  {
    USBD_CtlError(pdev, req);
    return USBD_FAIL;
  }

  if ((req->bmRequest & USB_REQ_TYPE_MASK) == USB_REQ_TYPE_CLASS)
  {
    /* The original Setup/Raw protocols are intentionally not implemented. */
    if (interface != 0U)
    {
      USBD_CtlError(pdev, req);
      return USBD_FAIL;
    }

    switch (req->bRequest)
    {
      case HID_REQ_SET_PROTOCOL:
        handle->protocol = (uint8_t)req->wValue;
        return USBD_OK;
      case HID_REQ_GET_PROTOCOL:
        USBD_CtlSendData(pdev, &handle->protocol, 1U);
        return USBD_OK;
      case HID_REQ_SET_IDLE:
        handle->idle = (uint8_t)(req->wValue >> 8);
        return USBD_OK;
      case HID_REQ_GET_IDLE:
        USBD_CtlSendData(pdev, &handle->idle, 1U);
        return USBD_OK;
      case HID_REQ_GET_REPORT:
      case HID_REQ_SET_REPORT:
      default:
        USBD_CtlError(pdev, req);
        return USBD_FAIL;
    }
  }

  if ((req->bmRequest & USB_REQ_TYPE_MASK) == USB_REQ_TYPE_STANDARD)
  {
    switch (req->bRequest)
    {
      case USB_REQ_GET_STATUS:
        USBD_CtlSendData(pdev, (uint8_t *)&status, sizeof(status));
        return USBD_OK;
      case USB_REQ_GET_INTERFACE:
        USBD_CtlSendData(pdev, &handle->altSetting[interface], 1U);
        return USBD_OK;
      case USB_REQ_SET_INTERFACE:
        if (req->wValue == 0U)
        {
          handle->altSetting[interface] = 0U;
          return USBD_OK;
        }
        break;
      case USB_REQ_GET_DESCRIPTOR:
        if ((req->wValue >> 8) == HID_REPORT_DESC)
        {
          if (interface == 0U)
          {
            descriptor = gamepadReportDescriptor;
            length = sizeof(gamepadReportDescriptor);
          }
          else if (interface == 1U)
          {
            descriptor = setupReportDescriptor;
            length = sizeof(setupReportDescriptor);
          }
          else
          {
            descriptor = rawReportDescriptor;
            length = sizeof(rawReportDescriptor);
          }
        }
        else if ((req->wValue >> 8) == HID_DESCRIPTOR_TYPE)
        {
          static const uint8_t hidOffsets[3] = {18U, 43U, 75U};
          descriptor = &dinputConfigDescriptor[hidOffsets[interface]];
          length = 9U;
        }
        if (descriptor != NULL)
        {
          USBD_CtlSendData(pdev, descriptor, MIN(length, req->wLength));
          return USBD_OK;
        }
        break;
      default:
        break;
    }
  }

  USBD_CtlError(pdev, req);
  return USBD_FAIL;
}

uint8_t USBD_DINPUT_SendReport(USBD_HandleTypeDef *pdev,
                               uint8_t *report,
                               uint16_t len)
{
  USBD_DINPUT_HandleTypeDef *handle =
      (USBD_DINPUT_HandleTypeDef *)pdev->pClassData;
  if ((pdev->dev_state == USBD_STATE_CONFIGURED) &&
      (handle != NULL) && (handle->busy == 0U) &&
      (len == DINPUT_REPORT_SIZE))
  {
    handle->busy = 1U;
    memcpy(dinputInBuffer, report, DINPUT_REPORT_SIZE);
    return USBD_LL_Transmit(pdev, DINPUT_GAMEPAD_EPIN_ADDR,
                            dinputInBuffer, len);
  }
  return USBD_OK;
}

static uint8_t DINPUT_DataIn(USBD_HandleTypeDef *pdev, uint8_t epnum)
{
  if ((epnum == (DINPUT_GAMEPAD_EPIN_ADDR & 0x0FU)) &&
      (pdev->pClassData != NULL))
  {
    ((USBD_DINPUT_HandleTypeDef *)pdev->pClassData)->busy = 0U;
    return USBD_OK;
  }
  return USBD_FAIL;
}

static uint8_t DINPUT_DataOut(USBD_HandleTypeDef *pdev, uint8_t epnum)
{
  if (epnum == (DINPUT_SETUP_EPOUT_ADDR & 0x0FU))
  {
    USBD_LL_PrepareReceive(pdev, DINPUT_SETUP_EPOUT_ADDR,
                           dinputSetupOutBuffer, DINPUT_EP_SIZE);
    return USBD_OK;
  }
  return USBD_FAIL;
}

static uint8_t *DINPUT_GetCfgDesc(uint16_t *length)
{
  *length = sizeof(dinputConfigDescriptor);
  return dinputConfigDescriptor;
}

static uint8_t *DINPUT_GetDeviceQualifierDesc(uint16_t *length)
{
  *length = sizeof(dinputDeviceQualifierDescriptor);
  return dinputDeviceQualifierDescriptor;
}

#if (USBD_SUPPORT_USER_STRING_DESC == 1U)
static uint8_t *DINPUT_GetUserString(USBD_HandleTypeDef *pdev,
                                     uint8_t index,
                                     uint16_t *length)
{
  const char *text;
  UNUSED(pdev);
  if (index == 0x06U)
  {
    text = "Setup";
  }
  else if (index == 0x07U)
  {
    text = "Raw";
  }
  else
  {
    *length = 0U;
    return NULL;
  }
  USBD_GetString((uint8_t *)text, dinputStringBuffer, length);
  return dinputStringBuffer;
}
#endif
