/**
  * This module is a confidential and proprietary property of RealTek and
  * possession or use of this module requires written permission of RealTek.
  *
  * Copyright(c) 2021, Realtek Semiconductor Corporation. All rights reserved.
  ******************************************************************************
  */

/* Includes ------------------------------------------------------------------ */

#include <platform_opts.h>
#if defined(CONFIG_EXAMPLE_USBD_CDC_ACM) && CONFIG_EXAMPLE_USBD_CDC_ACM
#include "usbd.h"
#include "usbd_cdc_acm.h"
#include "osdep_service.h"

/* Private defines -----------------------------------------------------------*/

// This configuration is used to enable a thread to check hotplug event
// and reset USB stack to avoid memory leak, only for example.
#define CONFIG_USBD_CDC_ACM_HOTPLUG					1

// USB speed
#ifdef CONFIG_USB_FS
#define CONFIG_USBD_CDC_ACM_SPEED					USB_SPEED_FULL
#else
#define CONFIG_USBD_CDC_ACM_SPEED					USB_SPEED_HIGH
#endif

// Echo asynchronously, for transfer size larger than packet size. While fpr
// transfer size less than packet size, the synchronous way is preferred.
#define CONFIG_USBD_CDC_ACM_ASYNC_XFER				0

// Asynchronous transfer size
#define CONFIG_CDC_ACM_ASYNC_BUF_SIZE				2048U

// Do not change the settings unless indeed necessary
#define CONFIG_CDC_ACM_BULK_IN_XFER_SIZE			2048U
#define CONFIG_CDC_ACM_BULK_OUT_XFER_SIZE			2048U

// Thread priorities
#define CONFIG_CDC_ACM_INIT_THREAD_PRIORITY			5
#define CONFIG_CDC_ACM_ISR_THREAD_PRIORITY			7
#define CONFIG_CDC_ACM_HOTPLUG_THREAD_PRIORITY		8 // Should be higher than CONFIG_CDC_ACM_ISR_THREAD_PRIORITY
#define CONFIG_CDC_ACM_XFER_THREAD_PRIORITY			5

/* Private types -------------------------------------------------------------*/

/* Private macros ------------------------------------------------------------*/

/* Private function prototypes -----------------------------------------------*/

static u8 cdc_acm_cb_init(void);
static u8 cdc_acm_cb_deinit(void);
static u8 cdc_acm_cb_setup(usb_setup_req_t *req, u8 *buf);
static u8 cdc_acm_cb_received(u8 *buf, u32 Len);
static void cdc_acm_cb_status_changed(u8 status);

/* Private variables ---------------------------------------------------------*/

static usbd_cdc_acm_cb_t cdc_acm_cb = {
	.init = cdc_acm_cb_init,
	.deinit = 	cdc_acm_cb_deinit,
	.setup = cdc_acm_cb_setup,
	.received = cdc_acm_cb_received,
	.status_changed = cdc_acm_cb_status_changed
};

static usbd_cdc_acm_line_coding_t cdc_acm_line_coding;

static u16 cdc_acm_ctrl_line_state;

static usbd_config_t cdc_acm_cfg = {
	.speed = CONFIG_USBD_CDC_ACM_SPEED,
	.dma_enable = 1U,
	.isr_priority = CONFIG_CDC_ACM_ISR_THREAD_PRIORITY,
	.intr_use_ptx_fifo = 0U,
	.nptx_max_epmis_cnt = 10U,
	.nptx_max_err_cnt = {0U, 0U, 0U, 2000U, }
};

#if CONFIG_USBD_CDC_ACM_ASYNC_XFER
static u8 cdc_acm_async_xfer_buf[CONFIG_CDC_ACM_ASYNC_BUF_SIZE];
static u16 cdc_acm_async_xfer_buf_pos = 0;
static volatile int cdc_acm_async_xfer_busy = 0;
static _sema cdc_acm_async_xfer_sema;
#endif

#if CONFIG_USBD_CDC_ACM_HOTPLUG
static u8 cdc_acm_attach_status;
static _sema cdc_acm_attach_status_changed_sema;
#endif

/* Private functions ---------------------------------------------------------*/

/**
  * @brief  Initializes the CDC media layer
  * @param  None
  * @retval Status
  */
