/*****************************************************************************
 * Copyright (c) 2013 IBM Corporation
 * All rights reserved.
 * This program and the accompanying materials
 * are made available under the terms of the BSD License
 * which accompanies this distribution, and is available at
 * http://www.opensource.org/licenses/bsd-license.php
 *
 * Contributors:
 *     IBM Corporation - initial implementation
 *****************************************************************************/

#include <string.h>
#include "usb-core.h"

#undef DEBUG
//#define DEBUG
#ifdef DEBUG
#define dprintf(_x ...) do { printf(_x); } while(0)
#else
#define dprintf(_x ...)
#endif

#define __unused __attribute__((unused))

struct usb_hcd_ops *head;
struct usb_dev *devpool;
#define USB_DEVPOOL_SIZE 4096

static struct usb_dev *usb_alloc_devpool(void)
{
	struct usb_dev *head, *curr, *prev;
	unsigned int dev_count = 0, i;

	head = SLOF_alloc_mem(USB_DEVPOOL_SIZE);
	if (!head)
		return NULL;

	dev_count = USB_DEVPOOL_SIZE/sizeof(struct usb_dev);
	dprintf("%s: %d number of devices\n", __func__, dev_count);
	/* Although an array, link them*/
	for (i = 0, curr = head, prev = NULL; i < dev_count; i++, curr++) {
		if (prev)
			prev->next = curr;
		curr->next = NULL;
		prev = curr;
	}

#ifdef DEBUG
	for (i = 0, curr = head; curr; curr = curr->next)
		printf("%s: %d dev %p\n", __func__, i++, curr);
#endif

	return head;
}

struct usb_dev *usb_devpool_get(void)
{
	struct usb_dev *new;

	if (!devpool) {
		devpool = usb_alloc_devpool();
		if (!devpool)
			return NULL;
	}

	new = devpool;
	devpool = devpool->next;
	memset(new, 0, sizeof(*new));
	new->next = NULL;
	return new;
}

void usb_devpool_put(struct usb_dev *dev)
{
	struct usb_dev *curr;
	if (!dev && !devpool)
		return;

	curr = devpool;
	while (curr->next)
		curr = curr->next;
	curr->next = dev;
	dev->next = NULL;
}

#ifndef DEBUG
#define validate_hcd_ops(dev) (dev && dev->hcidev && dev->hcidev->ops)
#else
int validate_hcd_ops(struct usb_dev *dev)
{
	int ret = true;

	if (!dev) {
		printf("dev is NULL\n");
		ret = false;
	} else if (!dev->hcidev) {
		printf("hcidev is NULL\n");
		ret = false;
	} else if (!dev->hcidev->ops)  {
		printf("ops is NULL\n");
		ret = false;
	}
	return ret;
}
#endif

struct usb_pipe *usb_get_pipe(struct usb_dev *dev, struct usb_ep_descr *ep,
			char *buf, size_t len)
{
	if (validate_hcd_ops(dev) && dev->hcidev->ops->get_pipe)
		return dev->hcidev->ops->get_pipe(dev, ep, buf, len);
	else {
		printf("%s: Failed\n", __func__);
		return NULL;
	}
}

void usb_put_pipe(struct usb_pipe *pipe)
{
	struct usb_dev *dev = NULL;
	if (pipe && pipe->dev) {
		dev = pipe->dev;
		if (validate_hcd_ops(dev) && dev->hcidev->ops->put_pipe)
			dev->hcidev->ops->put_pipe(pipe);
	}
}

int usb_poll_intr(struct usb_pipe *pipe, uint8_t *buf)
{
	struct usb_dev *dev = NULL;
	if (pipe && pipe->dev) {
		dev = pipe->dev;
		if (validate_hcd_ops(dev) && dev->hcidev->ops->poll_intr)
			return dev->hcidev->ops->poll_intr(pipe, buf);
	}
	return 0;
}

void usb_hcd_register(struct usb_hcd_ops *ops)
{
	struct usb_hcd_ops *list;

	if (!ops)
		printf("Error");
	dprintf("Registering %s %d\n", ops->name, ops->usb_type);

	if (head) {
		list = head;
		while (list->next)
			list = list->next;
		list->next = ops;
	} else
		head = ops;
}

