/*
 * (C) 2018, sysmocom -s.f.m.c. GmbH, Author: Kevin Redon <kredon@sysmocom.de>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */
#include "atmel_start.h"
#include "usb_start.h"
#include "config/usbd_config.h"

#if CONF_USBD_HS_SP
static uint8_t single_desc_bytes[] = {
    /* Device descriptors and Configuration descriptors list. */
    DFUD_HS_DESCES_LS_FS};
static uint8_t single_desc_bytes_hs[] = {
    /* Device descriptors and Configuration descriptors list. */
    DFUD_HS_DESCES_HS};
#else
static uint8_t single_desc_bytes[] = {
    /* Device descriptors and Configuration descriptors list. */
    DFUD_DESCES_LS_FS};
#endif

static struct usbd_descriptors single_desc[]
    = {{single_desc_bytes, single_desc_bytes + sizeof(single_desc_bytes)}
#if CONF_USBD_HS_SP
       ,
       {single_desc_bytes_hs, single_desc_bytes_hs + sizeof(single_desc_bytes_hs)}
#endif
};

/** USB DFU functional descriptor (with DFU attributes) */
static const uint8_t usb_dfu_func_desc_bytes[] = {DFUD_IFACE_DESCB};
static const usb_dfu_func_desc_t* usb_dfu_func_desc = (usb_dfu_func_desc_t*)&usb_dfu_func_desc_bytes;
/** Ctrl endpoint buffer */
static uint8_t ctrl_buffer[64];



/* transmit given string descriptor */
static bool send_str_desc(uint8_t ep, const struct usb_req *req, enum usb_ctrl_stage stage,
			  const uint8_t *desc)
{
	uint16_t len_req = LE16(req->wLength);
	uint16_t len_desc = desc[0];
	uint16_t len_tx;
	bool need_zlp = !(len_req & (CONF_USB_DFUD_BMAXPKSZ0 - 1));

	if (len_req <= len_desc) {
		need_zlp = false;
		len_tx = len_req;
	} else {
		len_tx = len_desc;
	}

	if (ERR_NONE != usbdc_xfer(ep, (uint8_t *)desc, len_tx, need_zlp)) {
		return true;
	}

	return false;
}

extern uint8_t sernr_buf_descr[];
extern uint8_t product_buf_descr[];

/* call-back for every control EP request */
static int32_t string_req_cb(uint8_t ep, struct usb_req *req, enum usb_ctrl_stage stage)
{
	uint8_t index, type;

	if (stage != USB_SETUP_STAGE)
		return ERR_NOT_FOUND;

	if ((req->bmRequestType & (USB_REQT_TYPE_MASK | USB_REQT_DIR_IN)) !=
	    (USB_REQT_TYPE_STANDARD | USB_REQT_DIR_IN))
		return ERR_NOT_FOUND;

	/* abort if it's not a GET DESCRIPTOR request */
	if (req->bRequest != USB_REQ_GET_DESC)
		return ERR_NOT_FOUND;

	/* abort if it's not about a string descriptor */
	type = req->wValue >> 8;
	if (type != USB_DT_STRING)
		return ERR_NOT_FOUND;
#if 0
	printf("ep=%02x, bmReqT=%04x, bReq=%02x, wValue=%04x, stage=%d\r\n",
		ep, req->bmRequestType, req->bRequest, req->wValue, stage);
#endif
	/* abort if it's not a standard GET request */
	index = req->wValue & 0x00FF;
	switch (index) {
	case CONF_USB_DFUD_ISERIALNUM:
		return send_str_desc(ep, req, stage, sernr_buf_descr);
	case CONF_USB_DFUD_IPRODUCT:
		return send_str_desc(ep, req, stage, product_buf_descr);
	default:
		return ERR_NOT_FOUND;
	}
}


static struct usbdc_handler string_req_h = {NULL, (FUNC_PTR)string_req_cb};



/**
 * \brief USB DFU Init
 */
void usb_dfu_init(void)
{
	usbdc_init(ctrl_buffer);
	dfudf_init();
#if defined(SYSMOOCTSIM)
	usbdc_register_handler(USBDC_HDL_REQ, &string_req_h);
#endif

	usbdc_start(single_desc);
	usbdc_attach();
}

/**
 * \brief reset device
 */
static void usb_dfu_reset(const enum usb_event ev, const uint32_t param)
{
	(void)param; // not used
	switch (ev) {
	case USB_EV_RESET:
		usbdc_detach(); // make sure we are detached
		NVIC_SystemReset(); // initiate a system reset
		break;
	default:
		break;
	}
	
}

