/*
 * (C) 2007 by OpenMoko, Inc.
 * Author: Harald Welte <laforge@openmoko.org>
 *
 * based on existing SAM7DFU code from OpenPCD:
 * (C) Copyright 2006 by Harald Welte <hwelte@hmw-consulting.de>
 *
 * See file CREDITS for list of people who contributed to this
 * project.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of
 * the License, or (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston,
 * MA 02111-1307 USA
 *
 * TODO:
 * - make NAND support reasonably self-contained and put in apropriate
 *   ifdefs
 * - add some means of synchronization, i.e. block commandline access
 *   while DFU transfer is in progress, and return to commandline once
 *   we're finished
 * - add VERIFY support after writing to flash
 * - sanely free() resources allocated during first uppload/download
 *   request when aborting
 * - sanely free resources when another alternate interface is selected
 *
 * Maybe:
 * - add something like uImage or some other header that provides CRC
 *   checking?
 * - make 'dnstate' attached to 'struct usb_device_instance'
 */

#include <asm/byteorder.h>
#include <usb/composite.h>
#include <linux/types.h>
#include <linux/list.h>
#include <usb/gadget.h>
#include <linux/stat.h>
#include <usb/ch9.h>
#include <usb/dfu.h>
#include <config.h>
#include <common.h>
#include <malloc.h>
#include <errno.h>
#include <fcntl.h>
#include <libbb.h>
#include <init.h>
#include <fs.h>

#define USB_DT_DFU_SIZE			9
#define USB_DT_DFU			0x21

#define CONFIG_USBD_DFU_XFER_SIZE     4096
#define DFU_TEMPFILE "/dfu_temp"

static int dfualt;
static int dfufd = -EINVAL;;
static struct usb_dfu_dev *dfu_devs;
static int dfu_num_alt;

/* USB DFU functional descriptor */
static struct usb_dfu_func_descriptor usb_dfu_func = {
	.bLength		= USB_DT_DFU_SIZE,
	.bDescriptorType	= USB_DT_DFU,
	.bmAttributes		= USB_DFU_CAN_UPLOAD | USB_DFU_CAN_DOWNLOAD | USB_DFU_MANIFEST_TOL,
	.wDetachTimeOut		= 0xff00,
	.wTransferSize		= CONFIG_USBD_DFU_XFER_SIZE,
	.bcdDFUVersion		= 0x0100,
};

struct f_dfu {
	struct usb_function		func;
	u8				port_num;

	u8	dfu_state;
	u8	dfu_status;
	struct usb_request		*dnreq;
};

static inline struct f_dfu *func_to_dfu(struct usb_function *f)
{
	return container_of(f, struct f_dfu, func);
}

/* static strings, in UTF-8 */
static struct usb_string *dfu_string_defs;

static struct usb_gadget_strings dfu_string_table = {
	.language =		0x0409,	/* en-us */
};

static struct usb_gadget_strings *dfu_strings[] = {
	&dfu_string_table,
	NULL,
};

static struct usb_interface_descriptor dfu_control_interface_desc = {
	.bLength =		USB_DT_INTERFACE_SIZE,
	.bDescriptorType =	USB_DT_INTERFACE,
	/* .bInterfaceNumber = DYNAMIC */
	.bNumEndpoints =	0,
	.bInterfaceClass =	0xfe,
	.bInterfaceSubClass =	1,
	.bInterfaceProtocol =	1,
	/* .iInterface = DYNAMIC */
};

