/*
 * SPDX-FileCopyrightText: Copyright (c) 2024 Matthew Tran
 * SPDX-FileCopyrightText: Copyright (c) 2024 Ha Thach
 * SPDX-License-Identifier: MIT
 *
 * This file is part of the TinyUSB stack.
 */

#include "tusb_option.h"

#if CFG_TUD_ENABLED && defined(TUP_USBIP_WCH_USBFS) && CFG_TUD_WCH_USBIP_USBFS

  #include "device/dcd.h"
  #include "ch32_usbfs_reg.h"

  /* private defines */
  #define EP_MAX         (8)

  // EP3 IN buffer size. CH32V20x/V30x/F20x USBFS support full-speed iso packets up to 1023 B on
  // endpoint 3 (every other endpoint is 64 B); those parts set CH32_USBFS_EP3_1023_BUFSIZE in
  // ch32_usbfs_reg.h. V103/X035/CH58x cap every endpoint at 64 B. Overridable per project.
  #ifndef CFG_TUD_WCH_USBFS_EP3_BUFSIZE
    #ifdef CH32_USBFS_EP3_1023_BUFSIZE
      #define CFG_TUD_WCH_USBFS_EP3_BUFSIZE 1023
    #else
      #define CFG_TUD_WCH_USBFS_EP3_BUFSIZE 64
    #endif
  #endif

  // Struct-based EP register access (uniform layout). CH58X has a different register map and
  // defines EP_DMA/EP_TX_LEN/EP_CTRL itself in ch32_usbfs_reg.h.
  #if CFG_TUSB_MCU == OPT_MCU_CH583
    // CH58X EP registers split into a low block (EP0-4) and a high block (EP5-7). Walk from each
    // block's first slot by the 4-byte slot stride (pointer arithmetic off slot 0, so the unused
    // ternary branch's index can't trip -Warray-bounds). EP4 has no DMA register of its own (it
    // shares EP0's, slot 0) and is never written (see ep_shares_ep0_dma()).
    #define EP_TX_LEN(ep) (*((ep) <= 4u ? &USBOTG_FS->EP_CTRL_0_4[0].T_LEN + (ep) * 4u \
                                        : &USBOTG_FS->EP_CTRL_5_7[0].T_LEN + ((ep) - 5u) * 4u))
    #define EP_CTRL(ep)   (*((ep) <= 4u ? &USBOTG_FS->EP_CTRL_0_4[0].CTRL  + (ep) * 4u \
                                        : &USBOTG_FS->EP_CTRL_5_7[0].CTRL  + ((ep) - 5u) * 4u))
    #define EP_DMA(ep)    (*((ep) <= 3u ? &USBOTG_FS->EP_DMA_0_3[0].DMA + (ep) * 2u \
                             : (ep) == 4u ? &USBOTG_FS->EP_DMA_0_3[0].DMA \
                                          : &USBOTG_FS->EP_DMA_5_7[0].DMA + ((ep) - 5u) * 2u))
  #else
    #define EP_DMA(ep)     ((&USBOTG_FS->UEP0_DMA)[ep])
    #define EP_TX_LEN(ep)  ((&USBOTG_FS->UEP0_TX_LEN)[2 * ep])
    #define EP_TX_CTRL(ep) ((&USBOTG_FS->UEP0_TX_CTRL)[4 * ep])
    #define EP_RX_CTRL(ep) ((&USBOTG_FS->UEP0_RX_CTRL)[4 * ep])
  #endif