/**
 * \brief Enter USB DFU runtime
 */
void usb_dfu(void)
{
	while (!dfudf_is_enabled()); // wait for DFU to be installed
	LED_SYSTEM_on(); // switch LED on to indicate USB DFU stack is ready

	uint32_t application_start_address = BL_SIZE_BYTE;
	int rc;
	ASSERT(application_start_address > 0);

	while (true) { // main DFU infinite loop
		enum usb_dfu_state last_dfu_state = dfu_state;

		// run the second part of the USB DFU state machine handling non-USB aspects
		switch (last_dfu_state) {
		case USB_DFU_STATE_DFU_DNLOAD_SYNC:
		case USB_DFU_STATE_DFU_DNBUSY:
			if (dfu_flash_done)
				break;

			LED_SYSTEM_off(); // switch LED off to indicate we are flashing
			if (dfu_download_length > 0) { // there is some data to be flashed
				/* The first block will be only erased, to ensure the
				 * full firmware has been written.
				 * In case of a power loss, the bootloader won't boot applications
				 * if the first 4 byte are 0xffffffff
				 */
				if (dfu_download_offset == 0) {
					dfu_download_length_first = dfu_download_length;
					memcpy(dfu_download_data_first, dfu_download_data, dfu_download_length);
					/* erasing the first page is enough, flash_write will keep care of the remaining ones */
					rc = flash_erase(&FLASH_0, application_start_address, 1);
				} else {
					rc = flash_write(&FLASH_0, application_start_address + dfu_download_offset,
							 dfu_download_data, dfu_download_length);
				}

				CRITICAL_SECTION_ENTER();
				switch (rc) {
				case ERR_NONE:
					dfu_flash_status = USB_DFU_STATUS_OK;
					dfu_download_offset += dfu_download_length;
					break;
				case ERR_BAD_ADDRESS:
					dfu_flash_status = USB_DFU_STATUS_ERR_ADDRESS;
					break;
				case ERR_DENIED:
					dfu_flash_status = USB_DFU_STATUS_ERR_WRITE;
					break;
				default:
					dfu_flash_status = USB_DFU_STATUS_ERR_PROG;
					break;
				}
				dfu_flash_done = true;
				CRITICAL_SECTION_LEAVE();
			}
			LED_SYSTEM_on(); // switch LED on to indicate USB DFU can resume
			break;
		case USB_DFU_STATE_DFU_MANIFEST:
			/* finish flashing by writing the first block */
			rc = flash_write(&FLASH_0, application_start_address, dfu_download_data_first,
					 dfu_download_length_first);
			switch (rc) {
			case ERR_NONE:
				break;
			case ERR_BAD_ADDRESS:
				dfu_flash_status = USB_DFU_STATUS_ERR_ADDRESS;
				break;
			case ERR_DENIED:
				dfu_flash_status = USB_DFU_STATUS_ERR_WRITE;
				break;
			default:
				dfu_flash_status = USB_DFU_STATUS_ERR_PROG;
				break;
			}

			/* try to clear it, to ensure it doesn't boot into a broken application */
			if (dfu_flash_status != ERR_NONE) {
				flash_erase(&FLASH_0, application_start_address, 1);
			}

			CRITICAL_SECTION_ENTER();
			dfu_manifestation_complete = true; // we completed flashing and all checks
			if (dfu_flash_status != ERR_NONE)
				dfu_state = USB_DFU_STATE_DFU_ERROR;
			else if (usb_dfu_func_desc->bmAttributes & USB_DFU_ATTRIBUTES_MANIFEST_TOLERANT)
				dfu_state = USB_DFU_STATE_DFU_MANIFEST_SYNC;
			else
				dfu_state = USB_DFU_STATE_DFU_MANIFEST_WAIT_RESET;
			CRITICAL_SECTION_LEAVE();
			break;
		case USB_DFU_STATE_DFU_MANIFEST_WAIT_RESET:
			if (usb_dfu_func_desc->bmAttributes & USB_DFU_ATTRIBUTES_WILL_DETACH) {
				usb_dfu_reset(USB_EV_RESET, 0); // immediately reset
			} else { // wait for USB reset
				usb_d_register_callback(USB_D_CB_EVENT, (FUNC_PTR)usb_dfu_reset); // register new USB reset event handler
			}
			break;
		default:
			break;
		}
	}
}

void usb_init(void)
{
	usb_dfu_init();
}
