/*
 * SPDX-FileCopyrightText: Copyright (c) 2020 Raspberry Pi (Trading) Ltd.
 * SPDX-FileCopyrightText: Copyright (c) 2020 Ha Thach (tinyusb.org)
 * SPDX-License-Identifier: MIT
 *
 * This file is part of the TinyUSB stack.
 */

#include "tusb_option.h"

#if CFG_TUD_ENABLED && (CFG_TUSB_MCU == OPT_MCU_RP2040) && !CFG_TUD_RPI_PIO_USB

#include "pico.h"
#include "hardware/sync.h"
#include "rp2040_usb.h"

#if TUD_OPT_RP2040_USB_DEVICE_ENUMERATION_FIX
#include "pico/fix/rp2040_usb_device_enumeration.h"
#endif

#include "device/dcd.h"

// Current implementation force vbus detection as always present, causing device think it is always plugged into host.
// Therefore, it cannot detect disconnect event, mistaken it as suspend.
// Note: won't work if change to 0 (for now)
  #define FORCE_VBUS_DETECT 1

  #define USB_INTS_ERROR_BITS                                                                 \
    (USB_INTS_ERROR_DATA_SEQ_BITS | USB_INTS_ERROR_BIT_STUFF_BITS | USB_INTS_ERROR_CRC_BITS | \
     USB_INTS_ERROR_RX_OVERFLOW_BITS | USB_INTS_ERROR_RX_TIMEOUT_BITS)

/*------------------------------------------------------------------*/
/* Low level controller
 *------------------------------------------------------------------*/
// HW buffer pointer from USB buffer space (max 3840 bytes)
static uint8_t *hw_buffer_ptr;

// USB_MAX_ENDPOINTS Endpoints, direction TUSB_DIR_OUT for out and TUSB_DIR_IN for in.
static struct hw_endpoint hw_endpoints[USB_MAX_ENDPOINTS][2];

// SOF may be used by remote wakeup as RESUME, this indicates whether SOF is actually used by usbd
static bool _sof_enable = false;

TU_ATTR_ALWAYS_INLINE static inline hw_endpoint_t *hw_endpoint_get(uint8_t epnum, tusb_dir_t dir) {
  return &hw_endpoints[epnum][dir];
}

TU_ATTR_ALWAYS_INLINE static inline hw_endpoint_t *hw_endpoint_get_by_addr(uint8_t ep_addr) {
  const uint8_t    num = tu_edpt_number(ep_addr);
  const tusb_dir_t dir = tu_edpt_dir(ep_addr);
  return hw_endpoint_get(num, dir);
}

TU_ATTR_ALWAYS_INLINE static inline io_rw_32 *get_ep_ctrl(uint8_t epnum, tusb_dir_t dir) {
  if (epnum == 0) {
    // EP0 has no endpoint control register because the buffer offsets are fixed and always enabled
    return NULL;
  }
  struct usb_device_dpram_ep_ctrl *ep_ctrl = &usb_dpram->ep_ctrl[epnum - 1];
  return (dir == TUSB_DIR_IN) ? &ep_ctrl->in : &ep_ctrl->out;
}

TU_ATTR_ALWAYS_INLINE static inline io_rw_32 *get_buf_ctrl(uint8_t epnum, tusb_dir_t dir) {
  struct usb_device_dpram_ep_buf_ctrl *buf_ctrl = &usb_dpram->ep_buf_ctrl[epnum];
  return (dir == TUSB_DIR_IN) ? &buf_ctrl->in : &buf_ctrl->out;
}