// Endpoint control register access. The newer USBFS IP (CH32V20x/V307/X035) has separate
// TX_CTRL and RX_CTRL bytes per endpoint; the older IP (CH32V103) has a single combined
// UEPn_CTRL register. These helpers hide the difference so the rest of the driver is shared.
// Values use the newer-IP encoding (USBFS_EP_T_*/USBFS_EP_R_*); the combined path remaps them.
#ifdef CH32_USBFS_EP_CTRL_COMBINED
  #ifndef EP_CTRL // parts with a custom register map (CH58X) define EP_CTRL directly in reg.h
  #define EP_CTRL(ep) EP_TX_CTRL(ep) // UEPn_TX_CTRL field aliases the combined UEPn_CTRL register
  #endif

  static inline uint8_t ep_tx_to_comb(uint8_t v) {
    uint8_t c = v & USBFS_EP_T_RES_MASK; // IN response: bits [1:0] in both encodings
    if (v & USBFS_EP_T_TOG)      { c |= USBFS_EPC_T_TOG; }
    if (v & USBFS_EP_T_AUTO_TOG) { c |= USBFS_EPC_AUTO_TOG; }
    return c;
  }
  static inline uint8_t ep_rx_to_comb(uint8_t v) {
    uint8_t c = (uint8_t) ((v & USBFS_EP_R_RES_MASK) << USBFS_EPC_R_RES_SHIFT); // OUT response -> bits [3:2]
    if (v & USBFS_EP_R_TOG)      { c |= USBFS_EPC_R_TOG; }
    if (v & USBFS_EP_R_AUTO_TOG) { c |= USBFS_EPC_AUTO_TOG; }
    return c;
  }
  // Set IN side (response/toggle/auto-tog), preserving the OUT response + OUT toggle.
  static inline void ep_tx_ctrl_set(uint8_t ep, uint8_t v) {
    EP_CTRL(ep) = (uint8_t) ((EP_CTRL(ep) & (USBFS_EPC_R_RES_MASK | USBFS_EPC_R_TOG)) | ep_tx_to_comb(v));
  }
  // Set OUT side, preserving the IN response + IN toggle.
  static inline void ep_rx_ctrl_set(uint8_t ep, uint8_t v) {
    EP_CTRL(ep) = (uint8_t) ((EP_CTRL(ep) & (USBFS_EPC_T_RES_MASK | USBFS_EPC_T_TOG)) | ep_rx_to_comb(v));
  }
  static inline void ep_tx_set_response(uint8_t ep, uint8_t res) {
    EP_CTRL(ep) = (uint8_t) ((EP_CTRL(ep) & ~USBFS_EPC_T_RES_MASK) | (res & USBFS_EP_T_RES_MASK));
  }
  static inline void ep_rx_set_response(uint8_t ep, uint8_t res) {
    EP_CTRL(ep) = (uint8_t) ((EP_CTRL(ep) & ~USBFS_EPC_R_RES_MASK) | ((res & USBFS_EP_R_RES_MASK) << USBFS_EPC_R_RES_SHIFT));
  }
  #define EP0_SETUP_RX_TOG USBFS_EP_R_TOG // combined IP: data/status stage after SETUP is DATA1
#else
  static inline void ep_tx_ctrl_set(uint8_t ep, uint8_t v) { EP_TX_CTRL(ep) = v; }
  static inline void ep_rx_ctrl_set(uint8_t ep, uint8_t v) { EP_RX_CTRL(ep) = v; }
  static inline void ep_tx_set_response(uint8_t ep, uint8_t res) {
    EP_TX_CTRL(ep) = (uint8_t) ((EP_TX_CTRL(ep) & ~USBFS_EP_T_RES_MASK) | res);
  }
  static inline void ep_rx_set_response(uint8_t ep, uint8_t res) {
    EP_RX_CTRL(ep) = (uint8_t) ((EP_RX_CTRL(ep) & ~USBFS_EP_R_RES_MASK) | res);
  }
  #define EP0_SETUP_RX_TOG 0
#endif

// Hardware auto data-toggle flag. Parts whose AUTO_TOG is reliable OR it into the EP setup so the
// controller flips DATA0/DATA1 itself; CH58x (CH32_USBFS_EP_MANUAL_TOG) leaves it clear and the
// ISR flips the toggle bit after each packet instead.
#ifdef CH32_USBFS_EP_MANUAL_TOG
  #define EP_T_AUTO_TOG 0
  #define EP_R_AUTO_TOG 0
#else
  #define EP_T_AUTO_TOG USBFS_EP_T_AUTO_TOG
  #define EP_R_AUTO_TOG USBFS_EP_R_AUTO_TOG
#endif

/* private data */
struct usb_xfer {
  bool     valid;
  uint8_t *buffer;
  size_t   len;
  size_t   processed_len;
  size_t   max_size;
};

