/*
 * Copyright (c) 2018, Nordic Semiconductor ASA
 * Copyright (c) 2018 Sundar Subramaniyan <sundar.subramaniyan@gmail.com>
 *
 * SPDX-License-Identifier: Apache-2.0
 */

/**
 * @file  usb_dc_nrfx.c
 * @brief Nordic USB device controller driver
 *
 * The driver implements the interface between the USBD peripheral
 * driver from nrfx package and the operating system.
 */

#include <soc.h>
#include <string.h>
#include <stdio.h>
#include <zephyr/init.h>
#include <zephyr/kernel.h>
#include <zephyr/drivers/usb/usb_dc.h>
#include <zephyr/usb/usb_device.h>
#include <zephyr/drivers/clock_control.h>
#include <zephyr/drivers/clock_control/nrf_clock_control.h>
#include <nrf_usbd_common.h>
#include <hal/nrf_usbd.h>
#include <nrfx_power.h>


#define LOG_LEVEL CONFIG_USB_DRIVER_LOG_LEVEL
#include <zephyr/logging/log.h>
#include <zephyr/irq.h>
LOG_MODULE_REGISTER(usb_nrfx);

/* USB device controller access from devicetree */
#define DT_DRV_COMPAT nordic_nrf_usbd

/**
 * @brief nRF USBD peripheral states
 */
enum usbd_periph_state {
	USBD_DETACHED,
	USBD_ATTACHED,
	USBD_POWERED,
	USBD_SUSPENDED,
	USBD_RESUMED,
	USBD_DEFAULT,
	USBD_ADDRESS_SET,
	USBD_CONFIGURED,
};

/**
 * @brief Endpoint event types.
 */
enum usbd_ep_event_type {
	EP_EVT_SETUP_RECV,
	EP_EVT_RECV_REQ,
	EP_EVT_RECV_COMPLETE,
	EP_EVT_WRITE_COMPLETE,
};

/**
 * @brief USBD peripheral event types.
 */
enum usbd_event_type {
	USBD_EVT_POWER,
	USBD_EVT_EP,
	USBD_EVT_RESET,
	USBD_EVT_SOF,
	USBD_EVT_REINIT
};

/**
 * @brief Endpoint configuration.
 *
 * @param cb      Endpoint callback.
 * @param max_sz  Max packet size supported by endpoint.
 * @param en      Enable/Disable flag.
 * @param addr    Endpoint address.
 * @param type    Endpoint transfer type.
 */
struct nrf_usbd_ep_cfg {
	usb_dc_ep_callback cb;
	uint32_t max_sz;
	bool en;
	uint8_t addr;
	enum usb_dc_ep_transfer_type type;

};

struct usbd_mem_block {
	void *data;
};

/**
 * @brief Endpoint buffer
 *
 * @param len    Remaining length to be read/written.
 * @param block  Mempool block, for freeing up buffer after use.
 * @param data	 Pointer to the data buffer	for the endpoint.
 * @param curr	 Pointer to the current offset in the endpoint buffer.
 */
struct nrf_usbd_ep_buf {
	uint32_t len;
	struct usbd_mem_block block;
	uint8_t *data;
	uint8_t *curr;
};

/**
 * @brief Endpoint context
 *
 * @param cfg			Endpoint configuration
 * @param buf			Endpoint buffer
 * @param read_complete		A flag indicating that DMA read operation
 *				has been completed.
 * @param read_pending		A flag indicating that the Host has requested
 *				a data transfer.
 * @param write_in_progress	A flag indicating that write operation has
 *				been scheduled.
 * @param trans_zlp		Flag required for Control IN Endpoint. It
 *				indicates that ZLP is required to end data
 *				stage of the control request.
 */
struct nrf_usbd_ep_ctx {
	struct nrf_usbd_ep_cfg cfg;
	struct nrf_usbd_ep_buf buf;
	volatile bool read_complete;
	volatile bool read_pending;
	volatile bool write_in_progress;
	bool trans_zlp;
};

/**
 * @brief Endpoint event structure
 *
 * @param ep		Endpoint control block pointer
 * @param evt_type	Event type
 */
struct usbd_ep_event {
	struct nrf_usbd_ep_ctx *ep;
	enum usbd_ep_event_type evt_type;
};

/**
 * @brief Power event structure
 *
 * @param state		New USBD peripheral state.
 */
struct usbd_pwr_event {
	enum usbd_periph_state state;
};

/**
 * @brief Endpoint USB event
 *	  Used by ISR to send events to work handler
 *
 * @param node		Used by the kernel for FIFO management
 * @param block		Mempool block pointer for freeing up after use
 * @param evt		Event data field
 * @param evt_type	Type of event that has occurred from the USBD peripheral
 */
struct usbd_event {
	sys_snode_t node;
	struct usbd_mem_block block;
	union {
		struct usbd_ep_event ep_evt;
		struct usbd_pwr_event pwr_evt;
	} evt;
	enum usbd_event_type evt_type;
};

/**
 * @brief Fifo element slab
 *	Used for allocating fifo elements to pass from ISR to work handler
 * TODO: The number of FIFO elements is an arbitrary number now but it should
 * be derived from the theoretical number of backlog events possible depending
 * on the number of endpoints configured.
 */
#define FIFO_ELEM_SZ            sizeof(struct usbd_event)
#define FIFO_ELEM_ALIGN         sizeof(unsigned int)

K_MEM_SLAB_DEFINE(fifo_elem_slab, FIFO_ELEM_SZ,
		  CONFIG_USB_NRFX_EVT_QUEUE_SIZE, FIFO_ELEM_ALIGN);


/** Number of IN Endpoints configured (including control) */
#define CFG_EPIN_CNT (DT_INST_PROP(0, num_in_endpoints) +	\
		      DT_INST_PROP(0, num_bidir_endpoints))

/** Number of OUT Endpoints configured (including control) */
#define CFG_EPOUT_CNT (DT_INST_PROP(0, num_out_endpoints) +	\
		       DT_INST_PROP(0, num_bidir_endpoints))

/** Number of ISO IN Endpoints */
#define CFG_EP_ISOIN_CNT DT_INST_PROP(0, num_isoin_endpoints)

/** Number of ISO OUT Endpoints */
#define CFG_EP_ISOOUT_CNT DT_INST_PROP(0, num_isoout_endpoints)

/** ISO endpoint index */
#define EP_ISOIN_INDEX CFG_EPIN_CNT
#define EP_ISOOUT_INDEX (CFG_EPIN_CNT + CFG_EP_ISOIN_CNT + CFG_EPOUT_CNT)

#define EP_BUF_MAX_SZ		64UL
#define ISO_EP_BUF_MAX_SZ	1024UL

/**
 * @brief Output endpoint buffers
 *	Used as buffers for the endpoints' data transfer
 *	Max buffers size possible: 1536 Bytes (8 EP * 64B + 1 ISO * 1024B)
 */
static uint8_t ep_out_bufs[CFG_EPOUT_CNT][EP_BUF_MAX_SZ]
	       __aligned(sizeof(uint32_t));
static uint8_t ep_isoout_bufs[CFG_EP_ISOOUT_CNT][ISO_EP_BUF_MAX_SZ]
	       __aligned(sizeof(uint32_t));

/** Total endpoints configured */
#define CFG_EP_CNT (CFG_EPIN_CNT + CFG_EP_ISOIN_CNT + \
		    CFG_EPOUT_CNT + CFG_EP_ISOOUT_CNT)