// Init and enable endpoint
static void hw_endpoint_open(uint8_t ep_addr, uint16_t wMaxPacketSize, uint8_t transfer_type, bool ep_enabled) {
  const uint8_t    epnum = tu_edpt_number(ep_addr);
  const tusb_dir_t dir   = tu_edpt_dir(ep_addr);

  hw_endpoint_t *ep   = hw_endpoint_get(epnum, dir);
  ep->ep_addr         = ep_addr;
  ep->next_pid        = 0u;
  ep->max_packet_size = wMaxPacketSize;

  // Clear existing buffer control state
  io_rw_32 *buf_reg = get_buf_ctrl(epnum, dir);
  *buf_reg          = 0;

  // allocated hw buffer
  if (epnum == 0) {
    // Buffer offset is fixed (2 buffer allocated).
    // Note: Only single buffer for EP since Double buffered RX can be troublesome with future data.
    ep->dpram_buf = (uint8_t *)&usb_dpram->ep0_buf_a[0];
  } else {
    uint32_t ep_ctrl = EP_CTRL_INTERRUPT_PER_BUFFER | ((uint32_t)transfer_type << EP_CTRL_BUFFER_TYPE_LSB);

    // round up size to multiple of 64
    uint16_t size = (uint16_t)tu_round_up(wMaxPacketSize, 64);

    // double buffered Bulk endpoint
    if (transfer_type == TUSB_XFER_BULK) {
      size *= 2u;
  #if CFG_TUSB_RP2_ERRATA_E15
      if (dir == TUSB_DIR_IN) {
        ep->e15_bulk_in = true;
      }
  #endif
    }

    // assign buffer
    ep->dpram_buf = hw_buffer_ptr;
    hw_buffer_ptr += size;

    ep_ctrl |= hw_data_offset(ep->dpram_buf);
    if (ep_enabled) {
      ep_ctrl |= EP_CTRL_ENABLE_BITS;
    }

    *get_ep_ctrl(epnum, dir) = ep_ctrl;

    hard_assert(hw_buffer_ptr < usb_dpram->epx_data + sizeof(usb_dpram->epx_data));
    pico_info("  Allocated %d bytes (0x%p)\r\n", size, ep->dpram_buf);
  }
}

static void hw_endpoint_abort_xfer(struct hw_endpoint* ep) {
  // Abort any pending transfer
  const uint8_t  dir        = (uint8_t)tu_edpt_dir(ep->ep_addr);
  const uint8_t  epnum      = tu_edpt_number(ep->ep_addr);
  const uint32_t abort_mask = TU_BIT((epnum << 1) | (dir ? 0 : 1));

  // Due to Errata RP2040-E2: ABORT flag is only applicable for B2 and later (unusable for B0, B1).
  // Which means we are not guaranteed to safely abort pending transfer on B0 and B1.
  if (rp2040_chip_version() >= 2) {
    usb_hw_set->abort = abort_mask;
    while ((usb_hw->abort_done & abort_mask) != abort_mask) {}
  }

  io_rw_32 *buf_reg = get_buf_ctrl(epnum, dir);
  *buf_reg          = 0; // clear buffer control
  rp2usb_reset_transfer(ep);

  if (rp2040_chip_version() >= 2) {
    usb_hw_clear->abort_done = abort_mask;
    usb_hw_clear->abort = abort_mask;
  }
}

static void __tusb_irq_path_func(handle_hw_buff_status)(void) {
  uint32_t buf_status = usb_hw->buf_status;
  pico_trace("buf_status = 0x%08lx\r\n", buf_status);
  while (buf_status) {
    // ctz/clz is faster than loop which has only a few bit set in general
    const uint8_t i = (uint8_t) __builtin_ctz(buf_status);
    const uint32_t bit = TU_BIT(i);

    // IN transfer for even i, OUT transfer for odd i
    const uint8_t    epnum   = i >> 1u;
    const tusb_dir_t dir     = (i & 1u) ? TUSB_DIR_OUT : TUSB_DIR_IN;
    hw_endpoint_t   *ep      = hw_endpoint_get(epnum, dir);
    io_rw_32        *ep_reg  = get_ep_ctrl(epnum, dir);
    io_rw_32        *buf_reg = get_buf_ctrl(epnum, dir);

    // Double-buffered: if both buffers completed at once, buf_status re-sets
    // immediately after clearing (datasheet Table 406). Process the second buffer too.
    while (usb_hw->buf_status & bit) {
      const uint8_t buf_id     = (usb_hw->buf_cpu_should_handle & bit) ? 1 : 0; // before clear buf_status
      usb_hw_clear->buf_status = bit;
      buf_status &= ~bit;

      if (rp2usb_xfer_continue(ep, ep_reg, buf_reg, buf_id, dir == TUSB_DIR_OUT)) {
        const uint16_t xferred_len = ep->xferred_len;
        rp2usb_reset_transfer(ep);
        dcd_event_xfer_complete(0, ep->ep_addr, xferred_len, XFER_RESULT_SUCCESS, true);
      }
    }
  }
}