static struct {
  bool            ep0_tog;
  bool            isochronous[EP_MAX][2]; // per [ep][dir]: an ep number may be iso in one direction
  struct usb_xfer xfer[EP_MAX][2];
#ifdef CH32_USBFS_EP4_SHARES_EP0
  // CH58X buffers laid out by hand so EP0/EP4 don't burn two unused buffer[] slots. EP0 and EP4
  // share one contiguous 192-byte DMA region (EP4 has no DMA register of its own):
  // EP0 [0:63] (half-duplex OUT+IN) + EP4 OUT [64:127] + EP4 IN [128:191]. Every other endpoint
  // (incl. EP3, which is bulk-only here — CH58X has no isochronous support) gets a plain 128-byte
  // OUT+IN buffer, so no oversized EP3 buffer is needed.
  TU_ATTR_ALIGNED(4) uint8_t ep0_ep4_buffer[3 * 64];
  TU_ATTR_ALIGNED(4) uint8_t ep1_buffer[2][64];
  TU_ATTR_ALIGNED(4) uint8_t ep2_buffer[2][64];
  TU_ATTR_ALIGNED(4) uint8_t ep3_buffer[2][64];
  TU_ATTR_ALIGNED(4) uint8_t ep5_buffer[2][64];
  TU_ATTR_ALIGNED(4) uint8_t ep6_buffer[2][64];
  TU_ATTR_ALIGNED(4) uint8_t ep7_buffer[2][64];
#else
  // Every endpoint gets a 64-byte OUT + 64-byte IN buffer.
  TU_ATTR_ALIGNED(4) uint8_t buffer[EP_MAX][2][64];
  #if CFG_TUD_WCH_USBFS_EP3_BUFSIZE > 64
  // ...except EP3, which supports full-speed iso packets up to 1023 B on CH32V20x/V30x/F20x, so its
  // IN buffer is enlarged (OUT stays 64 B; an OUT transfer >64 B on EP3 would overwrite queued IN).
  TU_ATTR_ALIGNED(4) struct {
    uint8_t out[64];
    uint8_t in[CFG_TUD_WCH_USBFS_EP3_BUFSIZE];
    uint8_t pad;
  } ep3_buffer;
  #endif
#endif
} data;

// DMA / copy buffer pointers per endpoint. The WCH USBFS buffer holds OUT (RX) at offset 0 and
// IN (TX) at +64; EP0 is half-duplex and reuses its OUT chunk for IN. On CH58X, EP0/EP4 share
// ep0_ep4_buffer and the regular endpoints use their own named buffer (see the struct above).
#ifdef CH32_USBFS_EP4_SHARES_EP0
// OUT base of the regular CH58X endpoints (EP1/2/3/5/6/7; EP0/EP4 share ep0_ep4_buffer).
static inline uint8_t* ch58x_ep_buffer(uint8_t ep) {
  switch (ep) {
    case 1:  return data.ep1_buffer[0];
    case 2:  return data.ep2_buffer[0];
    case 3:  return data.ep3_buffer[0];
    case 5:  return data.ep5_buffer[0];
    case 6:  return data.ep6_buffer[0];
    default: return data.ep7_buffer[0]; // ep == 7
  }
}
#endif

static inline uint32_t ep_dma_addr(uint8_t ep) {
#ifdef CH32_USBFS_EP4_SHARES_EP0
  if (ep == 0 || ep == 4) { return (uint32_t) &data.ep0_ep4_buffer[0]; } // EP4 shares EP0's DMA
  return (uint32_t) ch58x_ep_buffer(ep);
#else
  #if CFG_TUD_WCH_USBFS_EP3_BUFSIZE > 64
  if (ep == 3) { return (uint32_t) &data.ep3_buffer.out[0]; } // EP3 has an enlarged IN buffer
  #endif
  return (uint32_t) &data.buffer[ep][0];
#endif
}

static inline uint8_t* ep_out_buf(uint8_t ep) {
#ifdef CH32_USBFS_EP4_SHARES_EP0
  if (ep == 0) { return &data.ep0_ep4_buffer[0]; }
  if (ep == 4) { return &data.ep0_ep4_buffer[64]; }
  return ch58x_ep_buffer(ep);
#else
  #if CFG_TUD_WCH_USBFS_EP3_BUFSIZE > 64
  if (ep == 3) { return data.ep3_buffer.out; }
  #endif
  return data.buffer[ep][TUSB_DIR_OUT];
#endif
}