/**
 * @brief USBD control structure
 *
 * @param status_cb	Status callback for USB DC notifications
 * @param setup		Setup packet for Control requests
 * @param hfxo_cli	Onoff client used to control HFXO
 * @param hfxo_mgr	Pointer to onoff manager associated with HFXO.
 * @param clk_requested	Flag used to protect against double stop.
 * @param attached	USBD Attached flag
 * @param ready		USBD Ready flag set after pullup
 * @param usb_work	USBD work item
 * @param drv_lock	Mutex for thread-safe nrfx driver use
 * @param ep_ctx	Endpoint contexts
 * @param ctrl_read_len	State of control read operation (EP0).
 */
struct nrf_usbd_ctx {
	usb_dc_status_callback status_cb;
	struct usb_setup_packet setup;
	struct onoff_client hfxo_cli;
	struct onoff_manager *hfxo_mgr;
	atomic_t clk_requested;

	bool attached;
	bool ready;

	struct k_work  usb_work;
	struct k_mutex drv_lock;

	struct nrf_usbd_ep_ctx ep_ctx[CFG_EP_CNT];

	uint16_t ctrl_read_len;
};


/* FIFO used for queuing up events from ISR. */
K_FIFO_DEFINE(usbd_evt_fifo);

/* Work queue used for handling the ISR events (i.e. for notifying the USB
 * device stack, for executing the endpoints callbacks, etc.) out of the ISR
 * context.
 * The system work queue cannot be used for this purpose as it might be used in
 * applications for scheduling USB transfers and this could lead to a deadlock
 * when the USB device stack would not be notified about certain event because
 * of a system work queue item waiting for a USB transfer to be finished.
 */
static struct k_work_q usbd_work_queue;
static K_KERNEL_STACK_DEFINE(usbd_work_queue_stack,
			     CONFIG_USB_NRFX_WORK_QUEUE_STACK_SIZE);


static struct nrf_usbd_ctx usbd_ctx = {
	.attached = false,
	.ready = false,
};

static inline struct nrf_usbd_ctx *get_usbd_ctx(void)
{
	return &usbd_ctx;
}

static inline bool dev_attached(void)
{
	return get_usbd_ctx()->attached;
}

static inline bool dev_ready(void)
{
	return get_usbd_ctx()->ready;
}

static inline nrf_usbd_common_ep_t ep_addr_to_nrfx(uint8_t ep)
{
	return (nrf_usbd_common_ep_t)ep;
}

static inline uint8_t nrfx_addr_to_ep(nrf_usbd_common_ep_t ep)
{
	return (uint8_t)ep;
}

static inline bool ep_is_valid(const uint8_t ep)
{
	uint8_t ep_num = USB_EP_GET_IDX(ep);

	if (NRF_USBD_EPIN_CHECK(ep)) {
		if (unlikely(ep_num == NRF_USBD_EPISO_FIRST)) {
			if (CFG_EP_ISOIN_CNT == 0) {
				return false;
			}
		} else {
			if (ep_num >= CFG_EPIN_CNT) {
				return false;
			}
		}
	} else {
		if (unlikely(ep_num == NRF_USBD_EPISO_FIRST)) {
			if (CFG_EP_ISOOUT_CNT == 0) {
				return false;
			}
		} else {
			if (ep_num >= CFG_EPOUT_CNT) {
				return false;
			}
		}
	}

	return true;
}

static struct nrf_usbd_ep_ctx *endpoint_ctx(const uint8_t ep)
{
	struct nrf_usbd_ctx *ctx;
	uint8_t ep_num;

	if (!ep_is_valid(ep)) {
		return NULL;
	}

	ctx = get_usbd_ctx();
	ep_num = NRF_USBD_EP_NR_GET(ep);

	if (NRF_USBD_EPIN_CHECK(ep)) {
		if (unlikely(NRF_USBD_EPISO_CHECK(ep))) {
			return &ctx->ep_ctx[EP_ISOIN_INDEX];
		} else {
			return &ctx->ep_ctx[ep_num];
		}
	} else {
		if (unlikely(NRF_USBD_EPISO_CHECK(ep))) {
			return &ctx->ep_ctx[EP_ISOOUT_INDEX];
		} else {
			return &ctx->ep_ctx[CFG_EPIN_CNT +
					    CFG_EP_ISOIN_CNT +
					    ep_num];
		}
	}

	return NULL;
}

static struct nrf_usbd_ep_ctx *in_endpoint_ctx(const uint8_t ep)
{
	return endpoint_ctx(NRF_USBD_EPIN(ep));
}

static struct nrf_usbd_ep_ctx *out_endpoint_ctx(const uint8_t ep)
{
	return endpoint_ctx(NRF_USBD_EPOUT(ep));
}

/**
 * @brief Schedule USBD event processing.
 *
 * Should be called after usbd_evt_put().
 */
static inline void usbd_work_schedule(void)
{
	k_work_submit_to_queue(&usbd_work_queue, &get_usbd_ctx()->usb_work);
}

/**
 * @brief Free previously allocated USBD event.
 *
 * Should be called after usbd_evt_get().
 *
 * @param Pointer to the USBD event structure.
 */
static inline void usbd_evt_free(struct usbd_event *ev)
{
	k_mem_slab_free(&fifo_elem_slab, (void *)ev->block.data);
}

/**
 * @brief Enqueue USBD event.
 *
 * @param Pointer to the previously allocated and filled event structure.
 */
static inline void usbd_evt_put(struct usbd_event *ev)
{
	k_fifo_put(&usbd_evt_fifo, ev);
}

/**
 * @brief Get next enqueued USBD event if present.
 */
static inline struct usbd_event *usbd_evt_get(void)
{
	return k_fifo_get(&usbd_evt_fifo, K_NO_WAIT);
}

/**
 * @brief Drop all enqueued events.
 */
static inline void usbd_evt_flush(void)
{
	struct usbd_event *ev;

	do {
		ev = usbd_evt_get();
		if (ev) {
			usbd_evt_free(ev);
		}
	} while (ev != NULL);
}

/**
 * @brief Allocate USBD event.
 *
 * This function should be called prior to usbd_evt_put().
 *
 * @returns Pointer to the allocated event or NULL if there was no space left.
 */
static inline struct usbd_event *usbd_evt_alloc(void)
{
	struct usbd_event *ev;
	struct usbd_mem_block block;

	if (k_mem_slab_alloc(&fifo_elem_slab,
			     (void **)&block.data, K_NO_WAIT)) {
		LOG_ERR("USBD event allocation failed!");

		/*
		 * Allocation may fail if workqueue thread is starved or event
		 * queue size is too small (CONFIG_USB_NRFX_EVT_QUEUE_SIZE).
		 * Wipe all events, free the space and schedule
		 * reinitialization.
		 */
		usbd_evt_flush();

		if (k_mem_slab_alloc(&fifo_elem_slab, (void **)&block.data, K_NO_WAIT)) {
			LOG_ERR("USBD event memory corrupted");
			__ASSERT_NO_MSG(0);
			return NULL;
		}

		ev = (struct usbd_event *)block.data;
		ev->block = block;
		ev->evt_type = USBD_EVT_REINIT;
		usbd_evt_put(ev);
		usbd_work_schedule();

		return NULL;
	}

	ev = (struct usbd_event *)block.data;
	ev->block = block;

	return ev;
}

static void submit_dc_power_event(enum usbd_periph_state state)
{
	struct usbd_event *ev = usbd_evt_alloc();

	if (!ev) {
		return;
	}

	ev->evt_type = USBD_EVT_POWER;
	ev->evt.pwr_evt.state = state;

	usbd_evt_put(ev);

	if (usbd_ctx.attached) {
		usbd_work_schedule();
	}
}

#if CONFIG_USB_NRFX_ATTACHED_EVENT_DELAY
static void attached_evt_delay_handler(struct k_timer *timer)
{
	LOG_DBG("ATTACHED event delay done");
	submit_dc_power_event(USBD_ATTACHED);
}

static K_TIMER_DEFINE(delay_timer, attached_evt_delay_handler, NULL);
#endif