TU_ATTR_ALWAYS_INLINE static inline void reset_ep0(void) {
  // If we have finished this transfer on EP0 set pid back to 1 for next
  // setup transfer. Also clear a stall in case
  for (uint8_t dir = 0; dir < 2; dir++) {
    struct hw_endpoint *ep = hw_endpoint_get(0, dir);
    ep->next_pid = 1u;
    if (ep->state == EPSTATE_ACTIVE) {
      hw_endpoint_abort_xfer(ep); // Abort any pending transfer per USB specs
    }
  }
}

static void __tusb_irq_path_func(reset_non_control_endpoints)(void) {
  // Disable all non-control
  for (uint8_t i = 0; i < USB_MAX_ENDPOINTS - 1; i++) {
    usb_dpram->ep_ctrl[i].in = 0;
    usb_dpram->ep_ctrl[i].out = 0;
  }

  // clear non-control hw endpoints
  tu_memclr(hw_endpoints[1], sizeof(hw_endpoints) - 2 * sizeof(hw_endpoint_t));

  // reclaim buffer space
  hw_buffer_ptr = &usb_dpram->epx_data[0];
}

static void __tusb_irq_path_func(dcd_rp2040_irq)(void) {
  const uint32_t status  = usb_hw->ints;

  if (status & USB_INTF_DEV_SOF_BITS) {
    uint32_t sof_count = usb_hw->sof_rd & USB_SOF_RD_BITS; // clear interrupt by reading SOF_RD

  #if CFG_TUSB_RP2_ERRATA_E15
    e15_last_sof = time_us_32();                           // timing critical
  #endif

    dcd_event_sof(0, sof_count, true);
  }

  // xfer events are handled before setup req. So if a transfer completes immediately
  // before closing the EP, the events will be delivered in same order.
  if (status & USB_INTS_BUFF_STATUS_BITS) {
    handle_hw_buff_status();
  }

  if (status & USB_INTS_SETUP_REQ_BITS) {
    const uint8_t *setup = remove_volatile_cast(const uint8_t *, &usb_dpram->setup_packet);

    // reset pid to both 1 (data and ack)
    reset_ep0();

    // Pass setup packet to tiny usb
    dcd_event_setup_received(0, setup, true);
    usb_hw_clear->sie_status = USB_SIE_STATUS_SETUP_REC_BITS;
  }

  // Errata 15 workaround for Device Bulk-In endpoint, must be after BUF_STATUS interrupt to sync buf control first
  if (status & USB_INTF_DEV_SOF_BITS) {
    bool keep_sof_alive = false;

  #if CFG_TUSB_RP2_ERRATA_E15
    for (uint8_t i = 0; i < USB_MAX_ENDPOINTS; i++) {
      struct hw_endpoint *ep = hw_endpoint_get(i, TUSB_DIR_IN);

      // Active Bulk IN endpoint requires SOF
      if (ep->e15_bulk_in && ep->state >= EPSTATE_ACTIVE) {
        keep_sof_alive = true;
        hw_endpoint_lock_update(ep, 1);

        if (ep->state == EPSTATE_PENDING) {
          ep->state = EPSTATE_ACTIVE;

          io_rw_32 *buf_reg32 = get_buf_ctrl(i, TUSB_DIR_IN);
          io_rw_16 *buf_reg16 = (io_rw_16 *)buf_reg32;

          // Check each buffer half: idle when both FULL and AVAIL are clear.
          // Use 16-bit writes to avoid clobbering the other half (DPSRAM concurrent access).
          enum {
            BUSY_MASK = USB_BUF_CTRL_FULL | USB_BUF_CTRL_AVAIL
          };

          const bool buf0_idle = !(buf_reg16[0] & BUSY_MASK);
          const bool buf1_idle = (ep->remaining_len > 0) && !(buf_reg16[1] & BUSY_MASK);

          if (buf0_idle && buf1_idle) {
            // both are idle, start fresh
            io_rw_32 *ep_reg = get_ep_ctrl(i, TUSB_DIR_IN);
            rp2usb_buffer_start(ep, ep_reg, buf_reg32, false);
          } else if (buf0_idle) {
            uint16_t buf0 = bufctrl_prepare16(ep, ep->dpram_buf, false);
            bufctrl_write16(buf_reg16, buf0);
          } else if (buf1_idle) {
            uint16_t buf1 = bufctrl_prepare16(ep, ep->dpram_buf + 64, false);
            bufctrl_write16(buf_reg16 + 1, buf1);
          }
        }

        hw_endpoint_lock_update(ep, -1);
      }
    }
  #endif

    // disable SOF interrupt if it is used for RESUME in remote wakeup
    if (!keep_sof_alive && !_sof_enable) {
      usb_hw_clear->inte = USB_INTS_DEV_SOF_BITS;
    }
  }

  #if FORCE_VBUS_DETECT == 0
  // Since we force VBUS detect On, device will always think it is connected and
  // couldn't distinguish between disconnect and suspend
  if (status & USB_INTS_DEV_CONN_DIS_BITS) {
    if (usb_hw->sie_status & USB_SIE_STATUS_CONNECTED_BITS) {
      // Connected: nothing to do
    } else {
      // Disconnected
      dcd_event_bus_signal(0, DCD_EVENT_UNPLUGGED, true);
    }
    usb_hw_clear->sie_status = USB_SIE_STATUS_CONNECTED_BITS;
  }
#endif

  // SE0 for 2.5 us or more (will last at least 10ms)
  if (status & USB_INTS_BUS_RESET_BITS) {
    pico_trace("BUS RESET\r\n");
    usb_hw->dev_addr_ctrl = 0;
    reset_non_control_endpoints();
    dcd_event_bus_reset(0, TUSB_SPEED_FULL, true);
    usb_hw_clear->sie_status = USB_SIE_STATUS_BUS_RESET_BITS;

  #if TUD_OPT_RP2040_USB_DEVICE_ENUMERATION_FIX
    // Only run enumeration workaround if pull up is enabled
    if (usb_hw->sie_ctrl & USB_SIE_CTRL_PULLUP_EN_BITS) {
      rp2040_usb_device_enumeration_fix();
    }
  #endif
  }

  /* Note from pico datasheet 4.1.2.6.4 (v1.2)
   * If you enable the suspend interrupt, it is likely you will see a suspend interrupt when
   * the device is first connected but the bus is idle. The bus can be idle for a few ms before
   * the host begins sending start of frame packets. You will also see a suspend interrupt
   * when the device is disconnected if you do not have a VBUS detect circuit connected. This is
   * because without VBUS detection, it is impossible to tell the difference between
   * being disconnected and suspended.
   */
  if (status & USB_INTS_DEV_SUSPEND_BITS) {
    dcd_event_bus_signal(0, DCD_EVENT_SUSPEND, true);
    usb_hw_clear->sie_status = USB_SIE_STATUS_SUSPENDED_BITS;
  }

  if (status & USB_INTS_DEV_RESUME_FROM_HOST_BITS) {
    dcd_event_bus_signal(0, DCD_EVENT_RESUME, true);
    usb_hw_clear->sie_status = USB_SIE_STATUS_RESUME_BITS;
  }

}