static inline uint8_t* ep_in_buf(uint8_t ep) {
#ifdef CH32_USBFS_EP4_SHARES_EP0
  if (ep == 0) { return &data.ep0_ep4_buffer[0]; } // EP0 half-duplex: IN reuses OUT chunk
  if (ep == 4) { return &data.ep0_ep4_buffer[128]; }
  return ch58x_ep_buffer(ep) + 64; // IN at +64 within the endpoint's 128-byte buffer
#else
  if (ep == 0) { return data.buffer[0][TUSB_DIR_OUT]; } // EP0 half-duplex: IN reuses OUT chunk
  #if CFG_TUD_WCH_USBFS_EP3_BUFSIZE > 64
  if (ep == 3) { return data.ep3_buffer.in; } // enlarged IN buffer for full-speed iso
  #endif
  return data.buffer[ep][TUSB_DIR_IN];
#endif
}

// EP4 on CH58X has no DMA register (shares EP0's); skip its EP_DMA() write.
static inline bool ep_shares_ep0_dma(uint8_t ep) {
#ifdef CH32_USBFS_EP4_SHARES_EP0
  return ep == 4;
#else
  (void) ep;
  return false;
#endif
}

/* private helpers */
static void update_in(uint8_t rhport, uint8_t ep, bool force) {
  struct usb_xfer *xfer = &data.xfer[ep][TUSB_DIR_IN];
  if (xfer->valid) {
    if (force || xfer->len) {
      size_t len = TU_MIN(xfer->max_size, xfer->len);
#if CFG_TUSB_MCU == OPT_MCU_CH583
      // Every CH58x endpoint buffer is 64 bytes; cap the copy so an iso mps a class mistakenly set
      // larger can't write past the buffer into a neighbouring endpoint's.
      len = TU_MIN(len, 64u);
#endif
      memcpy(ep_in_buf(ep), xfer->buffer, len);
      xfer->buffer += len;
      xfer->len -= len;
      xfer->processed_len += len;

      EP_TX_LEN(ep) = len;
      if (ep == 0) {
        ep_tx_ctrl_set(0, USBFS_EP_T_RES_ACK | (data.ep0_tog ? USBFS_EP_T_TOG : 0));
        data.ep0_tog  = !data.ep0_tog;
      } else if (data.isochronous[ep][TUSB_DIR_IN]) {
        ep_tx_set_response(ep, USBFS_EP_T_RES_NYET);
      } else {
        ep_tx_set_response(ep, USBFS_EP_T_RES_ACK);
      }
    } else {
      xfer->valid = false;
      if (ep == 0) {
        ep_tx_ctrl_set(0, USBFS_EP_T_RES_NAK | (data.ep0_tog ? USBFS_EP_T_TOG : 0));
      } else if (!data.isochronous[ep][TUSB_DIR_IN]) {
        ep_tx_set_response(ep, USBFS_EP_T_RES_NAK);
      }
      dcd_event_xfer_complete(rhport, ep | TUSB_DIR_IN_MASK, xfer->processed_len, XFER_RESULT_SUCCESS, true);
    }
  }
}

static void update_out(uint8_t rhport, uint8_t ep, size_t rx_len) {
  struct usb_xfer *xfer = &data.xfer[ep][TUSB_DIR_OUT];
  if (xfer->valid) {
    size_t len = TU_MIN(xfer->max_size, TU_MIN(xfer->len, rx_len));
#if CFG_TUSB_MCU == OPT_MCU_CH583
    len = TU_MIN(len, 64u); // cap to the 64-byte EP buffer (see update_in)
#endif
    memcpy(xfer->buffer, ep_out_buf(ep), len);
    xfer->buffer += len;
    xfer->len -= len;
    xfer->processed_len += len;

    if (xfer->len == 0 || len < xfer->max_size) {
      xfer->valid = false;
      dcd_event_xfer_complete(rhport, ep, xfer->processed_len, XFER_RESULT_SUCCESS, true);
    }

    if (ep == 0) {
      ep_rx_set_response(0, USBFS_EP_R_RES_NAK);
    } else {
      uint8_t rx_res =
        data.isochronous[ep][TUSB_DIR_OUT] ? USBFS_EP_R_RES_NYET : (xfer->valid ? USBFS_EP_R_RES_ACK : USBFS_EP_R_RES_NAK);
      ep_rx_set_response(ep, rx_res);
    }
  }
}