static void usb_dc_power_event_handler(nrfx_power_usb_evt_t event)
{
	enum usbd_periph_state new_state;

	switch (event) {
	case NRFX_POWER_USB_EVT_DETECTED:
#if !CONFIG_USB_NRFX_ATTACHED_EVENT_DELAY
		new_state = USBD_ATTACHED;
		break;
#else
		LOG_DBG("ATTACHED event delayed");
		k_timer_start(&delay_timer,
			      K_MSEC(CONFIG_USB_NRFX_ATTACHED_EVENT_DELAY),
			      K_NO_WAIT);
		return;
#endif
	case NRFX_POWER_USB_EVT_READY:
		new_state = USBD_POWERED;
		break;
	case NRFX_POWER_USB_EVT_REMOVED:
		new_state = USBD_DETACHED;
		break;
	default:
		LOG_ERR("Unknown USB power event %d", event);
		return;
	}

	submit_dc_power_event(new_state);
}

/* Stopping HFXO, algorithm supports case when stop comes before clock is
 * started. In that case, it is stopped from the callback context.
 */
static int hfxo_stop(struct nrf_usbd_ctx *ctx)
{
	if (atomic_cas(&ctx->clk_requested, 1, 0)) {
		return onoff_cancel_or_release(ctx->hfxo_mgr, &ctx->hfxo_cli);
	}

	return 0;
}

static int hfxo_start(struct nrf_usbd_ctx *ctx)
{
	if (atomic_cas(&ctx->clk_requested, 0, 1)) {
		sys_notify_init_spinwait(&ctx->hfxo_cli.notify);

		return onoff_request(ctx->hfxo_mgr, &ctx->hfxo_cli);
	}

	return 0;
}

static void usbd_enable_endpoints(struct nrf_usbd_ctx *ctx)
{
	struct nrf_usbd_ep_ctx *ep_ctx;
	int i;

	for (i = 0; i < CFG_EPIN_CNT; i++) {
		ep_ctx = in_endpoint_ctx(i);
		__ASSERT_NO_MSG(ep_ctx);

		if (ep_ctx->cfg.en) {
			nrf_usbd_common_ep_enable(ep_addr_to_nrfx(ep_ctx->cfg.addr));
		}
	}

	if (CFG_EP_ISOIN_CNT) {
		ep_ctx = in_endpoint_ctx(NRF_USBD_EPIN(8));
		__ASSERT_NO_MSG(ep_ctx);

		if (ep_ctx->cfg.en) {
			nrf_usbd_common_ep_enable(ep_addr_to_nrfx(ep_ctx->cfg.addr));
		}
	}

	for (i = 0; i < CFG_EPOUT_CNT; i++) {
		ep_ctx = out_endpoint_ctx(i);
		__ASSERT_NO_MSG(ep_ctx);

		if (ep_ctx->cfg.en) {
			nrf_usbd_common_ep_enable(ep_addr_to_nrfx(ep_ctx->cfg.addr));
		}
	}

	if (CFG_EP_ISOOUT_CNT) {
		ep_ctx = out_endpoint_ctx(NRF_USBD_EPOUT(8));
		__ASSERT_NO_MSG(ep_ctx);

		if (ep_ctx->cfg.en) {
			nrf_usbd_common_ep_enable(ep_addr_to_nrfx(ep_ctx->cfg.addr));
		}
	}
}

/**
 * @brief Reset endpoint state.
 *
 * Resets the internal logic state for a given endpoint.
 *
 * @param[in]  ep_cts   Endpoint structure control block
 */
static void ep_ctx_reset(struct nrf_usbd_ep_ctx *ep_ctx)
{
	ep_ctx->buf.data = ep_ctx->buf.block.data;
	ep_ctx->buf.curr = ep_ctx->buf.data;
	ep_ctx->buf.len  = 0U;

	/* Abort ongoing write operation. */
	if (ep_ctx->write_in_progress) {
		nrf_usbd_common_ep_abort(ep_addr_to_nrfx(ep_ctx->cfg.addr));
	}

	ep_ctx->read_complete = true;
	ep_ctx->read_pending = false;
	ep_ctx->write_in_progress = false;
	ep_ctx->trans_zlp = false;
}

/**
 * @brief Initialize all endpoint structures.
 *
 * Endpoint buffers are allocated during the first call of this function.
 * This function may also be called again on every USB reset event
 * to reinitialize the state of all endpoints.
 */
static int eps_ctx_init(void)
{
	struct nrf_usbd_ep_ctx *ep_ctx;
	uint32_t i;

	for (i = 0U; i < CFG_EPIN_CNT; i++) {
		ep_ctx = in_endpoint_ctx(i);
		__ASSERT_NO_MSG(ep_ctx);
		ep_ctx_reset(ep_ctx);
	}

	for (i = 0U; i < CFG_EPOUT_CNT; i++) {
		ep_ctx = out_endpoint_ctx(i);
		__ASSERT_NO_MSG(ep_ctx);

		if (!ep_ctx->buf.block.data) {
			ep_ctx->buf.block.data = ep_out_bufs[i];
		}

		ep_ctx_reset(ep_ctx);
	}

	if (CFG_EP_ISOIN_CNT) {
		ep_ctx = in_endpoint_ctx(NRF_USBD_EPIN(8));
		__ASSERT_NO_MSG(ep_ctx);
		ep_ctx_reset(ep_ctx);
	}

	if (CFG_EP_ISOOUT_CNT) {
		BUILD_ASSERT(CFG_EP_ISOOUT_CNT <= 1);

		ep_ctx = out_endpoint_ctx(NRF_USBD_EPOUT(8));
		__ASSERT_NO_MSG(ep_ctx);

		if (!ep_ctx->buf.block.data) {
			ep_ctx->buf.block.data = ep_isoout_bufs[0];
		}

		ep_ctx_reset(ep_ctx);
	}

	return 0;
}

static inline void usbd_work_process_pwr_events(struct usbd_pwr_event *pwr_evt)
{
	struct nrf_usbd_ctx *ctx = get_usbd_ctx();
	int err;

	switch (pwr_evt->state) {
	case USBD_ATTACHED:
		if (!nrf_usbd_common_is_enabled()) {
			LOG_DBG("USB detected");
			nrf_usbd_common_enable();
			err = hfxo_start(ctx);
			__ASSERT_NO_MSG(err >= 0);
		}

		/* No callback here.
		 * Stack will be notified when the peripheral is ready.
		 */
		break;

	case USBD_POWERED:
		usbd_enable_endpoints(ctx);
		nrf_usbd_common_start(IS_ENABLED(CONFIG_USB_DEVICE_SOF));
		ctx->ready = true;

		LOG_DBG("USB Powered");

		if (ctx->status_cb) {
			ctx->status_cb(USB_DC_CONNECTED, NULL);
		}
		break;

	case USBD_DETACHED:
		ctx->ready = false;
		nrf_usbd_common_disable();
		err = hfxo_stop(ctx);
		__ASSERT_NO_MSG(err >= 0);

		LOG_DBG("USB Removed");

		if (ctx->status_cb) {
			ctx->status_cb(USB_DC_DISCONNECTED, NULL);
		}
		break;

	case USBD_SUSPENDED:
		if (dev_ready()) {
			nrf_usbd_common_suspend();
			LOG_DBG("USB Suspend state");

			if (ctx->status_cb) {
				ctx->status_cb(USB_DC_SUSPEND, NULL);
			}
		}
		break;
	case USBD_RESUMED:
		if (ctx->status_cb && dev_ready()) {
			LOG_DBG("USB resume");
			ctx->status_cb(USB_DC_RESUME, NULL);
		}
		break;

	default:
		break;
	}
}