static u8 cdc_acm_cb_init(void)
{
	usbd_cdc_acm_line_coding_t *lc = &cdc_acm_line_coding;

	lc->bitrate = 150000;
	lc->format = 0x00;
	lc->parity_type = 0x00;
	lc->data_type = 0x08;

#if CONFIG_USBD_CDC_ACM_ASYNC_XFER
	cdc_acm_async_xfer_buf_pos = 0;
	cdc_acm_async_xfer_busy = 0;
#endif

	return HAL_OK;
}

/**
  * @brief  DeInitializes the CDC media layer
  * @param  None
  * @retval Status
  */
static u8 cdc_acm_cb_deinit(void)
{
#if CONFIG_USBD_CDC_ACM_ASYNC_XFER
	cdc_acm_async_xfer_buf_pos = 0;
	cdc_acm_async_xfer_busy = 0;
#endif
	return HAL_OK;
}

/**
  * @brief  Data received over USB OUT endpoint are sent over CDC interface through this function.
  * @param  Buf: RX buffer
  * @param  Len: RX data length (in bytes)
  * @retval Status
  */
static u8 cdc_acm_cb_received(u8 *buf, u32 len)
{
#if CONFIG_USBD_CDC_ACM_ASYNC_XFER
	u8 ret = HAL_OK;
	if (0 == cdc_acm_async_xfer_busy) {
		if ((cdc_acm_async_xfer_buf_pos + len) > CONFIG_CDC_ACM_ASYNC_BUF_SIZE) {
			len = CONFIG_CDC_ACM_ASYNC_BUF_SIZE - cdc_acm_async_xfer_buf_pos;  // extra data discarded
		}

		memcpy((void *)((u32)cdc_acm_async_xfer_buf + cdc_acm_async_xfer_buf_pos), buf, len);
		cdc_acm_async_xfer_buf_pos += len;
		if (cdc_acm_async_xfer_buf_pos >= CONFIG_CDC_ACM_ASYNC_BUF_SIZE) {
			cdc_acm_async_xfer_buf_pos = 0;
			rtw_up_sema(&cdc_acm_async_xfer_sema);
		}
	} else {
		printf("\n[CDC] Busy, discarded %ld bytes\n", len);
		ret = HAL_BUSY;
	}

	return ret;
#else
	return usbd_cdc_acm_transmit(buf, len);
#endif
}

/**
  * @brief  Handle the CDC class control requests
  * @param  cmd: Command code
  * @param  buf: Buffer containing command data (request parameters)
  * @param  len: Number of data to be sent (in bytes)
  * @retval Status
  */
static u8 cdc_acm_cb_setup(usb_setup_req_t *req, u8 *buf)
{
	usbd_cdc_acm_line_coding_t *lc = &cdc_acm_line_coding;

	switch (req->bRequest) {
	case CDC_SEND_ENCAPSULATED_COMMAND:
		/* Do nothing */
		break;

	case CDC_GET_ENCAPSULATED_RESPONSE:
		/* Do nothing */
		break;

	case CDC_SET_COMM_FEATURE:
		/* Do nothing */
		break;

	case CDC_GET_COMM_FEATURE:
		/* Do nothing */
		break;

	case CDC_CLEAR_COMM_FEATURE:
		/* Do nothing */
		break;

	case CDC_SET_LINE_CODING:
		if (req->wLength == CDC_ACM_LINE_CODING_SIZE) {
			lc->bitrate = (u32)(buf[0] | (buf[1] << 8) | (buf[2] << 16) | (buf[3] << 24));
			lc->format = buf[4];
			lc->parity_type = buf[5];
			lc->data_type = buf[6];
		}
		break;

	case CDC_GET_LINE_CODING:
		buf[0] = (u8)(lc->bitrate & 0xFF);
		buf[1] = (u8)((lc->bitrate >> 8) & 0xFF);
		buf[2] = (u8)((lc->bitrate >> 16) & 0xFF);
		buf[3] = (u8)((lc->bitrate >> 24) & 0xFF);
		buf[4] = lc->format;
		buf[5] = lc->parity_type;
		buf[6] = lc->data_type;
		break;

	case CDC_SET_CONTROL_LINE_STATE:
		/*
		wValue:	wValue, Control Signal Bitmap
				D2-15:	Reserved, 0
				D1:	RTS, 0 - Deactivate, 1 - Activate
				D0:	DTR, 0 - Not Present, 1 - Present
		*/
		cdc_acm_ctrl_line_state = req->wValue;
		if (cdc_acm_ctrl_line_state & 0x01) {
			printf("\n[CDC] VCOM port activated\n");
#if CONFIG_CDC_ACM_NOTIFY
			usbd_cdc_acm_notify_serial_state(CDC_ACM_CTRL_DSR | CDC_ACM_CTRL_DCD);
#endif
		}
		break;

	case CDC_SEND_BREAK:
		/* Do nothing */
		break;

	default:
		break;
	}

	return HAL_OK;
}