static int
dfu_bind(struct usb_configuration *c, struct usb_function *f)
{
	struct usb_descriptor_header **header;
	struct usb_interface_descriptor *desc;
	int i;
	int			status;

	/* allocate instance-specific interface IDs, and patch descriptors */
	status = usb_interface_id(c, f);
	if (status < 0)
		return status;

	dfu_control_interface_desc.bInterfaceNumber = status;

	header = xzalloc(sizeof(void *) * (dfu_num_alt + 2));
	desc = xzalloc(sizeof(struct usb_interface_descriptor) * dfu_num_alt);
	for (i = 0; i < dfu_num_alt; i++) {
		desc[i].bLength =		USB_DT_INTERFACE_SIZE;
		desc[i].bDescriptorType =	USB_DT_INTERFACE;
		desc[i].bNumEndpoints	=	0;
		desc[i].bInterfaceClass =	0xfe;
		desc[i].bInterfaceSubClass =	1;
		desc[i].bInterfaceProtocol =	1;
		desc[i].bAlternateSetting = i;
		desc[i].iInterface = dfu_string_defs[i + 1].id;
		header[i] = (struct usb_descriptor_header *)&desc[i];
	}
	header[i] = (struct usb_descriptor_header *) &usb_dfu_func;
	header[i + 1] = NULL;

	/* copy descriptors, and track endpoint copies */
	f->descriptors = usb_copy_descriptors(header);
	if (!f->descriptors)
		goto out;

	/* support all relevant hardware speeds... we expect that when
	 * hardware is dual speed, all bulk-capable endpoints work at
	 * both speeds
	 */
	if (gadget_is_dualspeed(c->cdev->gadget)) {
		/* copy descriptors, and track endpoint copies */
		f->hs_descriptors = usb_copy_descriptors(header);
	}

	for (i = 0; i < dfu_num_alt; i++)
		printf("dfu: register alt%d(%s) with device %s\n",
				i, dfu_devs[i].name, dfu_devs[i].dev);
out:
	free(desc);
	free(header);

	if (status)
		ERROR(cdev, "%s/%p: can't bind, err %d\n", f->name, f, status);

	return status;
}

static void
dfu_unbind(struct usb_configuration *c, struct usb_function *f)
{
	struct f_dfu		*dfu = func_to_dfu(f);

	free(f->descriptors);
	if (gadget_is_dualspeed(c->cdev->gadget))
		free(f->hs_descriptors);

	free(dfu->dnreq->buf);
	usb_ep_free_request(c->cdev->gadget->ep0, dfu->dnreq);
	free(dfu);
}

static int dfu_set_alt(struct usb_function *f, unsigned intf, unsigned alt)
{
	dfualt = alt;

	return 0;
}

static int dfu_status(struct usb_function *f, const struct usb_ctrlrequest *ctrl)
{
	struct f_dfu		*dfu = func_to_dfu(f);
	struct usb_composite_dev *cdev = f->config->cdev;
	struct usb_request	*req = cdev->req;
	struct dfu_status *dstat = (struct dfu_status *) req->buf;

	dstat->bStatus = dfu->dfu_status;
	dstat->bState  = dfu->dfu_state;
	dstat->iString = 0;

	return sizeof(*dstat);
}

static void dfu_cleanup(struct f_dfu *dfu)
{
	struct stat s;

	if (dfufd > 0) {
		close(dfufd);
		dfufd = -EINVAL;
	}

	if (!stat(DFU_TEMPFILE, &s))
		unlink(DFU_TEMPFILE);
}

static void dn_complete(struct usb_ep *ep, struct usb_request *req)
{
	struct f_dfu		*dfu = req->context;
	int ret;

	ret = write(dfufd, req->buf, req->length);
	if (ret < (int)req->length) {
		perror("write");
		dfu->dfu_status = DFU_STATUS_errWRITE;
		dfu_cleanup(dfu);
	}
}

static int handle_dnload(struct usb_function *f, const struct usb_ctrlrequest *ctrl)
{
	struct f_dfu		*dfu = func_to_dfu(f);
	struct usb_composite_dev *cdev = f->config->cdev;
	u16			w_length = le16_to_cpu(ctrl->wLength);
	int ret;

	if (w_length == 0) {
		dfu->dfu_state = DFU_STATE_dfuIDLE;
		if (dfu_devs[dfualt].flags & DFU_FLAG_SAVE) {
			int fd;

			fd = open(dfu_devs[dfualt].dev, O_WRONLY);
			if (fd < 0) {
				perror("open");
				ret = -EINVAL;
				goto err_out;
			}
			ret = erase(fd, ~0, 0);
			close(fd);
			if (ret && ret != -ENOSYS) {
				perror("erase");
				ret = -EINVAL;
				goto err_out;
			}
			ret = copy_file(DFU_TEMPFILE, dfu_devs[dfualt].dev, 0);
			if (ret) {
				printf("copy file failed\n");
				ret = -EINVAL;
				goto err_out;
			}
		}
		dfu_cleanup(dfu);
		return 0;
	}

	dfu->dnreq->length = w_length;
	dfu->dnreq->context = dfu;
	usb_ep_queue(cdev->gadget->ep0, dfu->dnreq);

	return 0;

err_out:
	dfu_cleanup(dfu);
	return ret;
}

static void up_complete(struct usb_ep *ep, struct usb_request *req)
{
}