static inline void usbd_work_process_setup(struct nrf_usbd_ep_ctx *ep_ctx)
{
	__ASSERT_NO_MSG(ep_ctx);
	__ASSERT(ep_ctx->cfg.type == USB_DC_EP_CONTROL,
		 "Invalid event on CTRL EP.");

	struct usb_setup_packet *usbd_setup;

	/* SETUP packets are handled by USBD hardware.
	 * For compatibility with the USB stack,
	 * SETUP packet must be reassembled.
	 */
	usbd_setup = (struct usb_setup_packet *)ep_ctx->buf.data;
	memset(usbd_setup, 0, sizeof(struct usb_setup_packet));
	usbd_setup->bmRequestType = nrf_usbd_setup_bmrequesttype_get(NRF_USBD);
	usbd_setup->bRequest = nrf_usbd_setup_brequest_get(NRF_USBD);
	usbd_setup->wValue = nrf_usbd_setup_wvalue_get(NRF_USBD);
	usbd_setup->wIndex = nrf_usbd_setup_windex_get(NRF_USBD);
	usbd_setup->wLength = nrf_usbd_setup_wlength_get(NRF_USBD);
	ep_ctx->buf.len = sizeof(struct usb_setup_packet);

	/* Copy setup packet to driver internal structure */
	memcpy(&usbd_ctx.setup, usbd_setup, sizeof(struct usb_setup_packet));

	LOG_DBG("SETUP: bR:0x%02x bmRT:0x%02x wV:0x%04x wI:0x%04x wL:%d",
		(uint32_t)usbd_setup->bRequest,
		(uint32_t)usbd_setup->bmRequestType,
		(uint32_t)usbd_setup->wValue,
		(uint32_t)usbd_setup->wIndex,
		(uint32_t)usbd_setup->wLength);

	/* Inform the stack. */
	ep_ctx->cfg.cb(ep_ctx->cfg.addr, USB_DC_EP_SETUP);

	struct nrf_usbd_ctx *ctx = get_usbd_ctx();

	if (usb_reqtype_is_to_device(usbd_setup) && usbd_setup->wLength) {
		ctx->ctrl_read_len = usbd_setup->wLength;
		/* Allow data chunk on EP0 OUT */
		nrf_usbd_common_setup_data_clear();
	} else {
		ctx->ctrl_read_len = 0U;
	}
}

static inline void usbd_work_process_recvreq(struct nrf_usbd_ctx *ctx,
					     struct nrf_usbd_ep_ctx *ep_ctx)
{
	if (!ep_ctx->read_pending) {
		return;
	}
	if (!ep_ctx->read_complete) {
		return;
	}

	ep_ctx->read_pending = false;
	ep_ctx->read_complete = false;

	k_mutex_lock(&ctx->drv_lock, K_FOREVER);
	NRF_USBD_COMMON_TRANSFER_OUT(transfer, ep_ctx->buf.data,
				     ep_ctx->cfg.max_sz);
	nrfx_err_t err = nrf_usbd_common_ep_transfer(
		ep_addr_to_nrfx(ep_ctx->cfg.addr), &transfer);
	if (err != NRFX_SUCCESS) {
		LOG_ERR("nRF USBD transfer error (OUT): 0x%02x", err);
	}
	k_mutex_unlock(&ctx->drv_lock);
}


static inline void usbd_work_process_ep_events(struct usbd_ep_event *ep_evt)
{
	struct nrf_usbd_ctx *ctx = get_usbd_ctx();
	struct nrf_usbd_ep_ctx *ep_ctx = ep_evt->ep;

	__ASSERT_NO_MSG(ep_ctx);

	switch (ep_evt->evt_type) {
	case EP_EVT_SETUP_RECV:
		usbd_work_process_setup(ep_ctx);
		break;

	case EP_EVT_RECV_REQ:
		usbd_work_process_recvreq(ctx, ep_ctx);
		break;

	case EP_EVT_RECV_COMPLETE:
		ep_ctx->cfg.cb(ep_ctx->cfg.addr,
			       USB_DC_EP_DATA_OUT);
		break;

	case EP_EVT_WRITE_COMPLETE:
		if (ep_ctx->cfg.type == USB_DC_EP_CONTROL &&
		    !ep_ctx->trans_zlp) {
			/* Trigger the hardware to perform
			 * status stage, but only if there is
			 * no ZLP required.
			 */
			k_mutex_lock(&ctx->drv_lock, K_FOREVER);
			nrf_usbd_common_setup_clear();
			k_mutex_unlock(&ctx->drv_lock);
		}
		ep_ctx->cfg.cb(ep_ctx->cfg.addr,
			       USB_DC_EP_DATA_IN);
		break;
	default:
		break;
	}
}

static void usbd_event_transfer_ctrl(nrf_usbd_common_evt_t const *const p_event)
{
	struct nrf_usbd_ep_ctx *ep_ctx =
		endpoint_ctx(p_event->data.eptransfer.ep);

	if (NRF_USBD_EPIN_CHECK(p_event->data.eptransfer.ep)) {
		switch (p_event->data.eptransfer.status) {
		case NRF_USBD_COMMON_EP_OK: {
			struct usbd_event *ev = usbd_evt_alloc();

			if (!ev) {
				return;
			}

			ep_ctx->write_in_progress = false;
			ev->evt_type = USBD_EVT_EP;
			ev->evt.ep_evt.evt_type = EP_EVT_WRITE_COMPLETE;
			ev->evt.ep_evt.ep = ep_ctx;

			LOG_DBG("ctrl write complete");
			usbd_evt_put(ev);
			usbd_work_schedule();
		}
		break;

		case NRF_USBD_COMMON_EP_ABORTED: {
			LOG_DBG("Endpoint 0x%02x write aborted",
				p_event->data.eptransfer.ep);
		}
		break;

		default: {
			LOG_ERR("Unexpected event (nrfx_usbd): %d, ep 0x%02x",
				p_event->data.eptransfer.status,
				p_event->data.eptransfer.ep);
		}
		break;
		}
	} else {
		switch (p_event->data.eptransfer.status) {
		case NRF_USBD_COMMON_EP_WAITING: {
			struct usbd_event *ev = usbd_evt_alloc();

			if (!ev) {
				return;
			}

			LOG_DBG("ctrl read request");

			ep_ctx->read_pending = true;
			ev->evt_type = USBD_EVT_EP;
			ev->evt.ep_evt.evt_type = EP_EVT_RECV_REQ;
			ev->evt.ep_evt.ep = ep_ctx;

			usbd_evt_put(ev);
			usbd_work_schedule();
		}
		break;

		case NRF_USBD_COMMON_EP_OK: {
			struct nrf_usbd_ctx *ctx = get_usbd_ctx();
			struct usbd_event *ev = usbd_evt_alloc();

			if (!ev) {
				return;
			}
			nrf_usbd_common_ep_status_t err_code;

			ev->evt_type = USBD_EVT_EP;
			ev->evt.ep_evt.evt_type = EP_EVT_RECV_COMPLETE;
			ev->evt.ep_evt.ep = ep_ctx;

			err_code = nrf_usbd_common_ep_status_get(
				p_event->data.eptransfer.ep, &ep_ctx->buf.len);

			if (err_code != NRF_USBD_COMMON_EP_OK) {
				LOG_ERR("_ep_status_get failed! Code: %d",
					err_code);
				__ASSERT_NO_MSG(0);
			}
			LOG_DBG("ctrl read done: %d", ep_ctx->buf.len);

			if (ctx->ctrl_read_len > ep_ctx->buf.len) {
				ctx->ctrl_read_len -= ep_ctx->buf.len;
				/* Allow next data chunk on EP0 OUT */
				nrf_usbd_common_setup_data_clear();
			} else {
				ctx->ctrl_read_len = 0U;
			}

			usbd_evt_put(ev);
			usbd_work_schedule();
		}
		break;

		default: {
			LOG_ERR("Unexpected event (nrfx_usbd): %d, ep 0x%02x",
				p_event->data.eptransfer.status,
				p_event->data.eptransfer.ep);
		}
		break;
		}
	}
}