void usb_hcd_init(void *hcidev)
{
	struct usb_hcd_dev *dev = hcidev;
	struct usb_hcd_ops *list = head;

	if (!dev) {
		printf("Device Error");
		return;
	}

	while (list) {
		if (list->usb_type == dev->type) {
			dprintf("usb_ops(%p) for the controller found\n", list);
			dev->ops = list;
			dev->ops->init(dev);
			return;
		}
		list = list->next;
	}

	dprintf("usb_ops for the controller not found\n");
}

void usb_hcd_exit(void *_hcidev)
{
	struct usb_hcd_dev *hcidev = _hcidev;

	dprintf("%s: enter \n", __func__);
	if (!hcidev) {
		printf("Device Error");
		return;
	}

	if (hcidev->ops->exit)
		hcidev->ops->exit(hcidev);
}

int usb_send_ctrl(struct usb_pipe *pipe, struct usb_dev_req *req, void *data)
{
	struct usb_dev *dev = NULL;
	if (!pipe)
		return false;
	dev = pipe->dev;
	if (validate_hcd_ops(dev) && dev->hcidev->ops->send_ctrl)
		return dev->hcidev->ops->send_ctrl(pipe, req, data);
	else {
		printf("%s: Failed\n", __func__);
		return false;
	}
}

int usb_transfer_ctrl(void *dev, void *req, void *data)
{
	struct usb_pipe *pipe = NULL;
	struct usb_dev *usbdev;

	if (!dev)
		return false;
	usbdev = (struct usb_dev *)dev;
	pipe = usbdev->control;
	return usb_send_ctrl(pipe, req, data);
}

int usb_transfer_bulk(void *dev, int dir, void *td, void *td_phys, void *data, int size)
{
	struct usb_pipe *pipe = NULL;
	struct usb_dev *usbdev;

	if (!dev)
		return false;
	usbdev = (struct usb_dev *)dev;
	pipe = (dir == USB_PIPE_OUT) ? usbdev->bulk_out : usbdev->bulk_in;
	if (!pipe)
		return false;
	if (validate_hcd_ops(usbdev) && usbdev->hcidev->ops->transfer_bulk)
		return usbdev->hcidev->ops->transfer_bulk(pipe, td, td_phys, data, size);
	else {
		printf("%s: Failed\n", __func__);
		return false;
	}
}

/*
 * USB Specification 1.1
 *     9.3 USB Device Requests
 *     9.4 Standard Device Requests
 */
static int usb_set_address(struct usb_dev *dev, uint32_t port)
{
	struct usb_dev_req req;
	struct usb_hcd_dev *hcidev;

	if (!dev)
		return false;

	hcidev = dev->hcidev;
	req.bmRequestType = 0;
	req.bRequest = REQ_SET_ADDRESS;
	req.wIndex = 0;
	req.wLength = 0;
	req.wValue = cpu_to_le16((uint16_t)(hcidev->nextaddr));
	if (usb_send_ctrl(dev->control, &req, NULL)) {
		dev->addr = hcidev->nextaddr++;
		return true;
	} else
		return false;
}

static int usb_get_device_descr(struct usb_dev *dev, void *data, size_t size)
{
	struct usb_dev_req req;

	if (!dev)
		return false;

	req.bmRequestType = 0x80;
	req.bRequest = REQ_GET_DESCRIPTOR;
	req.wIndex = 0;
	req.wLength = cpu_to_le16((uint16_t) size);
	req.wValue = cpu_to_le16(DESCR_TYPE_DEVICE << 8);
	return usb_send_ctrl(dev->control, &req, data);
}

static int usb_get_config_descr(struct usb_dev *dev, void *data, size_t size)
{
	struct usb_dev_req req;

	if (!dev)
		return false;

	req.bmRequestType = 0x80;
	req.bRequest = REQ_GET_DESCRIPTOR;
	req.wIndex = 0;
	req.wLength = cpu_to_le16((uint16_t) size);
	req.wValue = cpu_to_le16(DESCR_TYPE_CONFIGURATION << 8);
	return usb_send_ctrl(dev->control, &req, data);

}

static int usb_set_config(struct usb_dev *dev, uint8_t cfg_value)
{
	struct usb_dev_req req;

	if (!dev)
		return false;

	req.bmRequestType = 0x00;
	req.bRequest = REQ_SET_CONFIGURATION;
	req.wIndex = 0;
	req.wLength = 0;
	req.wValue = cpu_to_le16(0x00FF & cfg_value);
	return usb_send_ctrl(dev->control, &req, NULL);
}