static int handle_upload(struct usb_function *f, const struct usb_ctrlrequest *ctrl)
{
	struct f_dfu		*dfu = func_to_dfu(f);
	struct usb_composite_dev *cdev = f->config->cdev;
	u16			w_length = le16_to_cpu(ctrl->wLength);
	int len;

	len = read(dfufd, dfu->dnreq->buf, w_length);

	dfu->dnreq->length = len;
	if (len < w_length) {
		dfu_cleanup(dfu);
		dfu->dfu_state = DFU_STATE_dfuIDLE;
	}

	dfu->dnreq->complete = up_complete;
	usb_ep_queue(cdev->gadget->ep0, dfu->dnreq);

	return 0;
}

static void dfu_abort(struct f_dfu *dfu)
{
	dfu->dfu_state = DFU_STATE_dfuIDLE;
	dfu->dfu_status = DFU_STATUS_OK;

	dfu_cleanup(dfu);
}

static int dfu_setup(struct usb_function *f, const struct usb_ctrlrequest *ctrl)
{
	struct f_dfu		*dfu = func_to_dfu(f);
	struct usb_composite_dev *cdev = f->config->cdev;
	struct usb_request	*req = cdev->req;
	int			value = -EOPNOTSUPP;
	int			w_length = le16_to_cpu(ctrl->wLength);
	int			w_value = le16_to_cpu(ctrl->wValue);
	int ret;

	if (ctrl->bRequestType == USB_DIR_IN && ctrl->bRequest == USB_REQ_GET_DESCRIPTOR
			&& (w_value >> 8) == 0x21) {
		value = min(w_length, (int)sizeof(usb_dfu_func));
		memcpy(req->buf, &usb_dfu_func, value);
		goto out;
	}

	/* Allow GETSTATUS in every state */
	if (ctrl->bRequest == USB_REQ_DFU_GETSTATUS) {
		value = dfu_status(f, ctrl);
		value = min(value, w_length);
		goto out;
	}

	/* Allow GETSTATE in every state */
	if (ctrl->bRequest == USB_REQ_DFU_GETSTATE) {
		*(u8 *)req->buf = dfu->dfu_state;
		value = sizeof(u8);
		goto out;
	}

	switch (dfu->dfu_state) {
	case DFU_STATE_appIDLE:
		switch (ctrl->bRequest) {
		case USB_REQ_DFU_DETACH:
			dfu->dfu_state = DFU_STATE_appDETACH;
			value = 0;
			goto out;
			break;
		default:
			value = -EINVAL;
		}
		break;
	case DFU_STATE_appDETACH:
		switch (ctrl->bRequest) {
		default:
			dfu->dfu_state = DFU_STATE_appIDLE;
			value = -EINVAL;
			goto out;
			break;
		}
		break;
	case DFU_STATE_dfuIDLE:
		switch (ctrl->bRequest) {
		case USB_REQ_DFU_DNLOAD:
			if (w_length == 0) {
				dfu->dfu_state = DFU_STATE_dfuERROR;
				value = -EINVAL;
				goto out;
			}
			debug("dfu: starting download to %s\n", dfu_devs[dfualt].dev);
			if (dfu_devs[dfualt].flags & DFU_FLAG_SAVE)
				dfufd = open(DFU_TEMPFILE, O_WRONLY | O_CREAT);
			else
				dfufd = open(dfu_devs[dfualt].dev, O_WRONLY);

			if (dfufd < 0) {
				dfu->dfu_state = DFU_STATE_dfuERROR;
				perror("open");
				goto out;
			}

			ret = erase(dfufd, ~0, 0);
			if (ret && ret != -ENOSYS) {
				dfu->dfu_status = DFU_STATUS_errERASE;
				perror("erase");
				goto out;
			}

			value = handle_dnload(f, ctrl);
			dfu->dfu_state = DFU_STATE_dfuDNLOAD_IDLE;
			return 0;
			break;
		case USB_REQ_DFU_UPLOAD:
			dfu->dfu_state = DFU_STATE_dfuUPLOAD_IDLE;
			debug("dfu: starting upload from %s\n", dfu_devs[dfualt].dev);
			if (!(dfu_devs[dfualt].flags & DFU_FLAG_READBACK)) {
				dfu->dfu_state = DFU_STATE_dfuERROR;
				goto out;
			}
			dfufd = open(dfu_devs[dfualt].dev, O_RDONLY);
			if (dfufd < 0) {
				dfu->dfu_state = DFU_STATE_dfuERROR;
				perror("open");
				goto out;
			}
			handle_upload(f, ctrl);
			return 0;
			break;
		case USB_REQ_DFU_ABORT:
			dfu->dfu_status = DFU_STATUS_OK;
			value = 0;
			break;
		case USB_REQ_DFU_DETACH:
			/* Proprietary extension: 'detach' from idle mode and
			 * get back to runtime mode in case of USB Reset.  As
			 * much as I dislike this, we just can't use every USB
			 * bus reset to switch back to runtime mode, since at
			 * least the Linux USB stack likes to send a number of resets
			 * in a row :( */
			dfu->dfu_state = DFU_STATE_dfuMANIFEST_WAIT_RST;
			break;
		default:
			dfu->dfu_state = DFU_STATE_dfuERROR;
			value = -EINVAL;
			goto out;
			break;
		}
		break;
	case DFU_STATE_dfuDNLOAD_IDLE:
		switch (ctrl->bRequest) {
		case USB_REQ_DFU_DNLOAD:
			value = handle_dnload(f, ctrl);
			if (dfu->dfu_state != DFU_STATE_dfuIDLE) {
				return 0;
			}
			break;
		case USB_REQ_DFU_ABORT:
			dfu_abort(dfu);
			value = 0;
			break;
		default:
			dfu->dfu_state = DFU_STATE_dfuERROR;
			value = -EINVAL;
			break;
		}
		break;
	case DFU_STATE_dfuUPLOAD_IDLE:
		switch (ctrl->bRequest) {
		case USB_REQ_DFU_UPLOAD:
			handle_upload(f, ctrl);
			return 0;
			break;
		case USB_REQ_DFU_ABORT:
			dfu_abort(dfu);
			value = 0;
			break;
		default:
			dfu->dfu_state = DFU_STATE_dfuERROR;
			value = -EINVAL;
			break;
		}
		break;
	case DFU_STATE_dfuERROR:
		switch (ctrl->bRequest) {
		case USB_REQ_DFU_CLRSTATUS:
			dfu_abort(dfu);
			/* no zlp? */
			value = 0;
			break;
		default:
			dfu->dfu_state = DFU_STATE_dfuERROR;
			value = -EINVAL;
			break;
		}
		break;
	case DFU_STATE_dfuDNLOAD_SYNC:
	case DFU_STATE_dfuDNBUSY:
	case DFU_STATE_dfuMANIFEST_SYNC:
	case DFU_STATE_dfuMANIFEST:
	case DFU_STATE_dfuMANIFEST_WAIT_RST:
		dfu->dfu_state = DFU_STATE_dfuERROR;
		value = -EINVAL;
		goto out;
		break;
	default:
		break;
	}
out:
	/* respond with data transfer or status phase? */
	if (value >= 0) {
		req->zero = 0;
		req->length = value;
		value = usb_ep_queue(cdev->gadget->ep0, req);
		if (value < 0)
			ERROR(cdev, "dfu response on ttyGS%d, err %d\n",
					dfu->port_num, value);
	}

	return value;
}