/*------------------------------------------------------------------*/
/* Controller API
 *------------------------------------------------------------------*/

// older SDK
#ifndef PICO_SHARED_IRQ_HANDLER_HIGHEST_ORDER_PRIORITY
#define PICO_SHARED_IRQ_HANDLER_HIGHEST_ORDER_PRIORITY 0xff
#endif

bool dcd_init(uint8_t rhport, const tusb_rhport_init_t* rh_init) {
  (void) rh_init;
  assert(rhport == 0);

  // TU_LOG(1, "Chip Version B%u\r\n", rp2040_chip_version());

  // Reset hardware to default state
  rp2usb_init();

  #if FORCE_VBUS_DETECT
  // Force VBUS detect so the device thinks it is plugged into a host
  usb_hw->pwr = USB_USB_PWR_VBUS_DETECT_BITS | USB_USB_PWR_VBUS_DETECT_OVERRIDE_EN_BITS;
#endif

  irq_add_shared_handler(USBCTRL_IRQ, dcd_rp2040_irq, PICO_SHARED_IRQ_HANDLER_HIGHEST_ORDER_PRIORITY);

  // Init control endpoints
  tu_memclr(hw_endpoints[0], 2 * sizeof(hw_endpoint_t));
  hw_endpoint_open(0x0, 64, TUSB_XFER_CONTROL, false);
  hw_endpoint_open(0x80, 64, TUSB_XFER_CONTROL, false);

  // Init non-control endpoints
  reset_non_control_endpoints();

  // Initializes the USB peripheral for device mode and enables it.
  // Don't need to enable the pull up here. Force VBUS
  usb_hw->main_ctrl = USB_MAIN_CTRL_CONTROLLER_EN_BITS;

  // Enable individual controller IRQS here. Processor interrupt enable will be used
  // for the global interrupt enable...
  // Note: Force VBUS detect cause disconnection not detectable
  usb_hw->sie_ctrl = USB_SIE_CTRL_EP0_INT_1BUF_BITS;
  usb_hw->inte = USB_INTS_BUFF_STATUS_BITS | USB_INTS_BUS_RESET_BITS | USB_INTS_SETUP_REQ_BITS |
                 USB_INTS_DEV_SUSPEND_BITS | USB_INTS_DEV_RESUME_FROM_HOST_BITS |
                 (FORCE_VBUS_DETECT ? 0 : USB_INTS_DEV_CONN_DIS_BITS);

  dcd_connect(rhport);
  return true;
}