static void reset_ep_ctrls(void) {
  for (uint8_t ep = 1; ep < EP_MAX; ep++) {
    if (!ep_shares_ep0_dma(ep)) { EP_DMA(ep) = ep_dma_addr(ep); }
    EP_TX_LEN(ep)  = 0;
    ep_tx_ctrl_set(ep, EP_T_AUTO_TOG | USBFS_EP_T_RES_NYET);
    ep_rx_ctrl_set(ep, EP_R_AUTO_TOG | USBFS_EP_R_RES_NYET);
  }
}

/* public functions */
bool dcd_init(uint8_t rhport, const tusb_rhport_init_t *rh_init) {
  (void)rh_init;
  // init registers
  USBOTG_FS->BASE_CTRL = USBFS_CTRL_SYS_CTRL | USBFS_CTRL_INT_BUSY | USBFS_CTRL_DMA_EN;
  USBOTG_FS->UDEV_CTRL = USBFS_UDEV_CTRL_PD_DIS | USBFS_UDEV_CTRL_PORT_EN;
  USBOTG_FS->DEV_ADDR  = 0x00;

  USBOTG_FS->INT_FG = 0xFF;
  USBOTG_FS->INT_EN = USBFS_INT_EN_BUS_RST | USBFS_INT_EN_TRANSFER | USBFS_INT_EN_SUSPEND;

  // setup endpoint 0 (also backs EP4's buffer on CH58X via the shared DMA region)
  EP_DMA(0)     = ep_dma_addr(0);
  EP_TX_LEN(0)  = 0;
  ep_tx_ctrl_set(0, USBFS_EP_T_RES_NAK);
  ep_rx_ctrl_set(0, USBFS_EP_R_RES_ACK);

  // enable other endpoints but NAK everything
  USBOTG_FS->UEP4_1_MOD = 0xCC;
  USBOTG_FS->UEP2_3_MOD = 0xCC;
#if CFG_TUSB_MCU == OPT_MCU_CH583
  // CH58X: a single mode register enables EP5/6/7 RX+TX (different bit layout than CH32).
  USBOTG_FS->UEP567_MOD = RB_UEP5_RX_EN | RB_UEP5_TX_EN | RB_UEP6_RX_EN | RB_UEP6_TX_EN |
                          RB_UEP7_RX_EN | RB_UEP7_TX_EN;
#else
  USBOTG_FS->UEP5_6_MOD = 0xCC;
  USBOTG_FS->UEP7_MOD   = 0x0C;
#endif

  reset_ep_ctrls();

  dcd_connect(rhport);

  return true;
}