static void cdc_acm_cb_status_changed(u8 status)
{
	printf("\n[CDC] USB status changed: %d\n", status);
#if CONFIG_USBD_CDC_ACM_HOTPLUG
	cdc_acm_attach_status = status;
	rtw_up_sema(&cdc_acm_attach_status_changed_sema);
#endif
}

#if CONFIG_USBD_CDC_ACM_HOTPLUG
static void cdc_acm_hotplug_thread(void *param)
{
	int ret = 0;

	UNUSED(param);

	for (;;) {
		if (rtw_down_sema(&cdc_acm_attach_status_changed_sema)) {
			if (cdc_acm_attach_status == USBD_ATTACH_STATUS_DETACHED) {
				printf("\n[CDC] USB DETACHED\n");
				usbd_cdc_acm_deinit();
				ret = usbd_deinit();
				if (ret != 0) {
					printf("\n[CDC] Fail to de-init USBD driver\n");
					break;
				}
				printf("\n[CDC] Free heap size: 0x%ld\n", rtw_getFreeHeapSize());
				ret = usbd_init(&cdc_acm_cfg);
				if (ret != 0) {
					printf("\n[CDC] Fail to re-init USBD driver\n");
					break;
				}
				ret = usbd_cdc_acm_init(CONFIG_CDC_ACM_BULK_OUT_XFER_SIZE, CONFIG_CDC_ACM_BULK_IN_XFER_SIZE, &cdc_acm_cb);
				if (ret != 0) {
					printf("\n[CDC] Fail to re-init CDC ACM class\n");
					usbd_deinit();
					break;
				}
			} else if (cdc_acm_attach_status == USBD_ATTACH_STATUS_ATTACHED) {
				printf("\n[CDC] USB ATTACHED\n");
			} else {
				printf("\n[CDC] USB INIT\n");
			}
		}
	}

	rtw_thread_exit();
}
#endif // CONFIG_USBD_MSC_CHECK_USB_STATUS

#if CONFIG_USBD_CDC_ACM_ASYNC_XFER
static void cdc_acm_xfer_thread(void *param)
{
	u8 ret;
	u8 *xfer_buf;
	u32 xfer_len;

	UNUSED(param);

	for (;;) {
		if (rtw_down_sema(&cdc_acm_async_xfer_sema)) {
			xfer_len = CONFIG_CDC_ACM_ASYNC_BUF_SIZE;
			xfer_buf = cdc_acm_async_xfer_buf;
			cdc_acm_async_xfer_busy = 1;
			printf("\n[CDC] Start transfer %d bytes\n", CONFIG_CDC_ACM_ASYNC_BUF_SIZE);
			while (xfer_len > 0) {
				if (xfer_len > CONFIG_CDC_ACM_BULK_IN_XFER_SIZE) {
					ret = usbd_cdc_acm_transmit(xfer_buf, CONFIG_CDC_ACM_BULK_IN_XFER_SIZE);
					if (ret == HAL_OK) {
						xfer_len -= CONFIG_CDC_ACM_BULK_IN_XFER_SIZE;
						xfer_buf += CONFIG_CDC_ACM_BULK_IN_XFER_SIZE;
					} else { // HAL_BUSY
						printf("\n[CDC] Busy to transmit data, retry[1]\n");
						rtw_udelay_os(200);
					}
				} else {
					ret = usbd_cdc_acm_transmit(xfer_buf, xfer_len);
					if (ret == HAL_OK) {
						xfer_len = 0;
						cdc_acm_async_xfer_busy = 0;
						printf("\n[CDC] Transmit done\n");
						break;
					} else { // HAL_BUSY
						printf("\n[CDC] Busy to transmit data, retry[2]\n");
						rtw_udelay_os(200);
					}
				}
			}
		}
	}
}
#endif

