/*
 * Copyright 2015 Google Inc.
 *
 * 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
 */

#include <assert.h>
#include <config.h>
#include <libpayload.h>

#include "fastboot/udc.h"
#include "image/symbols.h"

static device_descriptor_t device_descriptor = {
	.bLength = sizeof(device_descriptor_t),
	.bDescriptorType = 1,
	.bcdUSB = 0x0200,
	.bDeviceClass = 0,
	.bDeviceSubClass = 0,
	.bDeviceProtocol = 0,
	.bMaxPacketSize0 = 64,
	.idVendor = CONFIG_FASTBOOT_USBVID,
	.idProduct = CONFIG_FASTBOOT_USBDID,
};

/* Length of OUT transfer. Because these transfers
 * are named from the host's point of view, OUT
 * is "receive" for us.
 */
static int out_length;

static void fastboot_packet(struct usbdev_ctrl *this, int ep, int in_dir,
	void *data, int len)
{
	/* verify that we're responsible for the transfer */
	if (in_dir && (ep != CONFIG_FASTBOOT_EP_IN)) return;
	if (!in_dir && (ep != CONFIG_FASTBOOT_EP_OUT)) return;

	if (in_dir == 0) {
		// tell usb_gadget_recv() that the transfer is done
		out_length = len;
	}
}

/* configuration descriptor's wTotalLength and bConfigurationValue are
 * filled in by driver
 */
static struct usbdev_configuration fastboot_config = {
	.descriptor = {
		.bLength = sizeof(configuration_descriptor_t),
		.bDescriptorType = 2,
		.bNumInterfaces = 1,
		.iConfiguration = 0,
		.bmAttributes = 0xc0, /* self powered */
		.bMaxPower = 0, /* 0*2mA draw */
	},
	.interfaces = {{
		.descriptor = {
			.bLength = sizeof(interface_descriptor_t),
			.bDescriptorType = 4,
			.bInterfaceNumber = 0,
			.bAlternateSetting = 0,
			.bNumEndpoints = 2,
			.bInterfaceClass = 0xff,
			.bInterfaceSubClass = 0x42,
			.bInterfaceProtocol = 0x03,
			.iInterface = 0,
		},
		.eps = (endpoint_descriptor_t[]){{
			.bLength = sizeof(endpoint_descriptor_t),
			.bDescriptorType = 5,
			.bEndpointAddress = CONFIG_FASTBOOT_EP_OUT,
			.bmAttributes = 2, // Bulk
			.wMaxPacketSize = 512,
			.bInterval = 9,
		},
		{
			.bLength = sizeof(endpoint_descriptor_t),
			.bDescriptorType = 5,
			.bEndpointAddress = 0x80 | CONFIG_FASTBOOT_EP_IN,
			.bmAttributes = 2, // Bulk
			.wMaxPacketSize = 512,
			.bInterval = 9,
		}},
		.init = NULL,
		.handle_packet = fastboot_packet,
		.handle_setup = NULL,
	},
	},
};

static struct usbdev_ctrl *udc = NULL;

/* UDC string table operations */
#define MAX_STR_COUNT		256
static const char *str_table[MAX_STR_COUNT];

/* Index starts from 1 because 0 has special meaning in USB string table. */
#define STRING_TABLE_EMPTY	1
static uint8_t str_table_index = STRING_TABLE_EMPTY;

static uint8_t udc_add_string(const char *str)
{
	assert(str_table_index < MAX_STR_COUNT);

	uint8_t ret = str_table_index;

	str_table[str_table_index] = str;
	str_table_index++;

	return ret;
}

static inline uint8_t udc_string_table_count(void)
{
	return str_table_index;
}

static inline const char **udc_string_table(void)
{
	return str_table;
}

static inline void udc_string_table_reset(void)
{
	str_table_index = STRING_TABLE_EMPTY;
}

int usb_gadget_init(void)
{
	if (lib_sysinfo.serialno)
		device_descriptor.iSerialNumber =
			udc_add_string(lib_sysinfo.serialno);

	fastboot_chipset_init(&udc, &device_descriptor);
	if (udc == NULL)
		return 0;

	uint8_t count = udc_string_table_count();

	if (count != STRING_TABLE_EMPTY)
		udc->add_strings(LANG_EN_US, count, udc_string_table());

	udc->add_gadget(udc, &fastboot_config);
	return 1;
}

size_t usb_gadget_send(const char *msg, size_t size)
{
	void *tmp = udc->alloc_data(size);
	memcpy(tmp, msg, size);
	udc->enqueue_packet(udc, CONFIG_FASTBOOT_EP_IN, 1, tmp, size, 0, 1);
	return size;
}

static void usb_gadget_force_shutdown(void)
{
	udc_string_table_reset();
	udc->force_shutdown(udc);
}

size_t usb_gadget_recv(void *pkt, size_t size)
{
	/* max 64 packets at once */
	const uint32_t blocksize = 64 * 512;
	size_t total = 0;
	size_t remaining = size;
	if (size > blocksize)
		size = blocksize;

	void *tmp = udc->alloc_data(size);

	/* wait until the device is ready */
	while (!udc->initialized)
		udc->poll(udc);

	while (remaining != 0) {
		out_length = 0;
		if (size > remaining)
			size = remaining;

		udc->enqueue_packet(udc, CONFIG_FASTBOOT_EP_OUT, 0,
			tmp, size, 0, 0);
		while ((out_length == 0) && udc->initialized)
			udc->poll(udc);

		/* If lost connection, re-initialize gadget mode. */
		if (!udc->initialized) {
			usb_gadget_force_shutdown();
			usb_gadget_init();
			return 0;
		}

		memcpy(pkt, tmp, out_length);

		total += out_length;
		remaining -= out_length;
		pkt += out_length;

		/* short transfer should only happen at the end */
		if (out_length < size)
			break;
	}
	udc->free_data(tmp);
	return total;
}

void usb_gadget_stop(void)
{
	udc_string_table_reset();
	udc->shutdown(udc);
}