void dcd_int_handler(uint8_t rhport) {
  (void)rhport;
  uint8_t status = USBOTG_FS->INT_FG;
  if (status & USBFS_INT_FG_TRANSFER) {
    uint8_t  int_st   = USBOTG_FS->INT_ST;
    uint8_t  ep       = USBFS_INT_ST_MASK_UIS_ENDP(int_st);
    uint8_t  token    = USBFS_INT_ST_MASK_UIS_TOKEN(int_st);
    uint16_t rx_len   = USBOTG_FS->RX_LEN;

    switch (token) {
      case PID_OUT: {
        // Drop an OUT packet whose data toggle doesn't match what we expect -- a host retransmit
        // after a lost ACK, or a host that doesn't alternate DATA0/DATA1. The hardware auto-toggle
        // does not reject these on its own, so the check is needed on every variant. EP0 keeps its
        // own toggle via the SETUP/status flow and is exempt; isochronous is DATA0-only (no toggle),
        // so its packets must not be toggle-checked.
        if (ep != 0 && !data.isochronous[ep][TUSB_DIR_OUT] && !(int_st & USBFS_INT_ST_TOG_OK)) { break; }
#ifdef CH32_USBFS_EP_MANUAL_TOG
        // CH58x has no hardware auto-toggle: advance the expected RX toggle after each accepted packet
        // (EP0 included -- it also has no auto-toggle and a control-OUT data stage can span packets).
        // Iso endpoints are DATA0-only, so leave them alone (matches the PID_IN path).
        if (!data.isochronous[ep][TUSB_DIR_OUT]) { EP_CTRL(ep) ^= USBFS_EPC_R_TOG; }
#endif
        update_out(rhport, ep, rx_len);
        break;
      }

      case PID_IN:
#ifdef CH32_USBFS_EP_MANUAL_TOG
        // Manual toggle: flip the TX toggle after each ACK'd IN packet (EP0 manages its own).
        // Isochronous transfers are DATA0-only (no toggle), so leave iso endpoints alone.
        if (ep != 0 && !data.isochronous[ep][TUSB_DIR_IN]) { EP_CTRL(ep) ^= USBFS_EPC_T_TOG; }
#endif
        update_in(rhport, ep, false);
        break;

      case PID_SETUP:
        // setup clears stall
        ep_tx_ctrl_set(0, USBFS_EP_T_RES_NAK);
        data.ep0_tog  = true;
        // A new SETUP supersedes any control transfer still in flight; drop its stale EP0 state so a
        // spurious EP0 IN/OUT can't run update_in()/update_out() against the previous request.
        data.xfer[0][TUSB_DIR_OUT].valid = false;
        data.xfer[0][TUSB_DIR_IN].valid  = false;

        uint8_t *ep0_out = ep_out_buf(0);
        const tusb_control_request_t *setup = (const tusb_control_request_t *)ep0_out;
        // EP0_SETUP_RX_TOG arms the data/status stage at DATA1 on the combined-control IP
        ep_rx_ctrl_set(0, ((setup->wLength == 0) ? USBFS_EP_R_RES_ACK : USBFS_EP_R_RES_NAK) | EP0_SETUP_RX_TOG);

        dcd_event_setup_received(rhport, ep0_out, true);
        break;
    }

    USBOTG_FS->INT_FG = USBFS_INT_FG_TRANSFER;
  } else if (status & USBFS_INT_FG_BUS_RST) {
    data.ep0_tog                        = true;
    data.xfer[0][TUSB_DIR_OUT].max_size = 64;
    data.xfer[0][TUSB_DIR_IN].max_size  = 64;

    // dcd_event_bus_reset(rhport, (USBOTG_FS->BASE_CTRL & USBFS_CTRL_LOW_SPEED) ? TUSB_SPEED_LOW : TUSB_SPEED_FULL,
    // true);
    dcd_event_bus_reset(rhport, (USBOTG_FS->UDEV_CTRL & USBFS_UDEV_CTRL_LOW_SPEED) ? TUSB_SPEED_LOW : TUSB_SPEED_FULL,
                        true);

    USBOTG_FS->DEV_ADDR = 0x00;
    ep_rx_ctrl_set(0, USBFS_EP_R_RES_ACK);

    reset_ep_ctrls();

    USBOTG_FS->INT_FG = USBFS_INT_FG_BUS_RST;
  } else if (status & USBFS_INT_FG_SUSPEND) {
#if CFG_TUSB_MCU == OPT_MCU_CH583
    // CH58x raises this single interrupt for both suspend and resume; MIS_ST's suspend bit tells
    // them apart (set while suspended, clear once resumed) so tud_resume_cb() actually fires.
    dcd_event_t event = {.rhport = rhport,
                         .event_id = (USBOTG_FS->MIS_ST & USBFS_MIS_ST_SUSPEND) ? DCD_EVENT_SUSPEND : DCD_EVENT_RESUME};
#else
    dcd_event_t event = {.rhport = rhport, .event_id = DCD_EVENT_SUSPEND};
#endif
    dcd_event_handler(&event, true);
    USBOTG_FS->INT_FG = USBFS_INT_FG_SUSPEND;
  }
}

void dcd_int_enable(uint8_t rhport) {
  (void)rhport;
  NVIC_EnableIRQ(USBHD_IRQn);
}

void dcd_int_disable(uint8_t rhport) {
  (void)rhport;
  NVIC_DisableIRQ(USBHD_IRQn);
}

