/**
 * \file
 *
 * \brief USB Device Stack CDC ACM Function Implementation.
 *
 * Copyright (c) 2015-2018 Microchip Technology Inc. and its subsidiaries.
 *
 * \asf_license_start
 *
 * \page License
 *
 * Subject to your compliance with these terms, you may use Microchip
 * software and any derivatives exclusively with Microchip products.
 * It is your responsibility to comply with third party license terms applicable
 * to your use of third party software (including open source software) that
 * may accompany Microchip software.
 *
 * THIS SOFTWARE IS SUPPLIED BY MICROCHIP "AS IS". NO WARRANTIES,
 * WHETHER EXPRESS, IMPLIED OR STATUTORY, APPLY TO THIS SOFTWARE,
 * INCLUDING ANY IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY,
 * AND FITNESS FOR A PARTICULAR PURPOSE. IN NO EVENT WILL MICROCHIP BE
 * LIABLE FOR ANY INDIRECT, SPECIAL, PUNITIVE, INCIDENTAL OR CONSEQUENTIAL
 * LOSS, DAMAGE, COST OR EXPENSE OF ANY KIND WHATSOEVER RELATED TO THE
 * SOFTWARE, HOWEVER CAUSED, EVEN IF MICROCHIP HAS BEEN ADVISED OF THE
 * POSSIBILITY OR THE DAMAGES ARE FORESEEABLE.  TO THE FULLEST EXTENT
 * ALLOWED BY LAW, MICROCHIP'S TOTAL LIABILITY ON ALL CLAIMS IN ANY WAY
 * RELATED TO THIS SOFTWARE WILL NOT EXCEED THE AMOUNT OF FEES, IF ANY,
 * THAT YOU HAVE PAID DIRECTLY TO MICROCHIP FOR THIS SOFTWARE.
 *
 * \asf_license_stop
 *
 */

#include "cdcdf_acm.h"

#define CDCDF_ACM_VERSION 0x00000001u
#define CDCDF_ACM_COMM_EP_INDEX 0
#define CDCDF_ACM_DATA_EP_INDEX 1

/** USB Device CDC ACM Fucntion Specific Data */
struct cdcdf_acm_func_data {
	/** CDC Device ACM Interface information */
	uint8_t func_iface[2];
	/** CDC Device ACM IN Endpoint */
	uint8_t func_ep_in[2];
	/** CDC Device ACM OUT Endpoint */
	uint8_t func_ep_out;
	/** CDC Device ACM Enable Flag */
	bool enabled;
};

static struct usbdf_driver        _cdcdf_acm;
static struct cdcdf_acm_func_data _cdcdf_acm_funcd;
static struct usb_cdc_line_coding usbd_cdc_line_coding;

static cdcdf_acm_notify_state_t    cdcdf_acm_notify_state    = NULL;
static cdcdf_acm_set_line_coding_t cdcdf_acm_set_line_coding = NULL;

/**
 * \brief Enable CDC ACM Function
 * \param[in] drv Pointer to USB device function driver
 * \param[in] desc Pointer to USB interface descriptor
 * \return Operation status.
 */
static int32_t cdcdf_acm_enable(struct usbdf_driver *drv, struct usbd_descriptors *desc)
{
	struct cdcdf_acm_func_data *func_data = (struct cdcdf_acm_func_data *)(drv->func_data);

	usb_ep_desc_t    ep_desc;
	usb_iface_desc_t ifc_desc;
	uint8_t *        ifc, *ep;
	uint8_t          i;

	ifc = desc->sod;
	for (i = 0; i < 2; i++) {
		if (NULL == ifc) {
			return ERR_NOT_FOUND;
		}

		ifc_desc.bInterfaceNumber = ifc[2];
		ifc_desc.bInterfaceClass  = ifc[5];

		if ((CDC_CLASS_COMM == ifc_desc.bInterfaceClass) || (CDC_CLASS_DATA == ifc_desc.bInterfaceClass)) {
			if (func_data->func_iface[i] == ifc_desc.bInterfaceNumber) { // Initialized
				return ERR_ALREADY_INITIALIZED;
			} else if (func_data->func_iface[i] != 0xFF) { // Occupied
				return ERR_NO_RESOURCE;
			} else {
				func_data->func_iface[i] = ifc_desc.bInterfaceNumber;
			}
		} else { // Not supported by this function driver
			return ERR_NOT_FOUND;
		}

		// Install endpoints
		ep = usb_find_desc(ifc, desc->eod, USB_DT_ENDPOINT);
		while (NULL != ep) {
			ep_desc.bEndpointAddress = ep[2];
			ep_desc.bmAttributes     = ep[3];
			ep_desc.wMaxPacketSize   = usb_get_u16(ep + 4);
			if (usb_d_ep_init(ep_desc.bEndpointAddress, ep_desc.bmAttributes, ep_desc.wMaxPacketSize)) {
				return ERR_NOT_INITIALIZED;
			}
			if (ep_desc.bEndpointAddress & USB_EP_DIR_IN) {
				func_data->func_ep_in[i] = ep_desc.bEndpointAddress;
				usb_d_ep_enable(func_data->func_ep_in[i]);
			} else {
				func_data->func_ep_out = ep_desc.bEndpointAddress;
				usb_d_ep_enable(func_data->func_ep_out);
			}
			desc->sod = ep;
			ep        = usb_find_ep_desc(usb_desc_next(desc->sod), desc->eod);
		}
		ifc = usb_find_desc(usb_desc_next(desc->sod), desc->eod, USB_DT_INTERFACE);
	}
	// Installed
	_cdcdf_acm_funcd.enabled = true;
	return ERR_NONE;
}