static int usb_clear_halt(struct usb_pipe *pipe)
{
	struct usb_dev_req req;
	struct usb_dev *dev;

	if (pipe && pipe->dev) {
		dev = pipe->dev;
		dprintf("Clearing port %d dir %d type %d\n",
			pipe->epno, pipe->dir, pipe->type);
		req.bmRequestType = REQT_DIR_OUT | REQT_REC_EP;
		req.bRequest = REQ_CLEAR_FEATURE;
		req.wValue = FEATURE_ENDPOINT_HALT;
		req.wIndex = cpu_to_le16(pipe->epno | pipe->dir);
		req.wLength = 0;
		return usb_send_ctrl(dev->control, &req, NULL);
	}
	return false;
}

int usb_dev_populate_pipe(struct usb_dev *dev, struct usb_ep_descr *ep,
			void *buf, size_t len)
{
	uint8_t dir, type;

	dir = (ep->bEndpointAddress & 0x80) >> 7;
	type = ep->bmAttributes & USB_EP_TYPE_MASK;

	dprintf("EP: %s: %d size %d type %d\n", dir ? "IN " : "OUT",
		ep->bEndpointAddress & 0xF, le16_to_cpu(ep->wMaxPacketSize),
		type);
	if (type == USB_EP_TYPE_BULK) {
		if (dir)
			dev->bulk_in = usb_get_pipe(dev, ep, buf, len);
		else
			dev->bulk_out = usb_get_pipe(dev, ep, buf, len);
	} else if (type == USB_EP_TYPE_INTR)
		dev->intr = usb_get_pipe(dev, ep, buf, len);

	return true;
}

static void usb_dev_copy_epdesc(struct usb_dev *dev, struct usb_ep_descr *ep)
{
	uint32_t ep_cnt;

	ep_cnt = dev->ep_cnt;
	if (ep_cnt < USB_DEV_EP_MAX)
		memcpy((void *)&dev->ep[ep_cnt], ep, sizeof(*ep));
	else
		dprintf("usb-core: only %d EPs supported\n", USB_DEV_EP_MAX);
	dev->ep_cnt++;
}

int usb_hid_init(void *vdev)
{
	struct usb_dev *dev;
	dev = (struct usb_dev *) vdev;
	if (!dev)
		return false;
	if (dev->class == DEV_HID_KEYB)
		usb_hid_kbd_init(dev);
	return true;
}

int usb_hid_exit(void *vdev)
{
	struct usb_dev *dev;
	dev = (struct usb_dev *) vdev;
	if (!dev)
		return false;
	if (dev->class == DEV_HID_KEYB)
		usb_hid_kbd_exit(dev);
	return true;
}

int usb_msc_init(void *vdev)
{
	struct usb_dev *dev;
	int i;

	dev = (struct usb_dev *) vdev;
	dprintf("%s: enter %x\n", __func__, dev->class);
	if (!dev)
		return false;
	if (usb_get_intf_class(dev->class) == 8) {
		for (i = 0; i < dev->ep_cnt; i++) {
			if ((dev->ep[i].bmAttributes & USB_EP_TYPE_MASK)
				== USB_EP_TYPE_BULK)
				usb_dev_populate_pipe(dev, &dev->ep[i], NULL, 0);
		}
	}
	return true;
}

int usb_msc_exit(void *vdev)
{
	struct  usb_dev *dev;
	dev = (struct usb_dev *) vdev;
	dprintf("%s: enter %x\n", __func__, dev->class);
	if (!dev)
		return false;
	if (usb_get_intf_class(dev->class) == 8) {
		if (dev->bulk_in)
			usb_put_pipe(dev->bulk_in);
		if (dev->bulk_out)
			usb_put_pipe(dev->bulk_out);
	}
	return true;
}

int usb_msc_reset(struct usb_dev *dev)
{
	struct usb_dev_req req;

	if (!dev)
		return false;

	req.bmRequestType = REQT_TYPE_CLASS | REQT_REC_INTERFACE | REQT_DIR_OUT;
	req.bRequest = 0xFF;
	req.wLength = 0;
	req.wValue = 0;
	req.wIndex = cpu_to_le16(dev->intf_num);
	return usb_send_ctrl(dev->control, &req, NULL);
}

