/*
  Copyright 2004 Feyd
  Copyright 2004, 2005, 2006 Jean-Baptiste Note

  This file is part of prism54usb.

  prism54usb 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.

  prism54usb 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 prism54usb; if not, write to the Free Software Foundation,
  Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

*/

#include <linux/config.h>
#include <linux/kernel.h>
#include <linux/errno.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/module.h>
#include <linux/smp_lock.h>
#include <linux/completion.h>
#include <linux/usb.h>
#include <linux/pci.h>
#include <linux/firmware.h>
#include <asm/uaccess.h>

#include "isl_38xx.h"
#include "islusb_dev.h"
#include "isl_sm.h"
#include "islsm_log.h"


#include "islusb_net2280.h"
#include "islusb_3887.h"

static const struct usb_device_id p54u_table[] = {
	/* Version 1 devices (pci chip + net2280) */
	{USB_DEVICE(0x0506, 0x0a11)},	/* 3COM 3CRWE254G72 */
	{USB_DEVICE(0x0707, 0xee06)},	/* SMC 2862W-G */
	{USB_DEVICE(0x083a, 0x4501)},	/* Accton 802.11g WN4501 USB */
	{USB_DEVICE(0x083a, 0x4502)},	/* Siemens Gigaset USB Adapter */
	{USB_DEVICE(0x0846, 0x4200)},	/* Netgear WG121 */
	{USB_DEVICE(0x0846, 0x4210)},	/* Netgear WG121 the second ? */
	{USB_DEVICE(0x0846, 0x4220)},	/* Netgear WG111 */
	{USB_DEVICE(0x0cde, 0x0006)},	/* Medion 40900, Roper Europe */
	{USB_DEVICE(0x124a, 0x4023)},	/* Shuttle PN15, Airvast WM168g, IOGear GWU513 */
	{USB_DEVICE(0x1915, 0x2234)},	/* Linksys WUSB54G OEM */
	{USB_DEVICE(0x1915, 0x2235)},	/* Linksys WUSB54G Portable OEM */
	{USB_DEVICE(0x2001, 0x3701)},	/* DLink DWL-G120 Spinnaker */
	{USB_DEVICE(0x2001, 0x3703)},	/* DLink DWL-G122 */
	{USB_DEVICE(0x5041, 0x2234)},	/* Linksys WUSB54G */
	{USB_DEVICE(0x5041, 0x2235)},	/* Linksys WUSB54G Portable */

	/* Version 2 devices (3887) */
	{USB_DEVICE(0x050D, 0x7050)},	/* Belkin F5D7050 ver 1000 */
	{USB_DEVICE(0x0572, 0x2000)},	/* Cohiba Proto board */
	{USB_DEVICE(0x0572, 0x2002)},	/* Cohiba Proto board */
	{USB_DEVICE(0x0707, 0xee13)},   /* SMC 2862W-G version 2 */
	{USB_DEVICE(0x083a, 0x4521)},   /* Siemens Gigaset USB Adapter 54 version 2 */
	{USB_DEVICE(0x0846, 0x4240)},	/* Netgear WG111 */
	{USB_DEVICE(0x0915, 0x2000)},	/* Cohiba Proto board */
	{USB_DEVICE(0x0915, 0x2002)},	/* Cohiba Proto board */
	{USB_DEVICE(0x0baf, 0x0118)},   /* U.S. Robotics U5 802.11g Adapter*/
	{USB_DEVICE(0x0BF8, 0x1009)},   /* FUJITSU E-5400 USB D1700*/
	{USB_DEVICE(0x0cde, 0x0006)},   /* Medion MD40900 */
	{USB_DEVICE(0x0cde, 0x0008)},	/* Sagem XG703A */
	{USB_DEVICE(0x0D8E, 0x3762)},	/* DLink DWL-G120 Cohiba */
	{USB_DEVICE(0x09AA, 0x1000)},	/* Spinnaker Proto board */
	{USB_DEVICE(0x1435, 0x0427)},	/* Inventel UR054G */
	{USB_DEVICE(0x413C, 0x8102)},	/* Spinnaker DUT */
	{USB_DEVICE(0x413C, 0x8104)},	/* Cohiba Proto board */
	{}
};