static void usbd_event_transfer_data(nrf_usbd_common_evt_t const *const p_event)
{
	struct nrf_usbd_ep_ctx *ep_ctx =
		endpoint_ctx(p_event->data.eptransfer.ep);

	if (NRF_USBD_EPIN_CHECK(p_event->data.eptransfer.ep)) {
		switch (p_event->data.eptransfer.status) {
		case NRF_USBD_COMMON_EP_OK: {
			struct usbd_event *ev = usbd_evt_alloc();

			if (!ev) {
				return;
			}

			LOG_DBG("write complete, ep 0x%02x",
				(uint32_t)p_event->data.eptransfer.ep);

			ep_ctx->write_in_progress = false;
			ev->evt_type = USBD_EVT_EP;
			ev->evt.ep_evt.evt_type = EP_EVT_WRITE_COMPLETE;
			ev->evt.ep_evt.ep = ep_ctx;
			usbd_evt_put(ev);
			usbd_work_schedule();
		}
		break;

		case NRF_USBD_COMMON_EP_ABORTED: {
			LOG_DBG("Endpoint 0x%02x write aborted",
				p_event->data.eptransfer.ep);
		}
		break;

		default: {
			LOG_ERR("Unexpected event (nrfx_usbd): %d, ep 0x%02x",
				p_event->data.eptransfer.status,
				p_event->data.eptransfer.ep);
		}
		break;
		}

	} else {
		switch (p_event->data.eptransfer.status) {
		case NRF_USBD_COMMON_EP_WAITING: {
			struct usbd_event *ev = usbd_evt_alloc();

			if (!ev) {
				return;
			}

			LOG_DBG("read request, ep 0x%02x",
				(uint32_t)p_event->data.eptransfer.ep);

			ep_ctx->read_pending = true;
			ev->evt_type = USBD_EVT_EP;
			ev->evt.ep_evt.evt_type = EP_EVT_RECV_REQ;
			ev->evt.ep_evt.ep = ep_ctx;

			usbd_evt_put(ev);
			usbd_work_schedule();
		}
		break;

		case NRF_USBD_COMMON_EP_OK: {
			struct usbd_event *ev = usbd_evt_alloc();

			if (!ev) {
				return;
			}

			ep_ctx->buf.len = nrf_usbd_ep_amount_get(NRF_USBD,
				p_event->data.eptransfer.ep);

			LOG_DBG("read complete, ep 0x%02x, len %d",
				(uint32_t)p_event->data.eptransfer.ep,
				ep_ctx->buf.len);

			ev->evt_type = USBD_EVT_EP;
			ev->evt.ep_evt.evt_type = EP_EVT_RECV_COMPLETE;
			ev->evt.ep_evt.ep = ep_ctx;

			usbd_evt_put(ev);
			usbd_work_schedule();
		}
		break;

		default: {
			LOG_ERR("Unexpected event (nrfx_usbd): %d, ep 0x%02x",
				p_event->data.eptransfer.status,
				p_event->data.eptransfer.ep);
		}
		break;
		}
	}
}

/**
 * @brief nRFx USBD driver event handler function.
 */
static void usbd_event_handler(nrf_usbd_common_evt_t const *const p_event)
{
	struct usbd_event evt = {0};
	bool put_evt = false;

	switch (p_event->type) {
	case NRF_USBD_COMMON_EVT_SUSPEND:
		LOG_DBG("SUSPEND state detected");
		evt.evt_type = USBD_EVT_POWER;
		evt.evt.pwr_evt.state = USBD_SUSPENDED;
		put_evt = true;
		break;
	case NRF_USBD_COMMON_EVT_RESUME:
		LOG_DBG("RESUMING from suspend");
		evt.evt_type = USBD_EVT_POWER;
		evt.evt.pwr_evt.state = USBD_RESUMED;
		put_evt = true;
		break;
	case NRF_USBD_COMMON_EVT_WUREQ:
		LOG_DBG("RemoteWU initiated");
		evt.evt_type = USBD_EVT_POWER;
		evt.evt.pwr_evt.state = USBD_RESUMED;
		put_evt = true;
		break;
	case NRF_USBD_COMMON_EVT_RESET:
		evt.evt_type = USBD_EVT_RESET;
		put_evt = true;
		break;
	case NRF_USBD_COMMON_EVT_SOF:
		if (IS_ENABLED(CONFIG_USB_DEVICE_SOF)) {
			evt.evt_type = USBD_EVT_SOF;
			put_evt = true;
		}
		break;

	case NRF_USBD_COMMON_EVT_EPTRANSFER: {
		struct nrf_usbd_ep_ctx *ep_ctx;

		ep_ctx = endpoint_ctx(p_event->data.eptransfer.ep);
		switch (ep_ctx->cfg.type) {
		case USB_DC_EP_CONTROL:
			usbd_event_transfer_ctrl(p_event);
			break;
		case USB_DC_EP_BULK:
		case USB_DC_EP_INTERRUPT:
			usbd_event_transfer_data(p_event);
			break;
		case USB_DC_EP_ISOCHRONOUS:
			usbd_event_transfer_data(p_event);
			break;
		default:
			break;
		}
		break;
	}

	case NRF_USBD_COMMON_EVT_SETUP: {
		nrf_usbd_common_setup_t drv_setup;

		nrf_usbd_common_setup_get(&drv_setup);
		if ((drv_setup.bRequest != USB_SREQ_SET_ADDRESS)
		    || (USB_REQTYPE_GET_TYPE(drv_setup.bmRequestType)
			!= USB_REQTYPE_TYPE_STANDARD)) {
			/* SetAddress is handled by USBD hardware.
			 * No software action required.
			 */

			struct nrf_usbd_ep_ctx *ep_ctx =
				endpoint_ctx(NRF_USBD_EPOUT(0));

			evt.evt_type = USBD_EVT_EP;
			evt.evt.ep_evt.ep = ep_ctx;
			evt.evt.ep_evt.evt_type = EP_EVT_SETUP_RECV;
			put_evt = true;
		}
		break;
	}

	default:
		break;
	}

	if (put_evt) {
		struct usbd_event *ev;

		ev = usbd_evt_alloc();
		if (!ev) {
			return;
		}
		ev->evt_type = evt.evt_type;
		ev->evt = evt.evt;
		usbd_evt_put(ev);
		usbd_work_schedule();
	}
}

static inline void usbd_reinit(void)
{
	int ret;
	nrfx_err_t err;

	nrfx_power_usbevt_disable();
	nrf_usbd_common_disable();
	nrf_usbd_common_uninit();

	usbd_evt_flush();

	ret = eps_ctx_init();
	__ASSERT_NO_MSG(ret == 0);

	nrfx_power_usbevt_enable();
	err = nrf_usbd_common_init(usbd_event_handler);

	if (err != NRFX_SUCCESS) {
		LOG_DBG("nRF USBD driver reinit failed. Code: %d", err);
		__ASSERT_NO_MSG(0);
	}
}

/**
 * @brief function to generate fake receive request for
 * ISO OUT EP.
 *
 * ISO OUT endpoint does not generate irq by itself and reading
 * from ISO OUT ep is synchronized with SOF frame. For more details
 * refer to Nordic usbd specification.
 */