bool dcd_deinit(uint8_t rhport) {
  (void) rhport;

  reset_non_control_endpoints();
  irq_remove_handler(USBCTRL_IRQ, dcd_rp2040_irq);

  // reset usb hardware into initial state
  reset_block(RESETS_RESET_USBCTRL_BITS);
  unreset_block_wait(RESETS_RESET_USBCTRL_BITS);

  return true;
}

void dcd_int_enable(__unused uint8_t rhport) {
  assert(rhport == 0);
  irq_set_enabled(USBCTRL_IRQ, true);
}

void dcd_int_disable(__unused uint8_t rhport) {
  assert(rhport == 0);
  irq_set_enabled(USBCTRL_IRQ, false);
}

void dcd_set_address(uint8_t rhport, uint8_t dev_addr) {
  (void)dev_addr;
  // Can't set device address in hardware until status xfer has complete
  // Send 0len complete response on EP0 IN
  dcd_edpt_xfer(rhport, 0x80, NULL, 0, false);
}

void dcd_remote_wakeup(__unused uint8_t rhport) {
  pico_info("dcd_remote_wakeup %d\n", rhport);
  assert(rhport == 0);

  // since RESUME interrupt is not triggered if we are the one initiate
  // briefly enable SOF to notify usbd when bus is ready
  usb_hw_set->inte = USB_INTS_DEV_SOF_BITS;
  usb_hw_set->sie_ctrl = USB_SIE_CTRL_RESUME_BITS;
}

// disconnect by disabling internal pull-up resistor on D+/D-
void dcd_disconnect(__unused uint8_t rhport) {
  (void) rhport;
  usb_hw_clear->sie_ctrl = USB_SIE_CTRL_PULLUP_EN_BITS;
}