static void dfu_disable(struct usb_function *f)
{
	struct f_dfu		*dfu = func_to_dfu(f);

	switch (dfu->dfu_state) {
	case DFU_STATE_appDETACH:
		dfu->dfu_state = DFU_STATE_dfuIDLE;
		break;
	case DFU_STATE_dfuMANIFEST_WAIT_RST:
		dfu->dfu_state = DFU_STATE_appIDLE;
		break;
	default:
		dfu->dfu_state = DFU_STATE_appDETACH;
		break;
	}

	dfu_cleanup(dfu);
}

#define STRING_MANUFACTURER_IDX		0
#define STRING_PRODUCT_IDX		1
#define STRING_DESCRIPTION_IDX		2

static struct usb_string strings_dev[] = {
	[STRING_MANUFACTURER_IDX].s = NULL,
	[STRING_PRODUCT_IDX].s = NULL,
	[STRING_DESCRIPTION_IDX].s = "USB Device Firmware Upgrade",
	{  } /* end of list */
};

static struct usb_gadget_strings stringtab_dev = {
	.language	= 0x0409,	/* en-us */
	.strings	= strings_dev,
};

static struct usb_gadget_strings *dev_strings[] = {
	&stringtab_dev,
	NULL,
};

static int dfu_bind_config(struct usb_configuration *c)
{
	struct f_dfu *dfu;
	struct usb_function *func;
	int		status;
	int i;

	/* config description */
	status = usb_string_id(c->cdev);
	if (status < 0)
		return status;
	strings_dev[STRING_DESCRIPTION_IDX].id = status;

	status = usb_string_id(c->cdev);
	if (status < 0)
		return status;
	dfu_control_interface_desc.iInterface = status;

	/* allocate and initialize one new instance */
	dfu = xzalloc(sizeof *dfu);

	dfu->dfu_state = DFU_STATE_appIDLE;
	dfu->dfu_status = DFU_STATUS_OK;

	dfu_string_defs = xzalloc(sizeof(struct usb_string) * (dfu_num_alt + 2));
	dfu_string_defs[0].s = "Generic DFU";
	dfu_string_defs[0].id = status;
	for (i = 0; i < dfu_num_alt; i++) {
		dfu_string_defs[i + 1].s = dfu_devs[i].name;
		status = usb_string_id(c->cdev);
		if (status < 0)
			goto out;
		dfu_string_defs[i + 1].id = status;
	}
	dfu_string_defs[i + 1].s = NULL;
	dfu_string_table.strings = dfu_string_defs;

	func = &dfu->func;
	func->name = "DFU";
	func->strings = dfu_strings;
	/* descriptors are per-instance copies */
	func->bind = dfu_bind;
	func->unbind = dfu_unbind;
	func->set_alt = dfu_set_alt;
	func->setup = dfu_setup;
	func->disable = dfu_disable;

	dfu->dnreq = usb_ep_alloc_request(c->cdev->gadget->ep0);
	if (!dfu->dnreq)
		printf("usb_ep_alloc_request failed\n");
	dfu->dnreq->buf = xmalloc(CONFIG_USBD_DFU_XFER_SIZE);
	dfu->dnreq->complete = dn_complete;
	dfu->dnreq->zero = 0;

	status = usb_add_function(c, func);
	if (status)
		goto out;

	return 0;
out:
	free(dfu);
	free(dfu_string_defs);

	return status;
}