/**
 * \brief Disable CDC ACM Function
 * \param[in] drv Pointer to USB device function driver
 * \param[in] desc Pointer to USB device descriptor
 * \return Operation status.
 */
static int32_t cdcdf_acm_disable(struct usbdf_driver *drv, struct usbd_descriptors *desc)
{
	struct cdcdf_acm_func_data *func_data = (struct cdcdf_acm_func_data *)(drv->func_data);

	usb_iface_desc_t ifc_desc;
	uint8_t          i;

	if (desc) {
		ifc_desc.bInterfaceClass = desc->sod[5];
		// Check interface
		if ((ifc_desc.bInterfaceClass != CDC_CLASS_COMM) && (ifc_desc.bInterfaceClass != CDC_CLASS_DATA)) {
			return ERR_NOT_FOUND;
		}
	}

	for (i = 0; i < 2; i++) {
		if (func_data->func_iface[i] == 0xFF) {
			continue;
		} else {
			func_data->func_iface[i] = 0xFF;
			if (func_data->func_ep_in[i] != 0xFF) {
				usb_d_ep_deinit(func_data->func_ep_in[i]);
				func_data->func_ep_in[i] = 0xFF;
			}
		}
	}

	if (func_data->func_ep_out != 0xFF) {
		usb_d_ep_deinit(func_data->func_ep_out);
		func_data->func_ep_out = 0xFF;
	}

	_cdcdf_acm_funcd.enabled = false;
	return ERR_NONE;
}

/**
 * \brief CDC ACM Control Function
 * \param[in] drv Pointer to USB device function driver
 * \param[in] ctrl USB device general function control type
 * \param[in] param Parameter pointer
 * \return Operation status.
 */
static int32_t cdcdf_acm_ctrl(struct usbdf_driver *drv, enum usbdf_control ctrl, void *param)
{
	switch (ctrl) {
	case USBDF_ENABLE:
		return cdcdf_acm_enable(drv, (struct usbd_descriptors *)param);

	case USBDF_DISABLE:
		return cdcdf_acm_disable(drv, (struct usbd_descriptors *)param);

	case USBDF_GET_IFACE:
		return ERR_UNSUPPORTED_OP;

	default:
		return ERR_INVALID_ARG;
	}
}

/**
 * \brief Process the CDC class set request
 * \param[in] ep Endpoint address.
 * \param[in] req Pointer to the request.
 * \return Operation status.
 */
static int32_t cdcdf_acm_set_req(uint8_t ep, struct usb_req *req, enum usb_ctrl_stage stage)
{
	struct usb_cdc_line_coding line_coding_tmp;
	uint16_t                   len      = req->wLength;
	uint8_t *                  ctrl_buf = usbdc_get_ctrl_buffer();

	switch (req->bRequest) {
	case USB_REQ_CDC_SET_LINE_CODING:
		if (sizeof(struct usb_cdc_line_coding) != len) {
			return ERR_INVALID_DATA;
		}
		if (USB_SETUP_STAGE == stage) {
			return usbdc_xfer(ep, ctrl_buf, len, false);
		} else {
			memcpy(&line_coding_tmp, ctrl_buf, sizeof(struct usb_cdc_line_coding));
			if ((NULL == cdcdf_acm_set_line_coding) || (true == cdcdf_acm_set_line_coding(&line_coding_tmp))) {
				usbd_cdc_line_coding = line_coding_tmp;
			}
			return ERR_NONE;
		}
	case USB_REQ_CDC_SET_CONTROL_LINE_STATE:
		usbdc_xfer(0, NULL, 0, 0);
		if (NULL != cdcdf_acm_notify_state) {
			cdcdf_acm_notify_state(req->wValue);
		}
		return ERR_NONE;
	default:
		return ERR_INVALID_ARG;
	}
}

/**
 * \brief Process the CDC class get request
 * \param[in] ep Endpoint address.
 * \param[in] req Pointer to the request.
 * \return Operation status.
 */
static int32_t cdcdf_acm_get_req(uint8_t ep, struct usb_req *req, enum usb_ctrl_stage stage)
{
	uint16_t len = req->wLength;

	if (USB_DATA_STAGE == stage) {
		return ERR_NONE;
	}

	switch (req->bRequest) {
	case USB_REQ_CDC_GET_LINE_CODING:
		if (sizeof(struct usb_cdc_line_coding) != len) {
			return ERR_INVALID_DATA;
		}
		return usbdc_xfer(ep, (uint8_t *)&usbd_cdc_line_coding, len, false);
	default:
		return ERR_INVALID_ARG;
	}
}