// connect by enabling internal pull-up resistor on D+/D-
void dcd_connect(__unused uint8_t rhport) {
  (void) rhport;
  usb_hw_set->sie_ctrl = USB_SIE_CTRL_PULLUP_EN_BITS;
}

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

  _sof_enable = en;

  if (en) {
    usb_hw_set->inte = USB_INTS_DEV_SOF_BITS;
  }
  #if !CFG_TUSB_RP2_ERRATA_E15
  else {
    // Don't clear immediately if the SOF workaround is in use.
    // The SOF handler will conditionally disable the interrupt.
    usb_hw_clear->inte = USB_INTS_DEV_SOF_BITS;
  }
#endif
}

/*------------------------------------------------------------------*/
/* DCD Endpoint port
 *------------------------------------------------------------------*/
void dcd_edpt0_status_complete(uint8_t rhport, tusb_control_request_t const* 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) {
    usb_hw->dev_addr_ctrl = (uint8_t) request->wValue;
  }
}

bool dcd_edpt_open(uint8_t rhport, tusb_desc_endpoint_t const* desc_edpt) {
  (void) rhport;
  const uint8_t xfer_type = desc_edpt->bmAttributes.xfer;
  hw_endpoint_open(desc_edpt->bEndpointAddress, tu_edpt_packet_size(desc_edpt), xfer_type, true);
  return true;
}

// New API: Allocate packet buffer used by ISO endpoints
// Some MCU need manual packet buffer allocation, we allocate the largest size to avoid clustering
bool dcd_edpt_iso_alloc(uint8_t rhport, uint8_t ep_addr, uint16_t largest_packet_size) {
  (void)rhport;
  hw_endpoint_open(ep_addr, largest_packet_size, TUSB_XFER_ISOCHRONOUS, false);
  return true;
}

// New API: Configure and enable an ISO endpoint according to descriptor
bool dcd_edpt_iso_activate(uint8_t rhport, const tusb_desc_endpoint_t *ep_desc) {
  (void)rhport;
  const uint8_t       epnum = tu_edpt_number(ep_desc->bEndpointAddress);
  const tusb_dir_t    dir   = tu_edpt_dir(ep_desc->bEndpointAddress);
  struct hw_endpoint *ep    = hw_endpoint_get(epnum, dir);
  TU_ASSERT(ep->dpram_buf != NULL); // must be inited and allocated previously

  if (ep->state == EPSTATE_ACTIVE) {
    hw_endpoint_abort_xfer(ep); // abort any pending transfer
  }
  ep->max_packet_size = ep_desc->wMaxPacketSize;

  // enable endpoint
  io_rw_32 *ep_reg = get_ep_ctrl(epnum, dir);
  if (ep_reg != NULL) {
    *ep_reg |= EP_CTRL_ENABLE_BITS;
  }
  return true;
}

void dcd_edpt_close_all(uint8_t rhport) {
  (void) rhport;
  // may need to use EP Abort
  reset_non_control_endpoints();
}

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

  hw_endpoint_t *ep = hw_endpoint_get(epnum, dir);
  io_rw_32      *ep_reg  = get_ep_ctrl(epnum, dir);
  io_rw_32      *buf_reg = get_buf_ctrl(epnum, dir);
  rp2usb_xfer_start(ep, ep_reg, buf_reg, buffer, NULL, total_bytes);
  return true;
}

#if CFG_TUD_EDPT_DEDICATED_HWFIFO
bool dcd_edpt_xfer_fifo(uint8_t rhport, uint8_t ep_addr, tu_fifo_t *ff, uint16_t total_bytes, bool is_isr) {
  (void)rhport;
  (void)is_isr;
  hw_endpoint_t *ep = hw_endpoint_get(epnum, dir);
  io_rw_32      *ep_reg  = get_ep_ctrl(epnum, dir);
  io_rw_32      *buf_reg = get_buf_ctrl(epnum, dir);
  rp2usb_xfer_start(ep, ep_reg, buf_reg, NULL, ff, total_bytes);
  return true;
}
#endif