static void usbd_sof_trigger_iso_read(void)
{
	struct usbd_event *ev;
	struct nrf_usbd_ep_ctx *ep_ctx;

	ep_ctx = endpoint_ctx(NRF_USBD_COMMON_EPOUT8);
	if (!ep_ctx) {
		LOG_ERR("There is no ISO ep");
		return;
	}

	if (ep_ctx->cfg.en) {
		/* Dissect receive request
		 * if the iso OUT ep is enabled
		 */
		ep_ctx->read_pending = true;
		ep_ctx->read_complete = true;
		ev = usbd_evt_alloc();
		if (!ev) {
			LOG_ERR("Failed to alloc evt");
			return;
		}
		ev->evt_type = USBD_EVT_EP;
		ev->evt.ep_evt.evt_type = EP_EVT_RECV_REQ;
		ev->evt.ep_evt.ep = ep_ctx;
		usbd_evt_put(ev);
		usbd_work_schedule();
	} else {
		LOG_DBG("Endpoint is not enabled");
	}
}

/* Work handler */
static void usbd_work_handler(struct k_work *item)
{
	struct nrf_usbd_ctx *ctx;
	struct usbd_event *ev;

	ctx = CONTAINER_OF(item, struct nrf_usbd_ctx, usb_work);

	while ((ev = usbd_evt_get()) != NULL) {
		if (!dev_ready() && ev->evt_type != USBD_EVT_POWER) {
			/* Drop non-power events when cable is detached. */
			usbd_evt_free(ev);
			continue;
		}

		switch (ev->evt_type) {
		case USBD_EVT_EP:
			if (!ctx->attached) {
				LOG_ERR("not attached, EP 0x%02x event dropped",
					(uint32_t)ev->evt.ep_evt.ep->cfg.addr);
			}
			usbd_work_process_ep_events(&ev->evt.ep_evt);
			break;
		case USBD_EVT_POWER:
			usbd_work_process_pwr_events(&ev->evt.pwr_evt);
			break;
		case USBD_EVT_RESET:
			LOG_DBG("USBD reset event");
			k_mutex_lock(&ctx->drv_lock, K_FOREVER);
			eps_ctx_init();
			k_mutex_unlock(&ctx->drv_lock);

			if (ctx->status_cb) {
				ctx->status_cb(USB_DC_RESET, NULL);
			}
			break;
		case USBD_EVT_SOF:
			usbd_sof_trigger_iso_read();

			if (ctx->status_cb) {
				ctx->status_cb(USB_DC_SOF, NULL);
			}
			break;
		case USBD_EVT_REINIT: {
				/*
				 * Reinitialize the peripheral after queue
				 * overflow.
				 */
				LOG_ERR("USBD event queue full!");
				usbd_reinit();
				break;
			}
		default:
			LOG_ERR("Unknown USBD event: %"PRId16, ev->evt_type);
			break;
		}
		usbd_evt_free(ev);
	}
}

int usb_dc_attach(void)
{
	struct nrf_usbd_ctx *ctx = get_usbd_ctx();
	int ret;

	if (ctx->attached) {
		return 0;
	}

	k_mutex_init(&ctx->drv_lock);
	ctx->hfxo_mgr =
		z_nrf_clock_control_get_onoff(
			COND_CODE_1(NRF_CLOCK_HAS_HFCLK192M,
				    (CLOCK_CONTROL_NRF_SUBSYS_HF192M),
				    (CLOCK_CONTROL_NRF_SUBSYS_HF)));

	IRQ_CONNECT(DT_INST_IRQN(0), DT_INST_IRQ(0, priority),
		    nrfx_isr, nrf_usbd_common_irq_handler, 0);

	nrfx_power_usbevt_enable();

	ret = eps_ctx_init();
	if (ret == 0) {
		ctx->attached = true;
	}

	if (!k_fifo_is_empty(&usbd_evt_fifo)) {
		usbd_work_schedule();
	}

	if (nrfx_power_usbstatus_get() != NRFX_POWER_USB_STATE_DISCONNECTED) {
		/* USBDETECTED event is be generated on cable attachment and
		 * when cable is already attached during reset, but not when
		 * the peripheral is re-enabled.
		 * When USB-enabled bootloader is used, target application
		 * will not receive this event and it needs to be generated
		 * again here.
		 */
		usb_dc_power_event_handler(NRFX_POWER_USB_EVT_DETECTED);
	}

	return ret;
}

int usb_dc_detach(void)
{
	struct nrf_usbd_ctx *ctx = get_usbd_ctx();

	k_mutex_lock(&ctx->drv_lock, K_FOREVER);

	usbd_evt_flush();

	if (nrf_usbd_common_is_enabled()) {
		nrf_usbd_common_disable();
	}

	(void)hfxo_stop(ctx);
	nrfx_power_usbevt_disable();

	ctx->attached = false;
	k_mutex_unlock(&ctx->drv_lock);

	return 0;
}

int usb_dc_reset(void)
{
	int ret;

	if (!dev_attached() || !dev_ready()) {
		return -ENODEV;
	}

	LOG_DBG("USBD Reset");

	ret = usb_dc_detach();
	if (ret) {
		return ret;
	}

	ret = usb_dc_attach();
	if (ret) {
		return ret;
	}

	return 0;
}

int usb_dc_set_address(const uint8_t addr)
{
	struct nrf_usbd_ctx *ctx;

	if (!dev_attached() || !dev_ready()) {
		return -ENODEV;
	}

	/**
	 * Nothing to do here. The USBD HW already takes care of initiating
	 * STATUS stage. Just double check the address for sanity.
	 */
	__ASSERT(addr == (uint8_t)NRF_USBD->USBADDR, "USB Address incorrect!");

	ctx = get_usbd_ctx();

	LOG_DBG("Address set to: %d", addr);

	return 0;
}


int usb_dc_ep_check_cap(const struct usb_dc_ep_cfg_data *const ep_cfg)
{
	uint8_t ep_idx = NRF_USBD_EP_NR_GET(ep_cfg->ep_addr);

	LOG_DBG("ep 0x%02x, mps %d, type %d", ep_cfg->ep_addr, ep_cfg->ep_mps,
		ep_cfg->ep_type);

	if ((ep_cfg->ep_type == USB_DC_EP_CONTROL) && ep_idx) {
		LOG_ERR("invalid endpoint configuration");
		return -1;
	}

	if (!NRF_USBD_EP_VALIDATE(ep_cfg->ep_addr)) {
		LOG_ERR("invalid endpoint index/address");
		return -1;
	}

	if ((ep_cfg->ep_type == USB_DC_EP_ISOCHRONOUS) &&
	    (!NRF_USBD_EPISO_CHECK(ep_cfg->ep_addr))) {
		LOG_WRN("invalid endpoint type");
		return -1;
	}

	if ((ep_cfg->ep_type != USB_DC_EP_ISOCHRONOUS) &&
	    (NRF_USBD_EPISO_CHECK(ep_cfg->ep_addr))) {
		LOG_WRN("iso endpoint can only be iso");
		return -1;
	}

	return 0;
}

int usb_dc_ep_configure(const struct usb_dc_ep_cfg_data *const ep_cfg)
{
	struct nrf_usbd_ep_ctx *ep_ctx;

	if (!dev_attached()) {
		return -ENODEV;
	}

	/**
	 * TODO:
	 * For ISO endpoints, application has to use EPIN/OUT 8
	 * but right now there's no standard way of knowing the
	 * ISOIN/ISOOUT endpoint number in advance to configure
	 * accordingly. So either this needs to be chosen in the
	 * menuconfig in application area or perhaps in device tree
	 * at compile time or introduce a new API to read the endpoint
	 * configuration at runtime before configuring them.
	 */
	ep_ctx = endpoint_ctx(ep_cfg->ep_addr);
	if (!ep_ctx) {
		return -EINVAL;
	}

	ep_ctx->cfg.addr = ep_cfg->ep_addr;
	ep_ctx->cfg.type = ep_cfg->ep_type;
	ep_ctx->cfg.max_sz = ep_cfg->ep_mps;

	if (!NRF_USBD_EPISO_CHECK(ep_cfg->ep_addr)) {
		if ((ep_cfg->ep_mps & (ep_cfg->ep_mps - 1)) != 0U) {
			LOG_ERR("EP max packet size must be a power of 2");
			return -EINVAL;
		}
	}

	nrf_usbd_common_ep_max_packet_size_set(ep_addr_to_nrfx(ep_cfg->ep_addr),
					       ep_cfg->ep_mps);

	return 0;
}