void dcd_set_address(uint8_t rhport, uint8_t dev_addr) {
  (void)dev_addr;
  dcd_edpt_xfer(rhport, 0x80, NULL, 0, false); // zlp status response
}

void dcd_remote_wakeup(uint8_t rhport) {
  (void)rhport;
  // TODO optional
}

void dcd_connect(uint8_t rhport) {
  (void)rhport;
  USBOTG_FS->BASE_CTRL |= USBFS_CTRL_DEV_PUEN;
}

void dcd_disconnect(uint8_t rhport) {
  (void)rhport;
  USBOTG_FS->BASE_CTRL &= ~USBFS_CTRL_DEV_PUEN;
}

void dcd_sof_enable(uint8_t rhport, bool en) {
  (void)rhport;
  (void)en;

  // TODO implement later
}

void dcd_edpt0_status_complete(uint8_t rhport, const tusb_control_request_t *request) {
  (void)rhport;
  if (request->bmRequestType_bit.recipient == TUSB_REQ_RCPT_DEVICE &&
      request->bmRequestType_bit.type == TUSB_REQ_TYPE_STANDARD && request->bRequest == TUSB_REQ_SET_ADDRESS) {
#if CFG_TUSB_MCU == OPT_MCU_CH583
    // On CH58x R8_USB_DEV_AD bit 7 is a user general-purpose flag; only bits [6:0] are the address.
    USBOTG_FS->DEV_ADDR = (uint8_t)((USBOTG_FS->DEV_ADDR & 0x80u) | (request->wValue & 0x7Fu));
#else
    USBOTG_FS->DEV_ADDR = (uint8_t)request->wValue;
#endif
  }
}

bool dcd_edpt_open(uint8_t rhport, const tusb_desc_endpoint_t *desc_ep) {
  (void)rhport;
  uint8_t ep  = tu_edpt_number(desc_ep->bEndpointAddress);
  uint8_t dir = tu_edpt_dir(desc_ep->bEndpointAddress);
  TU_ASSERT(ep < EP_MAX);

  data.isochronous[ep][dir]   = false; // (re)opening as a non-iso endpoint clears any stale iso flag
  data.xfer[ep][dir].max_size = tu_edpt_packet_size(desc_ep);

  if (ep != 0) {
    // Opening clears the toggle to DATA0 (ep_*_ctrl_set writes the toggle bit clear since v has no
    // R/T_TOG); with manual toggle EP_*_AUTO_TOG is 0 so the ISR owns subsequent toggling.
    if (dir == TUSB_DIR_OUT) {
      ep_rx_ctrl_set(ep, EP_R_AUTO_TOG | USBFS_EP_R_RES_NAK);
    } else {
      ep_tx_ctrl_set(ep, EP_T_AUTO_TOG | USBFS_EP_T_RES_NAK);
    }
  }
  return true;
}

void dcd_edpt_close_all(uint8_t rhport) {
  (void)rhport;
  // TODO optional
}

bool dcd_edpt_iso_alloc(uint8_t rhport, uint8_t ep_addr, uint16_t largest_packet_size) {
  (void)rhport;
  uint8_t ep  = tu_edpt_number(ep_addr);
  uint8_t dir = tu_edpt_dir(ep_addr);
  TU_ASSERT(ep < EP_MAX);

  // Endpoint buffers are 64 B, except EP3 IN which is enlarged for full-speed iso on the parts that
  // support 1023-byte EP3 packets (CH32V20x/V30x/F20x; CFG_TUD_WCH_USBFS_EP3_BUFSIZE). Reject a
  // larger mps rather than running off the end into the neighbouring endpoint's memory.
  uint16_t max_packet = 64;
#if CFG_TUD_WCH_USBFS_EP3_BUFSIZE > 64
  if (ep == 3 && dir == TUSB_DIR_IN) { max_packet = CFG_TUD_WCH_USBFS_EP3_BUFSIZE; }
#endif
  TU_VERIFY(largest_packet_size <= max_packet);

  data.isochronous[ep][dir]   = true;
  data.xfer[ep][dir].max_size = largest_packet_size;
  return true;
}