/**
 * \brief Process the CDC class request
 * \param[in] ep Endpoint address.
 * \param[in] req Pointer to the request.
 * \return Operation status.
 */
static int32_t cdcdf_acm_req(uint8_t ep, struct usb_req *req, enum usb_ctrl_stage stage)
{
	if (0x01 != ((req->bmRequestType >> 5) & 0x03)) { // class request
		return ERR_NOT_FOUND;
	}
	if ((req->wIndex == _cdcdf_acm_funcd.func_iface[0]) || (req->wIndex == _cdcdf_acm_funcd.func_iface[1])) {
		if (req->bmRequestType & USB_EP_DIR_IN) {
			return cdcdf_acm_get_req(ep, req, stage);
		} else {
			return cdcdf_acm_set_req(ep, req, stage);
		}
	} else {
		return ERR_NOT_FOUND;
	}
}

/** USB Device CDC ACM Handler Struct */
static struct usbdc_handler cdcdf_acm_req_h = {NULL, (FUNC_PTR)cdcdf_acm_req};

/**
 * \brief Initialize the USB CDC ACM Function Driver
 */
int32_t cdcdf_acm_init(void)
{
	if (usbdc_get_state() > USBD_S_POWER) {
		return ERR_DENIED;
	}

	_cdcdf_acm.ctrl      = cdcdf_acm_ctrl;
	_cdcdf_acm.func_data = &_cdcdf_acm_funcd;

	usbdc_register_function(&_cdcdf_acm);
	usbdc_register_handler(USBDC_HDL_REQ, &cdcdf_acm_req_h);
	return ERR_NONE;
}

/**
 * \brief Deinitialize the USB CDC ACM Function Driver
 */
void cdcdf_acm_deinit(void)
{
	usb_d_ep_deinit(_cdcdf_acm_funcd.func_ep_in[CDCDF_ACM_COMM_EP_INDEX]);
	usb_d_ep_deinit(_cdcdf_acm_funcd.func_ep_in[CDCDF_ACM_DATA_EP_INDEX]);
	usb_d_ep_deinit(_cdcdf_acm_funcd.func_ep_out);
}

/**
 * \brief USB CDC ACM Function Read Data
 */
int32_t cdcdf_acm_read(uint8_t *buf, uint32_t size)
{
	if (!cdcdf_acm_is_enabled()) {
		return ERR_DENIED;
	}
	return usbdc_xfer(_cdcdf_acm_funcd.func_ep_out, buf, size, false);
}

/**
 * \brief USB CDC ACM Function Write Data
 */
int32_t cdcdf_acm_write(uint8_t *buf, uint32_t size)
{
	if (!cdcdf_acm_is_enabled()) {
		return ERR_DENIED;
	}
	return usbdc_xfer(_cdcdf_acm_funcd.func_ep_in[CDCDF_ACM_DATA_EP_INDEX], buf, size, true);
}

/**
 * \brief USB CDC ACM Stop the data transfer
 */
void cdcdf_acm_stop_xfer(void)
{
	/* Stop transfer. */
	usb_d_ep_abort(_cdcdf_acm_funcd.func_ep_in[CDCDF_ACM_DATA_EP_INDEX]);
	usb_d_ep_abort(_cdcdf_acm_funcd.func_ep_out);
}

/**
 * \brief USB CDC ACM Function Register Callback
 */
int32_t cdcdf_acm_register_callback(enum cdcdf_acm_cb_type cb_type, FUNC_PTR func)
{
	switch (cb_type) {
	case CDCDF_ACM_CB_READ:
		usb_d_ep_register_callback(_cdcdf_acm_funcd.func_ep_out, USB_D_EP_CB_XFER, func);
		break;
	case CDCDF_ACM_CB_WRITE:
		usb_d_ep_register_callback(_cdcdf_acm_funcd.func_ep_in[CDCDF_ACM_DATA_EP_INDEX], USB_D_EP_CB_XFER, func);
		break;
	case CDCDF_ACM_CB_LINE_CODING_C:
		cdcdf_acm_set_line_coding = (cdcdf_acm_set_line_coding_t)func;
		break;
	case CDCDF_ACM_CB_STATE_C:
		cdcdf_acm_notify_state = (cdcdf_acm_notify_state_t)func;
		break;
	default:
		return ERR_INVALID_ARG;
	}
	return ERR_NONE;
}

/**
 * \brief Check whether CDC ACM Function is enabled
 */
bool cdcdf_acm_is_enabled(void)
{
	return _cdcdf_acm_funcd.enabled;
}

/**
 * \brief Return the CDC ACM line coding structure start address
 */
const struct usb_cdc_line_coding *cdcdf_acm_get_line_coding(void)
{
	return (const struct usb_cdc_line_coding *)&usbd_cdc_line_coding;
}

/**
 * \brief Return version
 */
uint32_t cdcdf_acm_get_version(void)
{
	return CDCDF_ACM_VERSION;
}