void dcd_edpt_stall(uint8_t rhport, uint8_t ep_addr) {
  (void)rhport;
  const uint8_t    epnum = tu_edpt_number(ep_addr);
  const tusb_dir_t dir   = tu_edpt_dir(ep_addr);
  hw_endpoint_t   *ep    = hw_endpoint_get(epnum, dir);

  if (epnum == 0) {
    // A stall on EP0 has to be armed so it can be cleared on the next setup packet
    usb_hw_set->ep_stall_arm = (dir == TUSB_DIR_IN) ? USB_EP_STALL_ARM_EP0_IN_BITS : USB_EP_STALL_ARM_EP0_OUT_BITS;
  }

  // abort first then stall and clear current pending buffer
  hw_endpoint_abort_xfer(ep);
  io_rw_32 *buf_reg = get_buf_ctrl(epnum, dir);
  *buf_reg          = USB_BUF_CTRL_STALL;
}

void dcd_edpt_clear_stall(uint8_t rhport, uint8_t ep_addr) {
  (void) rhport;
  const uint8_t    epnum = tu_edpt_number(ep_addr);
  const tusb_dir_t dir   = tu_edpt_dir(ep_addr);

  if (epnum != 0) {
    struct hw_endpoint* ep = hw_endpoint_get(epnum, dir);

    if (ep->state == EPSTATE_ACTIVE) {
      // Clear-halt on an endpoint with an in-flight transfer is used as a data-toggle reset
      // (e.g. usbtest case 29) rather than to recover from a real stall (a stall aborts the
      // transfer, leaving the endpoint IDLE). Abort and re-issue the transfer with the toggle
      // reset to DATA0 so it still completes and releases the usbd claim, instead of silently
      // dropping it and starving the endpoint. Save the buffer/length before the abort clears them.
      uint8_t*       user_buf  = ep->user_buf;
      uint16_t       remaining = ep->remaining_len;
      const uint16_t xferred   = ep->xferred_len; // bytes already moved on this submission
      io_rw_32 *ep_reg  = get_ep_ctrl(epnum, dir);
      io_rw_32 *buf_reg = get_buf_ctrl(epnum, dir);
      // bufctrl_prepare16() subtracts each armed buffer's length from remaining_len when arming,
      // for BOTH directions, before the host has drained (IN) or filled (OUT) it. The abort below
      // discards those still-armed buffers, so rewind remaining_len by their lengths or the re-issue
      // is short by 1-2 packets. IN additionally advances user_buf as packets are copied into DPRAM,
      // so its pointer must rewind too; OUT copies out only on completion, so its pointer is intact.
      const uint32_t bc = *buf_reg;
      uint16_t staged = 0;
      if (bc & USB_BUF_CTRL_AVAIL) {
        staged = (uint16_t)(bc & USB_BUF_CTRL_LEN_MASK);
      }
      if ((bc >> 16) & USB_BUF_CTRL_AVAIL) {
        staged = (uint16_t)(staged + ((bc >> 16) & USB_BUF_CTRL_LEN_MASK));
      }
      remaining = (uint16_t)(remaining + staged);
      if (dir == TUSB_DIR_IN) {
        user_buf -= staged;
      }
      hw_endpoint_abort_xfer(ep); // safe abort (handles RP2040-E2), resets ep transfer state
      ep->next_pid = 0;           // DATA0
      rp2usb_xfer_start(ep, ep_reg, buf_reg, user_buf, NULL, remaining);
      // rp2usb_xfer_start() zeroes xferred_len; add back what the aborted transfer already moved so
      // the eventual completion reports the full length, not just the post-clear-halt remainder.
      ep->xferred_len += xferred;
    } else {
      ep->next_pid = 0; // reset data toggle
      io_rw_32 *buf_reg = get_buf_ctrl(epnum, dir);
      *buf_reg          = 0; // clear the stall response
    }
  }
}

void __tusb_irq_path_func(dcd_int_handler)(uint8_t rhport) {
  (void) rhport;
  dcd_rp2040_irq();
}

#endif