bool dcd_edpt_iso_activate(uint8_t rhport, const tusb_desc_endpoint_t *desc_ep) {
  (void)rhport;
  const uint8_t ep = tu_edpt_number(desc_ep->bEndpointAddress);
  const uint8_t dir = tu_edpt_dir(desc_ep->bEndpointAddress);

  // a transfer armed before SET_INTERFACE survives to here (no dcd close on this port): drop the
  // stale descriptor and NAK the endpoint so the ISR can't complete it against the old buffer
  data.xfer[ep][dir].valid = false;
  if (dir == TUSB_DIR_IN) {
    ep_tx_set_response(ep, USBFS_EP_T_RES_NAK);
  } else {
    ep_rx_set_response(ep, USBFS_EP_R_RES_NAK);
  }
  return true;
}

bool dcd_edpt_xfer(uint8_t rhport, uint8_t ep_addr, uint8_t *buffer, uint16_t total_bytes, bool is_isr) {
  (void)is_isr;
  (void)rhport;
  uint8_t ep  = tu_edpt_number(ep_addr);
  uint8_t dir = tu_edpt_dir(ep_addr);

  struct usb_xfer *xfer = &data.xfer[ep][dir];
  // Keep the IRQ masked across the whole arming sequence: update_in()/ep_rx_set_response() do a
  // read-modify-write of the (combined) EP control register, which the ISR also RMWs to flip the
  // manual data toggle; re-enabling before they run lets a transfer IRQ clobber that toggle.
  dcd_int_disable(rhport);
  xfer->valid         = true;
  xfer->buffer        = buffer;
  xfer->len           = total_bytes;
  xfer->processed_len = 0;

  if (dir == TUSB_DIR_IN) {
    update_in(rhport, ep, true);
  } else {
    uint8_t rx_res = data.isochronous[ep][TUSB_DIR_OUT] ? USBFS_EP_R_RES_NYET : USBFS_EP_R_RES_ACK;
    ep_rx_set_response(ep, rx_res);
  }
  dcd_int_enable(rhport);
  return true;
}

void dcd_edpt_stall(uint8_t rhport, uint8_t ep_addr) {
  (void)rhport;
  uint8_t ep  = tu_edpt_number(ep_addr);
  uint8_t dir = tu_edpt_dir(ep_addr);
  if (ep == 0) {
    if (dir == TUSB_DIR_OUT) {
      ep_rx_ctrl_set(0, USBFS_EP_R_RES_STALL);
    } else {
      EP_TX_LEN(0)  = 0;
      ep_tx_ctrl_set(0, USBFS_EP_T_RES_STALL);
    }
  } else {
    if (dir == TUSB_DIR_OUT) {
      ep_rx_set_response(ep, USBFS_EP_R_RES_STALL);
    } else {
      ep_tx_set_response(ep, USBFS_EP_T_RES_STALL);
    }
  }
}

void dcd_edpt_clear_stall(uint8_t rhport, uint8_t ep_addr) {
  (void)rhport;
  uint8_t ep  = tu_edpt_number(ep_addr);
  uint8_t dir = tu_edpt_dir(ep_addr);
  if (ep == 0) {
    if (dir == TUSB_DIR_OUT) {
      ep_rx_ctrl_set(0, USBFS_EP_R_RES_ACK);
    }
  } else {
    // clear-stall resets the toggle to DATA0 (USB spec); manual-toggle parts then re-sync via ISR.
    // Preserve an in-flight receive: if a read is still armed (the class driver considers it
    // submitted and won't re-arm), fall back to ACK, not NAK, or the endpoint NAKs forever and the
    // host times out (usbtest toggle test 29 clears the halt between bulk writes on an armed EP).
    if (dir == TUSB_DIR_OUT) {
      uint8_t res = data.xfer[ep][TUSB_DIR_OUT].valid
                    ? (data.isochronous[ep][TUSB_DIR_OUT] ? USBFS_EP_R_RES_NYET : USBFS_EP_R_RES_ACK)
                    : USBFS_EP_R_RES_NAK;
      ep_rx_ctrl_set(ep, EP_R_AUTO_TOG | res);
    } else {
      ep_tx_ctrl_set(ep, EP_T_AUTO_TOG | USBFS_EP_T_RES_NAK);
    }
  }
}
#endif