int usb_dc_ep_set_stall(const uint8_t ep)
{
	struct nrf_usbd_ep_ctx *ep_ctx;

	if (!dev_attached() || !dev_ready()) {
		return -ENODEV;
	}

	ep_ctx = endpoint_ctx(ep);
	if (!ep_ctx) {
		return -EINVAL;
	}

	switch (ep_ctx->cfg.type) {
	case USB_DC_EP_CONTROL:
		nrf_usbd_common_setup_stall();
		break;
	case USB_DC_EP_BULK:
	case USB_DC_EP_INTERRUPT:
		nrf_usbd_common_ep_stall(ep_addr_to_nrfx(ep));
		break;
	case USB_DC_EP_ISOCHRONOUS:
		LOG_ERR("STALL unsupported on ISO endpoint");
		return -EINVAL;
	}

	ep_ctx->buf.len = 0U;
	ep_ctx->buf.curr = ep_ctx->buf.data;

	LOG_DBG("STALL on EP 0x%02x", ep);

	return 0;
}

int usb_dc_ep_clear_stall(const uint8_t ep)
{

	struct nrf_usbd_ep_ctx *ep_ctx;

	if (!dev_attached() || !dev_ready()) {
		return -ENODEV;
	}

	ep_ctx = endpoint_ctx(ep);
	if (!ep_ctx) {
		return -EINVAL;
	}

	if (NRF_USBD_EPISO_CHECK(ep)) {
		/* ISO transactions do not support a handshake phase. */
		return -EINVAL;
	}

	nrf_usbd_common_ep_dtoggle_clear(ep_addr_to_nrfx(ep));
	nrf_usbd_common_ep_stall_clear(ep_addr_to_nrfx(ep));
	LOG_DBG("Unstall on EP 0x%02x", ep);

	return 0;
}

int usb_dc_ep_halt(const uint8_t ep)
{
	return usb_dc_ep_set_stall(ep);
}

int usb_dc_ep_is_stalled(const uint8_t ep, uint8_t *const stalled)
{
	struct nrf_usbd_ep_ctx *ep_ctx;

	if (!dev_attached() || !dev_ready()) {
		return -ENODEV;
	}

	ep_ctx = endpoint_ctx(ep);
	if (!ep_ctx) {
		return -EINVAL;
	}

	if (!stalled) {
		return -EINVAL;
	}

	*stalled = (uint8_t) nrf_usbd_common_ep_stall_check(ep_addr_to_nrfx(ep));

	return 0;
}

int usb_dc_ep_enable(const uint8_t ep)
{
	struct nrf_usbd_ep_ctx *ep_ctx;

	if (!dev_attached()) {
		return -ENODEV;
	}

	ep_ctx = endpoint_ctx(ep);
	if (!ep_ctx) {
		return -EINVAL;
	}

	if (!NRF_USBD_EPISO_CHECK(ep)) {
		/* ISO transactions for full-speed device do not support
		 * toggle sequencing and should only send DATA0 PID.
		 */
		nrf_usbd_common_ep_dtoggle_clear(ep_addr_to_nrfx(ep));
		/** Endpoint is enabled on SetInterface request.
		 * This should also clear EP's halt status.
		 */
		nrf_usbd_common_ep_stall_clear(ep_addr_to_nrfx(ep));
	}
	if (ep_ctx->cfg.en) {
		return -EALREADY;
	}

	LOG_DBG("EP enable: 0x%02x", ep);

	ep_ctx->cfg.en = true;

	/* Defer the endpoint enable if USBD is not ready yet. */
	if (dev_ready()) {
		nrf_usbd_common_ep_enable(ep_addr_to_nrfx(ep));
	}

	return 0;
}

int usb_dc_ep_disable(const uint8_t ep)
{
	struct nrf_usbd_ep_ctx *ep_ctx;

	ep_ctx = endpoint_ctx(ep);
	if (!ep_ctx) {
		return -EINVAL;
	}

	if (!ep_ctx->cfg.en) {
		return -EALREADY;
	}

	LOG_DBG("EP disable: 0x%02x", ep);

	nrf_usbd_common_ep_disable(ep_addr_to_nrfx(ep));
	/* Clear write_in_progress as nrf_usbd_common_ep_disable()
	 * terminates endpoint transaction.
	 */
	ep_ctx->write_in_progress = false;
	ep_ctx_reset(ep_ctx);
	ep_ctx->cfg.en = false;

	return 0;
}

int usb_dc_ep_flush(const uint8_t ep)
{
	struct nrf_usbd_ep_ctx *ep_ctx;

	if (!dev_attached() || !dev_ready()) {
		return -ENODEV;
	}

	ep_ctx = endpoint_ctx(ep);
	if (!ep_ctx) {
		return -EINVAL;
	}

	ep_ctx->buf.len = 0U;
	ep_ctx->buf.curr = ep_ctx->buf.data;

	nrf_usbd_common_transfer_out_drop(ep_addr_to_nrfx(ep));

	return 0;
}

int usb_dc_ep_write(const uint8_t ep, const uint8_t *const data,
		    const uint32_t data_len, uint32_t *const ret_bytes)
{
	LOG_DBG("ep_write: ep 0x%02x, len %d", ep, data_len);
	struct nrf_usbd_ctx *ctx = get_usbd_ctx();
	struct nrf_usbd_ep_ctx *ep_ctx;
	int result = 0;

	if (!dev_attached() || !dev_ready()) {
		return -ENODEV;
	}

	if (NRF_USBD_EPOUT_CHECK(ep)) {
		return -EINVAL;
	}

	ep_ctx = endpoint_ctx(ep);
	if (!ep_ctx) {
		return -EINVAL;
	}

	if (!ep_ctx->cfg.en) {
		LOG_ERR("Endpoint 0x%02x is not enabled", ep);
		return -EINVAL;
	}

	k_mutex_lock(&ctx->drv_lock, K_FOREVER);

	/* USBD driver does not allow scheduling multiple DMA transfers
	 * for one EP at a time. Next USB transfer on this endpoint can be
	 * triggered after the completion of previous one.
	 */
	if (ep_ctx->write_in_progress) {
		k_mutex_unlock(&ctx->drv_lock);
		return -EAGAIN;
	}

	/** Clear the ZLP flag if current write is ZLP. After the ZLP will be
	 * send the driver will perform status stage.
	 */
	if (!data_len && ep_ctx->trans_zlp) {
		ep_ctx->trans_zlp = false;
	}

	/** If writing to a Control Endpoint there might be a need to transfer
	 * ZLP. If the Hosts asks for more data that the device may return and
	 * the last packet is wMaxPacketSize long. The driver must send ZLP.
	 * For consistence with the Zephyr USB stack sending ZLP must be issued
	 * from the stack level. Making trans_zlp flag true results in blocking
	 * the driver from starting setup stage without required ZLP.
	 */
	if (ep_ctx->cfg.type == USB_DC_EP_CONTROL) {
		if (data_len && usbd_ctx.setup.wLength > data_len &&
		    !(data_len % ep_ctx->cfg.max_sz)) {
			ep_ctx->trans_zlp = true;
		}
	}

	/* Setup stage is handled by hardware.
	 * Detect the setup stage initiated by the stack
	 * and perform appropriate action.
	 */
	if ((ep_ctx->cfg.type == USB_DC_EP_CONTROL)
	    && (nrf_usbd_common_last_setup_dir_get() != ep)) {
		nrf_usbd_common_setup_clear();
		k_mutex_unlock(&ctx->drv_lock);
		return 0;
	}

	ep_ctx->write_in_progress = true;
	NRF_USBD_COMMON_TRANSFER_IN(transfer, data, data_len, 0);
	nrfx_err_t err = nrf_usbd_common_ep_transfer(ep_addr_to_nrfx(ep), &transfer);

	if (err != NRFX_SUCCESS) {
		ep_ctx->write_in_progress = false;
		if (ret_bytes) {
			*ret_bytes = 0;
		}
		result = -EIO;
		LOG_ERR("nRF USBD write error: %d", (uint32_t)err);
	} else {
		if (ret_bytes) {
			*ret_bytes = data_len;
		}
	}

	k_mutex_unlock(&ctx->drv_lock);
	return result;
}

