#include <common.h>
#include <usb/usb.h>
#include <usb/usbnet.h>
#include <asm/byteorder.h>
#include <errno.h>
#include <malloc.h>

static inline int usb_endpoint_dir_in(const struct usb_endpoint_descriptor *epd)
{
	return ((epd->bEndpointAddress & USB_ENDPOINT_DIR_MASK) == USB_DIR_IN);
}

/* handles CDC Ethernet and many other network "bulk data" interfaces */
int usbnet_get_endpoints(struct usbnet *dev)
{
	struct usb_device	*udev = dev->udev;
	int				tmp;
	struct usb_interface_descriptor	*alt = NULL;
	struct usb_endpoint_descriptor	*in = NULL, *out = NULL;
	struct usb_endpoint_descriptor	*status = NULL;

	for (tmp = 0; tmp < udev->config.no_of_if; tmp++) {
		unsigned	ep;

		in = out = status = NULL;
		alt = &udev->config.if_desc[tmp];

		/* take the first altsetting with in-bulk + out-bulk;
		 * remember any status endpoint, just in case;
		 * ignore other endpoints and altsetttings.
		 */
		for (ep = 0; ep < alt->bNumEndpoints; ep++) {
			struct usb_endpoint_descriptor	*e;
			int				intr = 0;

			e = &alt->ep_desc[ep];
			switch (e->bmAttributes) {
			case USB_ENDPOINT_XFER_INT:
				if (!usb_endpoint_dir_in(e))
					continue;
				intr = 1;
				/* FALLTHROUGH */
			case USB_ENDPOINT_XFER_BULK:
				break;
			default:
				continue;
			}
			if (usb_endpoint_dir_in(e)) {
				if (!intr && !in)
					in = e;
				else if (intr && !status)
					status = e;
			} else {
				if (!out)
					out = e;
			}
		}
		if (in && out)
			break;
	}

	if (!alt || !in || !out)
		return -EINVAL;

	if (alt->bAlternateSetting != 0
			|| !(dev->driver_info->flags & FLAG_NO_SETINT)) {
		tmp = usb_set_interface (dev->udev, alt->bInterfaceNumber,
				alt->bAlternateSetting);
		if (tmp < 0)
			return tmp;
	}

	dev->in = usb_rcvbulkpipe (dev->udev,
			in->bEndpointAddress & USB_ENDPOINT_NUMBER_MASK);
	dev->out = usb_sndbulkpipe (dev->udev,
			out->bEndpointAddress & USB_ENDPOINT_NUMBER_MASK);
	dev_dbg(&dev->edev.dev, "found endpoints: IN=%d OUT=%d\n",
			in->bEndpointAddress, out->bEndpointAddress);

	return 0;
}
EXPORT_SYMBOL(usbnet_get_endpoints);

char tx_buffer[4096];

static int usbnet_send(struct eth_device *edev, void *eth_data, int data_length)
{
	struct usbnet		*dev = edev->priv;
	struct driver_info	*info = dev->driver_info;
	int			len, alen, ret;

	dev_dbg(&edev->dev, "%s\n",__func__);

	/* some devices want funky USB-level framing, for
	 * win32 driver (usually) and/or hardware quirks
	 */
        if(info->tx_fixup) {
                if(info->tx_fixup(dev, eth_data, data_length, tx_buffer, &len)) {
			dev_dbg(&edev->dev, "can't tx_fixup packet");
                        return 0;
                }
        } else {
                len = data_length;
                memmove(tx_buffer, (void*) eth_data, len);
        }

	/* don't assume the hardware handles USB_ZERO_PACKET
	 * NOTE:  strictly conforming cdc-ether devices should expect
	 * the ZLP here, but ignore the one-byte packet.
	 */
	if ((len % dev->maxpacket) == 0)
		tx_buffer[len++] = 0;

	ret = usb_bulk_msg(dev->udev, dev->out, tx_buffer, len, &alen, 1000);
	dev_dbg(&edev->dev, "%s: ret: %d len: %d alen: %d\n", __func__, ret, len, alen);

	return ret;
}

static char rx_buf[4096];

static int usbnet_recv(struct eth_device *edev)
{
	struct usbnet		*dev = (struct usbnet*) edev->priv;
	struct driver_info	*info = dev->driver_info;
	int len, ret, alen = 0;

	dev_dbg(&edev->dev, "%s\n",__func__);

	len = dev->rx_urb_size;

	ret = usb_bulk_msg(dev->udev, dev->in, rx_buf, len, &alen, 1000);
	if (ret)
		return ret;

	if (alen) {
		if (info->rx_fixup)
			return info->rx_fixup(dev, rx_buf, alen);
		else
			net_receive(rx_buf, alen);
	}

        return 0;
}

static int usbnet_init(struct eth_device *edev)
{
	struct usbnet		*dev = (struct usbnet*) edev->priv;
	struct driver_info      *info = dev->driver_info;
	int ret = 0;

	dev_dbg(&edev->dev, "%s\n",__func__);

	/* put into "known safe" state */
	if (info->reset)
		ret = info->reset(dev);

	if (ret) {
                dev_info(&edev->dev, "open reset fail (%d)", ret);
                return ret;
        }

	miidev_restart_aneg(&dev->miidev);

	return 0;
}

static int usbnet_open(struct eth_device *edev)
{
	struct usbnet		*dev = (struct usbnet*)edev->priv;

	dev_dbg(&edev->dev, "%s\n",__func__);

	if (miidev_wait_aneg(&dev->miidev))
		return -1;

	miidev_print_status(&dev->miidev);

	return 0;
}

static void usbnet_halt(struct eth_device *edev)
{
	dev_dbg(&edev->dev, "%s\n",__func__);
}

int usbnet_probe(struct usb_device *usbdev, const struct usb_device_id *prod)
{
	struct usbnet *undev;
	struct eth_device *edev;
	struct driver_info *info;
	int status;

	dev_dbg(&usbdev->dev, "%s\n", __func__);

	undev = xzalloc(sizeof (*undev));

	usbdev->drv_data = undev;

	edev = &undev->edev;
	undev->udev = usbdev;

	edev->open = usbnet_open,
	edev->init = usbnet_init,
	edev->send = usbnet_send,
	edev->recv = usbnet_recv,
	edev->halt = usbnet_halt,
	edev->priv = undev;
	edev->parent = &usbdev->dev;

	info = (struct driver_info *)prod->driver_info;
	undev->driver_info = info;

	if (info->bind) {
		status = info->bind (undev);
		if (status < 0)
			goto out1;
	}

	if (!undev->rx_urb_size)
		undev->rx_urb_size = 1514; /* FIXME: What to put here? */
	undev->maxpacket = usb_maxpacket(undev->udev, undev->out);

	eth_register(edev);

	return 0;
out1:
	dev_dbg(&edev->dev, "err: %d\n", status);
	return status;
}

void usbnet_disconnect(struct usb_device *usbdev)
{
	struct usbnet *undev = usbdev->drv_data;
	struct eth_device *edev = &undev->edev;
	struct driver_info *info;

	eth_unregister(edev);
	unregister_device(&edev->dev);

	info = undev->driver_info;
	if (info->unbind)
		info->unbind(undev);

	free(undev);
}