MODULE_DESCRIPTION("Prism54 USB Driver");
MODULE_AUTHOR
    ("Feyd <feyd@seznam.cz>, Jean-Baptiste Note <jean-baptiste.note@m4x.org>");
MODULE_LICENSE("GPL");
MODULE_DEVICE_TABLE(usb, p54u_table);

static int
p54u_alloc_buffers(struct p54u *p54u)
{
	struct usb_interface *interface = p54u->interface;
	struct usb_host_interface *iface_desc = &interface->altsetting[0];
	struct usb_endpoint_descriptor *desc;
	int err = 0, i;

	FN_ENTER;

	for (i = 0; i < iface_desc->desc.bNumEndpoints; ++i) {
		desc = &iface_desc->endpoint[i].desc;
		switch (desc->bEndpointAddress) {
		case P54U_PIPE_DATA:
			err = p54u_queue_init(p54u, desc, &p54u->data_tx);
			break;
		case P54U_PIPE_MGMT:
			err = p54u_queue_init(p54u, desc, &p54u->mgmt_tx);
			break;
		case P54U_PIPE_DATA | USB_DIR_IN:
			err = p54u_queue_init(p54u, desc, &p54u->data_rx);
			break;
		case P54U_PIPE_MGMT | USB_DIR_IN:
			err = p54u_queue_init(p54u, desc, &p54u->mgmt_rx);
			break;
		case P54U_PIPE_INT | USB_DIR_IN:
			err = p54u_int_queue_init(p54u, desc);
			break;
		default:
			islog(L_DEBUG, "unknown %02x endpoint left alone\n",
			      desc->bEndpointAddress);
			break;
		}
		if (err)
			goto do_err;
	}
	FN_EXIT1(0);
	return 0;

      do_err:
	/* FIXME : free buffers */
	FN_EXIT1(err);
	return err;
}

/* kicks the hardware state machine on */
static int
p54u_boot(struct islsm *islsm)
{
	struct p54u *p54u = P54U_OF_ISLSM(islsm);
	const struct firmware *fw_entry;
	int err = 0;
	int try = 0;

	FN_ENTER;

	/* do nothing if we're already up */
	if (p54u->state == P54U_RUN) {
		FN_EXIT1(0);
		return 0;
	}

	/* Allocations. Here only software can fault */
	p54u->state = P54U_BOOT;

	err = islsm_request_firmware(&fw_entry, islsm);
	if (err)
		goto exit;

	do {
		err = islsm->isl_load_fw(islsm, fw_entry);
		try++;
	} while(err && try < 3);

	if (err) {
		printk(KERN_ERR "%s: Boot the device failed: %i\n",
		       DRV_NAME, err);
		goto exit_release_firmware;
	}

	p54u->state = P54U_RUN;

 exit_release_firmware:
	release_firmware(fw_entry);
 exit:
	FN_EXIT1(err);
	return err;
}

/* TODO: Devices only have one setting, and it is fixed. Separate
 * version 1 and version 2 validation
 */