static void example_usbd_cdc_acm_thread(void *param)
{
	int ret = 0;
#if CONFIG_USBD_CDC_ACM_HOTPLUG
	struct task_struct check_task;
#endif
#if CONFIG_USBD_CDC_ACM_ASYNC_XFER
	struct task_struct xfer_task;
#endif

	UNUSED(param);

#if CONFIG_USBD_CDC_ACM_ASYNC_XFER
	rtw_init_sema(&cdc_acm_async_xfer_sema, 0);
#endif

#if CONFIG_USBD_CDC_ACM_HOTPLUG
	rtw_init_sema(&cdc_acm_attach_status_changed_sema, 0);
#endif

	ret = usbd_init(&cdc_acm_cfg);
	if (ret != HAL_OK) {
		printf("\n[CDC] Fail to init USB device driver\n");
		goto exit_usbd_init_fail;
	}

	ret = usbd_cdc_acm_init(CONFIG_CDC_ACM_BULK_OUT_XFER_SIZE, CONFIG_CDC_ACM_BULK_IN_XFER_SIZE, &cdc_acm_cb);
	if (ret != HAL_OK) {
		printf("\n[CDC] Fail to init CDC ACM class\n");
		goto exit_usbd_cdc_acm_init_fail;
	}

#if CONFIG_USBD_CDC_ACM_HOTPLUG
	ret = rtw_create_task(&check_task, "cdc_check_usb_status_thread", 512, CONFIG_CDC_ACM_HOTPLUG_THREAD_PRIORITY, cdc_acm_hotplug_thread, NULL);
	if (ret != pdPASS) {
		printf("\n[CDC] Fail to create CDC ACM status check thread\n");
		goto exit_create_check_task_fail;
	}
#endif

#if CONFIG_USBD_CDC_ACM_ASYNC_XFER
	// The priority of transfer thread shall be lower than USB isr priority
	ret = rtw_create_task(&xfer_task, "cdc_acm_xfer_thread", 512, CONFIG_CDC_ACM_XFER_THREAD_PRIORITY, cdc_acm_xfer_thread, NULL);
	if (ret != pdPASS) {
		printf("\n[CDC] Fail to create CDC ACM transfer thread\n");
		goto exit_create_xfer_task_fail;
	}
#endif

	rtw_mdelay_os(100);

	printf("\n[CDC] CDC ACM demo started\n");

	rtw_thread_exit();

	return;

#if CONFIG_USBD_CDC_ACM_ASYNC_XFER
exit_create_xfer_task_fail:
#if CONFIG_USBD_CDC_ACM_HOTPLUG
	rtw_delete_task(&check_task);
#endif
#endif

#if CONFIG_USBD_CDC_ACM_HOTPLUG
exit_create_check_task_fail:
	usbd_cdc_acm_deinit();
#endif

exit_usbd_cdc_acm_init_fail:
	usbd_deinit();

exit_usbd_init_fail:
#if CONFIG_USBD_CDC_ACM_HOTPLUG
	rtw_free_sema(&cdc_acm_attach_status_changed_sema);
#endif
#if CONFIG_USBD_CDC_ACM_ASYNC_XFER
	rtw_free_sema(&cdc_acm_async_xfer_sema);
#endif

	rtw_thread_exit();
}

/* Exported functions --------------------------------------------------------*/

/**
  * @brief  USB download de-initialize
  * @param  None
  * @retval Result of the operation: 0 if success else fail
  */
void example_usbd_cdc_acm(void)
{
	int ret;
	struct task_struct task;

	ret = rtw_create_task(&task, "example_usbd_cdc_acm_thread", 1024, CONFIG_CDC_ACM_INIT_THREAD_PRIORITY, example_usbd_cdc_acm_thread, NULL);
	if (ret != pdPASS) {
		printf("\n[CDC] Fail to create CDC ACM thread\n");
	}
}

#endif