void usb_msc_resetrecovery(struct usb_dev *dev)
{
	// usb_msc_reset(dev);
	usb_clear_halt(dev->bulk_in);
	usb_clear_halt(dev->bulk_out);
	SLOF_msleep(2);
}

static int usb_handle_device(struct usb_dev *dev, struct usb_dev_config_descr *cfg,
		uint8_t *ptr, uint16_t len)
{
	struct usb_dev_intf_descr *intf = NULL;
	struct usb_ep_descr *ep = NULL;
	struct usb_dev_hid_descr *hid __unused = NULL;
	uint8_t desc_len, desc_type;

	len -= sizeof(struct usb_dev_config_descr);
	ptr = (uint8_t *)(ptr + sizeof(struct usb_dev_config_descr));

	while (len > 0) {
		desc_len = *ptr;
		desc_type = *(ptr + 1);
		switch (desc_type) {
		case DESCR_TYPE_INTERFACE:
			intf = (struct usb_dev_intf_descr *)ptr;
			dev->class = intf->bInterfaceClass << 16 |
				intf->bInterfaceSubClass << 8 |
				intf->bInterfaceProtocol;
			break;
		case DESCR_TYPE_ENDPOINT:
			ep = (struct usb_ep_descr *)ptr;
			dev->intf_num = intf->bInterfaceNumber;
			usb_dev_copy_epdesc(dev, ep);
			break;
		case DESCR_TYPE_HID:
			hid = (struct usb_dev_hid_descr *)ptr;
			dprintf("hid-report %d size %d\n",
				hid->bReportType, le16_to_cpu(hid->wReportLength));
			break;
		case DESCR_TYPE_HUB:
			break;
		default:
			dprintf("ptr %p desc_type %d\n", ptr, desc_type);
		}
		ptr += desc_len;
		len -= desc_len;
	}
	return true;
}

int usb_setup_new_device(struct usb_dev *dev, unsigned int port)
{
	struct usb_dev_descr descr;
	struct usb_dev_config_descr cfg;
	struct usb_ep_descr ep;
	uint16_t len;
	void *data = NULL;

	dprintf("usb: %s - port %d\n", __func__, port);

	dev->addr = 0;
	dev->port = port;
	ep.bEndpointAddress = 0;
	ep.bmAttributes = USB_EP_TYPE_CONTROL;
	ep.wMaxPacketSize = cpu_to_le16(8);
	dev->control = usb_get_pipe(dev, &ep, NULL, 0);

	if (!usb_get_device_descr(dev, &descr, 8))
		goto fail;
	dev->control->mps = descr.bMaxPacketSize0;

	/*
	 * For USB3.0 ADDRESS-SLOT command takes care of setting
	 * address, skip this during generic device setup for USB3.0
	 * devices
	 */
	if (dev->speed != USB_SUPER_SPEED) {
		/*
		 * Qemu starts the port number from 1 which was
		 * revealed in bootindex and resulted in mismatch for
		 * storage devices names. Adjusting this here for
		 * compatibility.
		 */
		dev->port = port + 1;
		if(!usb_set_address(dev, dev->port))
			goto fail;
	}
	mb();
	SLOF_msleep(100);

	if (!usb_get_device_descr(dev, &descr, sizeof(struct usb_dev_descr)))
		goto fail;

	if (!usb_get_config_descr(dev, &cfg, sizeof(struct usb_dev_config_descr)))
		goto fail;

	len = le16_to_cpu(cfg.wTotalLength);
	/* No device config descriptor present */
	if (len == sizeof(struct usb_dev_config_descr))
		goto fail;

	data = SLOF_dma_alloc(len);
	if (!data) {
		printf("%s: alloc failed %d\n", __func__, port);
		goto fail;
	}

	if (!usb_get_config_descr(dev, data, len))
		goto fail_mem_free;
	if (!usb_set_config(dev, cfg.bConfigurationValue))
		goto fail_mem_free;
	mb();
	SLOF_msleep(100);

	if (!usb_handle_device(dev, &cfg, data, len))
		goto fail_mem_free;

	SLOF_dma_free(data, len);
	return true;
fail_mem_free:
	SLOF_dma_free(data, len);
fail:
	return false;
}