static int
p54u_validate_device(struct usb_interface *interface,
		     const struct usb_device_id *id)
{
	/* We'll check that the device has the required endpoints in at
	 * least one of its alternate configurations. returns -1 in case there's
	 * not, otherwise, the index of the first alternate
	 * configuration which matches what we need */

	struct usb_endpoint_descriptor *epdesc;
	struct usb_host_interface *curint = interface->altsetting;
	unsigned int i, j;
	islog(L_DEBUG, "%s: %i alternate settings for device\n",
	      DRV_NAME, interface->num_altsetting);
	for (i = 0; i < interface->num_altsetting; i++) {
		unsigned int recognized_pipes = 0;

		for (j = 0; j < curint->desc.bNumEndpoints; j++) {
			epdesc = &curint->endpoint[j].desc;
			switch (epdesc->bEndpointAddress) {
			case P54U_PIPE_DATA:
			case P54U_PIPE_MGMT:
			case P54U_PIPE_BRG:
			case P54U_PIPE_DEV:
			case P54U_PIPE_DATA | USB_DIR_IN:
			case P54U_PIPE_MGMT | USB_DIR_IN:
			case P54U_PIPE_BRG | USB_DIR_IN:
			case P54U_PIPE_DEV | USB_DIR_IN:
			case P54U_PIPE_INT | USB_DIR_IN:
				recognized_pipes++;
				islog(L_DEBUG, "%s: recognized endpoint %02x\n",
				      DRV_NAME, epdesc->bEndpointAddress);
				break;
			default:
				islog(L_DEBUG,
				      "%s: unrecognized endpoint %02x\n",
				      DRV_NAME, epdesc->bEndpointAddress);
			}
		}
		if (recognized_pipes >= P54U_PIPE_NUMBER) {
			islog(L_DEBUG,
			      "%s: Alternate setting %i ok for driver\n",
			      DRV_NAME, i);
			return i;
		}
		curint++;
	}
	return -1;
}

/* manages allocation of the netdev and 802.11 resources */
static int
p54u_probe(struct usb_interface *interface, const struct usb_device_id *id)
{
	struct usb_device *usbdev = interface_to_usbdev(interface);
	struct net_device *netdev;
	struct p54u *p54u;
	struct islsm *islsm;
	int error;
	int conf_index;

	usb_get_dev(usbdev);

	islog(L_DEBUG,
	      "%s: Prism54 USB Device Probe (Device number:%d): "
	      "0x%4.4x:0x%4.4x:0x%4.4x\n", DRV_NAME,
	      usbdev->devnum, (int) usbdev->descriptor.idVendor,
	      (int) usbdev->descriptor.idProduct,
	      (int) usbdev->descriptor.bcdDevice);
	islog(L_DEBUG, "%s: Device at %p\n", DRV_NAME, usbdev);
	islog(L_DEBUG, "%s: Descriptor length: %x type: %x\n", DRV_NAME,
	      (int) usbdev->descriptor.bLength,
	      (int) usbdev->descriptor.bDescriptorType);

	conf_index = p54u_validate_device(interface, id);

	/* do this allocation elsewhere ? */
	netdev = alloc_islsm(sizeof (struct p54u));
	if (!netdev) {
		printk(KERN_ERR "%s: failed to allocate netdevice\n", DRV_NAME);
		return -ENOMEM;
	}

	islsm = ISLSM_OF_NETDEV(netdev);
	p54u = P54U_OF_NETDEV(netdev);

	p54u->interface = interface;
	p54u->usbdev = usbdev;

	SET_MODULE_OWNER(netdev);

	// FIXME
	usb_set_intfdata(interface, islsm);

	/* version-dependent initialization */
	/* we take the hardware up to the point where it is ready to
	   talk to us via the ROM */
	if (conf_index == 0) {
		printk(KERN_INFO
		       "%s: suitable configuration found for net2280 + PCI device\n",
		       DRV_NAME);
		p54u->device_version = P54U_DEVICE_VER1;
		init_MUTEX(&p54u->reg_sem);
		islusb_fill_net2280(islsm);
		strcpy(islsm->firmware_name, P54U_IMAGE_FILE);
	} else {
		printk(KERN_INFO
		       "%s: No suitable configuration found, trying 3887 support\n",
		       DRV_NAME);
		p54u->device_version = P54U_DEVICE_VER2;
		islusb_fill_3887(islsm);
		strcpy(islsm->firmware_name, IMAGE_FILE_3887);
	}

	islsm->isl_boot = p54u_boot;
	error = p54u_alloc_buffers(p54u);

	if (error) {
		printk(KERN_ERR "%s: Failed to allocate buffers", DRV_NAME);
		goto do_cleanup_buf;
	}
	// usb_put_dev(usbdev);
	// I don't know what this means, if this is needed at all...
	SET_NETDEV_DEV(netdev, &interface->dev);

	error = register_islsm(netdev);
	if (error) {
		printk(KERN_ERR "%s: Failed to register islsm\n", DRV_NAME);
		goto do_cleanup_setup;
	}
#ifdef PCIUART
	error = uart_init_dev(islsm);
	if (error)
		goto do_cleanup_uart;
#endif				/* PCIUART */

	/* in case of a version 1 device, we want to setup the net2280
	 * right away and activate the bootrom */
	if (p54u->device_version == P54U_DEVICE_VER1)
		p54u_setup_rom(p54u);

	return 0;

#ifdef PCIUART
	uart_release_dev(islsm);
      do_cleanup_uart:
#endif				/* PCIUART */
	unregister_islsm(netdev);
	/* FIXME */
      do_cleanup_setup:
	//p54u_free_buffers()
      do_cleanup_buf:
	usb_set_intfdata(interface, NULL);
	free_islsm(netdev);
	usb_put_dev(usbdev);
	return -EIO;
}