static void dfu_unbind_config(struct usb_configuration *c)
{
	free(dfu_string_defs);
}

static struct usb_configuration dfu_config_driver = {
	.label			= "USB DFU",
	.bind			= dfu_bind_config,
	.unbind			= dfu_unbind_config,
	.bConfigurationValue	= 1,
	.bmAttributes		= USB_CONFIG_ATT_SELFPOWER,
};

static struct usb_device_descriptor dfu_dev_descriptor = {
	.bLength		= USB_DT_DEVICE_SIZE,
	.bDescriptorType	= USB_DT_DEVICE,
	.bcdUSB			= 0x0100,
	.bDeviceClass		= 0x00,
	.bDeviceSubClass	= 0x00,
	.bDeviceProtocol	= 0x00,
/*	.idVendor		= dynamic */
/*	.idProduct		= dynamic */
	.bcdDevice		= 0x0000,
	.bNumConfigurations	= 0x01,
};

static int dfu_driver_bind(struct usb_composite_dev *cdev)
{
	int status;

	status = usb_string_id(cdev);
	if (status < 0)
		goto fail;
	strings_dev[STRING_MANUFACTURER_IDX].id = status;
	dfu_dev_descriptor.iManufacturer = status;

	status = usb_string_id(cdev);
	if (status < 0)
		goto fail;
	strings_dev[STRING_PRODUCT_IDX].id = status;
	dfu_dev_descriptor.iProduct = status;

	/* config description */
	status = usb_string_id(cdev);
	if (status < 0)
		goto fail;
	strings_dev[STRING_DESCRIPTION_IDX].id = status;
	dfu_config_driver.iConfiguration = status;

	status = usb_add_config(cdev, &dfu_config_driver);
	if (status < 0)
		goto fail;

	return 0;
fail:
	return status;
}

static struct usb_composite_driver dfu_driver = {
	.name		= "g_dfu",
	.dev		= &dfu_dev_descriptor,
	.strings	= dev_strings,
	.bind		= dfu_driver_bind,
};

int usb_dfu_register(struct usb_dfu_pdata *pdata)
{
	dfu_devs = pdata->alts;
	dfu_num_alt = pdata->num_alts;
	dfu_dev_descriptor.idVendor = pdata->idVendor;
	dfu_dev_descriptor.idProduct = pdata->idProduct;
	strings_dev[STRING_MANUFACTURER_IDX].s = pdata->manufacturer;
	strings_dev[STRING_PRODUCT_IDX].s = pdata->productname;

	usb_composite_register(&dfu_driver);

	while (1) {
		usb_gadget_poll();
		if (ctrlc())
			goto out;
	}

out:
	usb_composite_unregister(&dfu_driver);

	return 0;
}