int usb_dc_ep_read_wait(uint8_t ep, uint8_t *data, uint32_t max_data_len,
			uint32_t *read_bytes)
{
	struct nrf_usbd_ep_ctx *ep_ctx;
	struct nrf_usbd_ctx *ctx = get_usbd_ctx();
	uint32_t bytes_to_copy;

	if (!dev_attached() || !dev_ready()) {
		return -ENODEV;
	}

	if (NRF_USBD_EPIN_CHECK(ep)) {
		return -EINVAL;
	}

	if (!data && max_data_len) {
		return -EINVAL;
	}

	ep_ctx = endpoint_ctx(ep);
	if (!ep_ctx) {
		return -EINVAL;
	}

	if (!ep_ctx->cfg.en) {
		LOG_ERR("Endpoint 0x%02x is not enabled", ep);
		return -EINVAL;
	}

	k_mutex_lock(&ctx->drv_lock, K_FOREVER);

	bytes_to_copy = MIN(max_data_len, ep_ctx->buf.len);

	if (!data && !max_data_len) {
		if (read_bytes) {
			*read_bytes = ep_ctx->buf.len;
		}
		k_mutex_unlock(&ctx->drv_lock);
		return 0;
	}

	memcpy(data, ep_ctx->buf.curr, bytes_to_copy);

	ep_ctx->buf.curr += bytes_to_copy;
	ep_ctx->buf.len -= bytes_to_copy;
	if (read_bytes) {
		*read_bytes = bytes_to_copy;
	}

	k_mutex_unlock(&ctx->drv_lock);
	return 0;
}

int usb_dc_ep_read_continue(uint8_t ep)
{
	struct nrf_usbd_ep_ctx *ep_ctx;
	struct nrf_usbd_ctx *ctx = get_usbd_ctx();

	if (!dev_attached() || !dev_ready()) {
		return -ENODEV;
	}

	if (NRF_USBD_EPIN_CHECK(ep)) {
		return -EINVAL;
	}

	ep_ctx = endpoint_ctx(ep);
	if (!ep_ctx) {
		return -EINVAL;
	}

	if (!ep_ctx->cfg.en) {
		LOG_ERR("Endpoint 0x%02x is not enabled", ep);
		return -EINVAL;
	}

	k_mutex_lock(&ctx->drv_lock, K_FOREVER);
	if (!ep_ctx->buf.len) {
		ep_ctx->buf.curr = ep_ctx->buf.data;
		ep_ctx->read_complete = true;

		if (ep_ctx->read_pending) {
			struct usbd_event *ev = usbd_evt_alloc();

			if (!ev) {
				k_mutex_unlock(&ctx->drv_lock);
				return -ENOMEM;
			}

			ev->evt_type = USBD_EVT_EP;
			ev->evt.ep_evt.ep = ep_ctx;
			ev->evt.ep_evt.evt_type = EP_EVT_RECV_REQ;
			usbd_evt_put(ev);
			usbd_work_schedule();
		}
	}
	k_mutex_unlock(&ctx->drv_lock);

	return 0;
}

int usb_dc_ep_read(const uint8_t ep, uint8_t *const data,
		   const uint32_t max_data_len, uint32_t *const read_bytes)
{
	LOG_DBG("ep_read: ep 0x%02x, maxlen %d", ep, max_data_len);
	int ret;

	ret = usb_dc_ep_read_wait(ep, data, max_data_len, read_bytes);
	if (ret) {
		return ret;
	}

	if (!data && !max_data_len) {
		return ret;
	}

	ret = usb_dc_ep_read_continue(ep);
	return ret;
}

int usb_dc_ep_set_callback(const uint8_t ep, const usb_dc_ep_callback cb)
{
	struct nrf_usbd_ep_ctx *ep_ctx;

	if (!dev_attached()) {
		return -ENODEV;
	}

	ep_ctx = endpoint_ctx(ep);
	if (!ep_ctx) {
		return -EINVAL;
	}

	ep_ctx->cfg.cb = cb;

	return 0;
}

void usb_dc_set_status_callback(const usb_dc_status_callback cb)
{
	get_usbd_ctx()->status_cb = cb;
}

int usb_dc_ep_mps(const uint8_t ep)
{
	struct nrf_usbd_ep_ctx *ep_ctx;

	if (!dev_attached()) {
		return -ENODEV;
	}

	ep_ctx = endpoint_ctx(ep);
	if (!ep_ctx) {
		return -EINVAL;
	}

	return ep_ctx->cfg.max_sz;
}

int usb_dc_wakeup_request(void)
{
	bool res = nrf_usbd_common_wakeup_req();

	if (!res) {
		return -EAGAIN;
	}
	return 0;
}

static int usb_init(void)
{
	struct nrf_usbd_ctx *ctx = get_usbd_ctx();
	nrfx_err_t err;

#ifdef CONFIG_HAS_HW_NRF_USBREG
	/* Use CLOCK/POWER priority for compatibility with other series where
	 * USB events are handled by CLOCK interrupt handler.
	 */
	IRQ_CONNECT(USBREGULATOR_IRQn,
		    DT_IRQ(DT_INST(0, nordic_nrf_clock), priority),
		    nrfx_isr, nrfx_usbreg_irq_handler, 0);
	irq_enable(USBREGULATOR_IRQn);
#endif

	static const nrfx_power_config_t power_config = {
		.dcdcen = IS_ENABLED(CONFIG_SOC_DCDC_NRF52X) ||
			  IS_ENABLED(CONFIG_SOC_DCDC_NRF53X_APP),
#if NRFX_POWER_SUPPORTS_DCDCEN_VDDH
		.dcdcenhv = IS_ENABLED(CONFIG_SOC_DCDC_NRF52X_HV) ||
			    IS_ENABLED(CONFIG_SOC_DCDC_NRF53X_HV),
#endif
	};

	static const nrfx_power_usbevt_config_t usbevt_config = {
		.handler = usb_dc_power_event_handler
	};

	err = nrf_usbd_common_init(usbd_event_handler);
	if (err != NRFX_SUCCESS) {
		LOG_DBG("nRF USBD driver init failed. Code: %d", (uint32_t)err);
		return -EIO;
	}

	/* Ignore the return value, as NRFX_ERROR_ALREADY_INITIALIZED is not
	 * a problem here.
	 */
	(void)nrfx_power_init(&power_config);
	nrfx_power_usbevt_init(&usbevt_config);

	k_work_queue_start(&usbd_work_queue,
			   usbd_work_queue_stack,
			   K_KERNEL_STACK_SIZEOF(usbd_work_queue_stack),
			   CONFIG_SYSTEM_WORKQUEUE_PRIORITY, NULL);

	k_thread_name_set(&usbd_work_queue.thread, "usbd_workq");
	k_work_init(&ctx->usb_work, usbd_work_handler);

	return 0;
}

SYS_INIT(usb_init, POST_KERNEL, CONFIG_KERNEL_INIT_PRIORITY_DEVICE);
