/* iBurst (TM) compatible driver for Linux kernel 2.6
 * based on the original ArrayComm (TM) iBurst (TM) driver.
 * Nicholas Jefferson <xanthophile@gmail.com>
 * 11 May 2005
 *
 * Ported to 2.6 Linux kernel by David Michael Barr.
 *
 * 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.
 *
 * 30 May 2005 - Reference implementation:
 * - Complete conformance with the Linux 2.6 USB API.
 * - Reading now continues until disconnect.
 *   ie no longer wait for interrupt message.
 * - Clean disconnect.
 */

#include <asm/io.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/timer.h>
#include <linux/usb.h>

#include "ib-net.h"

static int debug = 0;
module_param(debug, int, 0);
#define DEBUG(n, args...) if ((n) < debug) printk(KERN_INFO args)

/**
 * enum ib_usb_endpoint - USB pipe endpoints.
 */
enum ib_usb_endpoint
{
	ENDPOINT_OUTPUT = 0,
	ENDPOINT_INPUT = 1,
};

/**
 * struct ib_usb_priv_t - USB device private state.
 * @usbdev: USB device state.
 * @rx_status:
 * @rx_critical:
 * @rx_urb:
 * @tx_urb:
 * @ctl_buf:
 * @intr_buf:
 * @modem: corresponding modem state.
 */
struct ib_usb_priv_t {
	struct usb_device *usbdev;
	int rx_status;
	int rx_critical;
        int endpoints[2];       // endpoint mapping
	struct urb rx_urb;
	struct urb tx_urb;
	u_char ctl_buf[32];
	u_char intr_buf[16];
	struct ib_net_modem_t *modem;
};

static spinlock_t ib_usb_lock = SPIN_LOCK_UNLOCKED;

/**
 * ib_usb_disconnect - USB device disconnected.
 * @usbintf:
 */
static void ib_usb_disconnect(struct usb_interface *usbintf)
{
	u_long state;
	struct ib_usb_priv_t *priv = usb_get_intfdata(usbintf);

	DEBUG(1, "ib_usb_disconnect\n");
	spin_lock_irqsave(&ib_usb_lock, state);
	priv->usbdev = NULL;
	priv->rx_status = 0;
	spin_unlock_irqrestore(&ib_usb_lock, state);
	if (priv->rx_urb.status == -EINPROGRESS)
		usb_kill_urb(&priv->rx_urb);
	priv->rx_critical = 0;
	if (priv->tx_urb.status == -EINPROGRESS)
		usb_kill_urb(&priv->tx_urb);
	ib_net_deregister(priv->modem);
}

/**
 * ib_usb_rx_read - receive cell.
 * @urb:
 * @pt_regs:
 */
static void ib_usb_rx_read(struct urb *urb, struct pt_regs *pt_regs)
{
	u_long state;
	struct ib_usb_priv_t *priv = urb->context;
	struct ib_net_modem_t *modem = priv->modem;

	spin_lock_irqsave(&ib_usb_lock, state);
	priv->rx_critical = 0;
	if (urb->status == 0 && urb->actual_length) {
		ib_net_parse(modem, urb->actual_length);
		ib_net_schedule(modem);
	}
	if (urb->status != -ECONNRESET) {
		urb->actual_length = 0;
		urb->dev = priv->usbdev;
		if (usb_submit_urb(urb, GFP_ATOMIC) == 0)
			priv->rx_critical = 1;
	}
	spin_unlock_irqrestore(&ib_usb_lock, state);
	return;
}

/**
 * ib_usb_tx_done - transfer completed.
 * @urb:
 * @pt_regs:
 */
static void ib_usb_tx_done(struct urb *urb, struct pt_regs *pt_regs)
{
	u_long state;
	struct ib_usb_priv_t *priv = urb->context;

	spin_lock_irqsave(&ib_usb_lock, state);
	if (urb->status)
		priv->modem->stats.tx_errors += 1;
	if (urb->status != -ECONNRESET)
		ib_net_schedule(priv->modem);
	spin_unlock_irqrestore(&ib_usb_lock, state);
}

/**
 * ib_usb_poll - poll device.
 * @_priv: device private state.
 */
static void ib_usb_poll(void *_priv)
{
	struct ib_usb_priv_t *priv = _priv;
	struct ib_net_modem_t *modem;
	int len;

	if(priv == NULL) {
		/* can't happen */
		DEBUG(1,"ib-usb: NULL passed to ib_usb_poll\n");
		return;
	}
	modem = priv->modem;
	if(modem == NULL) {
		/* can't happen */
		DEBUG(1,"ib-usb: NULL modem in ib_usb_poll\n");
		return;
	}
	if (priv->tx_urb.status == -EINPROGRESS)
		return;
	len = ib_net_fill(modem);
	if (len == 0)
		return;
	usb_fill_bulk_urb(&priv->tx_urb, priv->usbdev,
			usb_sndbulkpipe(priv->usbdev, 
                        	priv->endpoints[ENDPOINT_OUTPUT]),
			modem->tx_buf, IB_NET_CELL_SIZE,
			ib_usb_tx_done, priv);
	priv->tx_urb.transfer_buffer_length = len;
	if (usb_submit_urb(&priv->tx_urb, GFP_KERNEL)) {
		modem->stats.tx_errors += 1;
		return;
	}
	modem->stats.tx_bytes += len;
	modem->stats.tx_packets += 1;
}

/**
 * ib_usb_tx_timeout
 * @_priv: device private state.
 */