static void
p54u_disconnect(struct usb_interface *interface)
{
	struct usb_device *usbdev = interface_to_usbdev(interface);
	struct islsm *islsm = usb_get_intfdata(interface);
	struct p54u *p54u = P54U_OF_ISLSM(islsm);
	struct net_device *netdev = NETDEV_OF_ISLSM(islsm);

	islog(L_DEBUG, "prism54 USB device %i disconnecting\n", islsm->minor);

	unregister_islsm(netdev);
	islog(L_DEBUG, "Stopped software state machines\n");

	// Once the madwifi is detached, there is no bh problem any more
	// so this is okay. Previously there were heavy problems with
	// RUN->INIT transition that left bh disabled when running this.

	if (p54u->state == P54U_RUN) {
		p54u->state = P54U_SHUTDOWN;
		p54u_queue_destroy(&p54u->data_rx);
		p54u_queue_destroy(&p54u->mgmt_rx);
		p54u_queue_destroy(&p54u->data_tx);
		p54u_queue_destroy(&p54u->mgmt_tx);
		if (p54u->device_version == P54U_DEVICE_VER1)
			p54u_int_queue_destroy(p54u);
		islog(L_DEBUG, "All queues destroyed\n");
	}
#ifdef PCIUART
	uart_release_dev(islsm);
#endif				/* PCIUART */

	usb_set_intfdata(interface, NULL);
	islog(L_DEBUG, "Cleared interface data\n");

	free_islsm(netdev);
	islog(L_DEBUG, "Freed netdevice\n");

	// could be done earlier
	usb_put_dev(usbdev);

	islog(L_DEBUG, "Disconnect complete\n");
}

static struct usb_driver p54u_driver = {
#if LINUX_VERSION_CODE < KERNEL_VERSION(2,6,16)
	.owner = THIS_MODULE,
#endif
	.name = DRV_NAME,
	.probe = p54u_probe,
	.disconnect = p54u_disconnect,
	.id_table = p54u_table,
};

static int __init
p54u_init(void)
{
	int err;

	/* register this driver with the USB subsystem */
	err = usb_register(&p54u_driver);
	if (err) {
		printk(KERN_ERR "%s: usb_register failed, %d\n",
		       DRV_NAME, err);
	}

	return err;
}

static void __exit
p54u_exit(void)
{
	/* deregister this driver with the USB subsystem */
	usb_deregister(&p54u_driver);
}

module_init(p54u_init);
module_exit(p54u_exit);