static void ib_usb_tx_timeout(void *_priv)
{
	struct ib_usb_priv_t *priv = _priv;

	if (priv == NULL) {
		/* can't happen */
		DEBUG(1,"ib-usb: NULL passed to ib_usb_tx_timeout\n");
		return;
	}
	if (priv->tx_urb.status == -EINPROGRESS) {
		priv->tx_urb.transfer_flags |= URB_ASYNC_UNLINK;
		usb_unlink_urb(&priv->tx_urb);
		priv->tx_urb.transfer_flags &= ~URB_ASYNC_UNLINK;
	}
}

/**
 * ib_usb_net_driver - USB driver callback functions.
 */
static struct ib_net_driver_t ib_usb_net_driver = {
	.name = "usb",
	.poll = ib_usb_poll,
	.stop = NULL,
	.tx_timeout = ib_usb_tx_timeout,
};

/**
 * ib_usb_probe -probe USB device.
 * @usbintf:
 * @id:
 */
static int ib_usb_probe(struct usb_interface *usbintf,
		const struct usb_device_id *id)
{
	struct usb_device *usbdev = interface_to_usbdev(usbintf);
	struct ib_usb_priv_t *priv;
	unsigned char configdata[8] =
	    { 0, 8, 0, 247, 172, 3, 0, 2 };
	int sendlen;

	DEBUG(1, "ib_usb_probe\n");
	if (usb_reset_configuration(usbdev)) {
		DEBUG(1, "ib-usb: usb_reset_configuration failed\n");
		return -EIO;
	}
	priv = kmalloc(sizeof(struct ib_usb_priv_t), GFP_KERNEL);
	if (priv == NULL) {
		DEBUG(1, "ib-usb: no memory available\n");
		return -ENOMEM;
	}
	memset(priv, 0, sizeof(struct ib_usb_priv_t)); /* hack */
	priv->usbdev = usbdev;
	usb_init_urb(&priv->rx_urb);
	usb_init_urb(&priv->tx_urb);
	if (usb_control_msg(usbdev, usb_rcvctrlpipe(usbdev, 0), 99, 0xc0,
			cpu_to_le16(0), cpu_to_le16(0), priv->ctl_buf,
			cpu_to_le16(8), 0) != 8) {
		DEBUG(1, "ib-usb: invalid read count\n");
	}
	if (priv->ctl_buf[1] == 99) {
        	priv->endpoints[ENDPOINT_OUTPUT] = 1;
                priv->endpoints[ENDPOINT_INPUT] = 2;
        } else if (priv->ctl_buf[1] == 77) {
        	priv->endpoints[ENDPOINT_OUTPUT] = 2;
                priv->endpoints[ENDPOINT_INPUT] = 3;
        } else {
		DEBUG(1, "ib-usb: invalid response\n");
		goto failed;
	}
        if (usb_bulk_msg(usbdev,
			usb_sndbulkpipe(usbdev,
				priv->endpoints[ENDPOINT_OUTPUT]),
			configdata, 
			sizeof(configdata),
			&sendlen,
			(3*HZ) )) {
                DEBUG(1, "ib-usb: err sending vendor config data\n");
		goto failed;
	}
	priv->modem = ib_net_register(NULL);
	if (priv->modem == NULL) {
		DEBUG(1, "ib-usb: ib_net_register failed\n");
		goto failed;
	}
	priv->modem->driver = &ib_usb_net_driver;
	priv->modem->instance = priv;
	ib_net_addr(priv->modem, priv->ctl_buf + 2);
	usb_fill_bulk_urb(&priv->rx_urb, usbdev,
			usb_rcvbulkpipe(usbdev, 
                        	priv->endpoints[ENDPOINT_INPUT]),
			priv->modem->rx_buf, IB_NET_CELL_SIZE,
			ib_usb_rx_read, priv);
	usb_fill_bulk_urb(&priv->tx_urb, usbdev,
			usb_sndbulkpipe(priv->usbdev, 
                        	priv->endpoints[ENDPOINT_OUTPUT]),
			priv->modem->tx_buf, IB_NET_CELL_SIZE,
			ib_usb_tx_done, priv);
	priv->tx_urb.transfer_flags |= URB_ZERO_PACKET;
	usb_set_intfdata(usbintf, priv);
	priv->rx_urb.actual_length = 0;
	priv->rx_urb.dev = priv->usbdev;
	if (usb_submit_urb(&priv->rx_urb, GFP_ATOMIC) == 0)
		priv->rx_critical = 1;
	return 0;
failed:
	kfree(priv);
	return -ENODEV;
}

/**
 * ib_usb_table - USB device table.
 */
static struct usb_device_id ib_usb_table[3];
MODULE_DEVICE_TABLE(usb, ib_usb_table);

/**
 * ib_usb_driver - USB driver.
 */
static struct usb_driver ib_usb_driver = {
	.owner = THIS_MODULE,
	.name = "iburst",
	.probe = ib_usb_probe,
	.disconnect = ib_usb_disconnect,
	.id_table = ib_usb_table,
};

/**
 * ib_usb_init - ib-usb module init.
 */
static int __init ib_usb_init(void)
{
	DEBUG(1, "ib_usb_init\n");
	ib_usb_table[0].idVendor = 0x0d14;
	ib_usb_table[0].idProduct = 0x0009;
	ib_usb_table[1].idVendor = 0x0482;
	ib_usb_table[1].idProduct = 0x0204;
	if (usb_register(&ib_usb_driver) < 0)
		DEBUG(1, "ib-usb: usb_register failed.\n");
	return 0;
}

/**
 * ib_usb_exit - ib-usb module exit.
 */
static void __exit ib_usb_exit(void)
{
	usb_deregister(&ib_usb_driver);
}

module_init(ib_usb_init);
module_exit(ib_usb_exit);

MODULE_DESCRIPTION("iBurst compatible USB driver");
MODULE_LICENSE("GPL");
