/******************************************************************************
  
  Copyright(c) 2003 - 2004 Intel Corporation. All rights reserved.

  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.
  
  The full GNU General Public License is included in this distribution in the
  file called LICENSE.
  
  Contact Information:
  James P. Ketrenos <ipw2100-admin@linux.intel.com>
  Intel Corporation, 5200 N.E. Elam Young Parkway, Hillsboro, OR 97124-6497

******************************************************************************/

/******************************************************************************

  Changes

  0.33 Luc Saillard
       Added support for using firmware hotplug vs. a custom binary loader.

  Portions of ipw2100_mod_firmware_load, ipw2100_do_mod_firmware_load, and 
  ipw2100_fw_load are loosely based on drivers/sound/sound_firmware.c 
  available in the 2.4.25 kernel sources, and are copyright (c) Alan Cox

******************************************************************************/

#include <linux/802_11.h>
#include <linux/compiler.h>
#include <linux/config.h>
#include <linux/errno.h>
#include <linux/if_arp.h>
#include <linux/in6.h>
#include <linux/in.h>
#include <linux/ip.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/netdevice.h>
#ifndef CONFIG_IPW2100_LEGACY_FW_LOAD
#include <linux/firmware.h>
#endif
#include <linux/pci.h>
#include <linux/proc_fs.h>
#include <linux/skbuff.h>
#include <linux/slab.h>
#include <linux/tcp.h>
#include <linux/types.h>
#include <linux/version.h>
#include <asm/io.h>
#include <asm/uaccess.h>
#define __KERNEL_SYSCALLS__
#include <linux/fs.h>
#include <linux/mm.h>
#include <linux/slab.h>
#include <linux/unistd.h>
#include <linux/stringify.h>

#include "ipw2100.h"
#include "ipw2100_hw.h"
#include "ipw2100_fw.h"

#define IPW2100_FW_MAJOR_VERSION 1
#define IPW2100_FW_MINOR_VERSION 0

#define IPW2100_FW_MINOR(x) ((x & 0xff) >> 8)
#define IPW2100_FW_MAJOR(x) (x & 0xff)

#define IPW2100_FW_VERSION ((IPW2100_FW_MINOR_VERSION << 8) | \
                             IPW2100_FW_MAJOR_VERSION)

#define IPW2100_FW_PREFIX "ipw2100-" __stringify(IPW2100_FW_MAJOR_VERSION) \
"." __stringify(IPW2100_FW_MINOR_VERSION)

#define IPW2100_FW_NAME(x) IPW2100_FW_PREFIX "" x ".fw"

#ifdef CONFIG_IPW2100_LEGACY_FW_LOAD

static char *firmware = NULL;

/* Module paramter for path to the firmware*/
#if LINUX_VERSION_CODE < KERNEL_VERSION(2,6,0)

MODULE_PARM(firmware, "s");

#else /* LINUX_VERSION_CODE < 2.6.0 */

#include <linux/moduleparam.h>
module_param(firmware, charp, 0);

#endif /* LINUX_VERSION_CODE < 2.6.0 */

MODULE_PARM_DESC(firmware, "complete path to firmware file");

#endif /* CONFIG_IPW2100_LEGACY_FW_LOAD */


/*

BINARY FIRMWARE HEADER FORMAT

offset      length   desc
0           2        version
2           2        mode == 0:BSS,1:IBSS,2:PROMISC
4           4        fw_len
8           4        uc_len
C           fw_len   firmware data
12 + fw_len uc_len   microcode data

*/

struct ipw2100_fw_header {
	short version;
	short mode;
	unsigned int fw_size;
	unsigned int uc_size;
} __attribute__ ((packed));


#ifdef CONFIG_IPW2100_LEGACY_FW_LOAD

/*
 * 
 * The following was originally based on the mod_firmware_load in
 * drivers/sound/sound_firmware.c.  Primary changes revolved around 
 * making it work for firmware images > 128k and to support having
 * both a firmware and microcode image in the file loaded.
 *
 */
static void ipw2100_fw_free(struct ipw2100_fw *fw)
{
	struct ipw2100_fw_chunk *c;
	struct list_head *e;

	while (!list_empty(&fw->fw.chunk_list)) {
		e = fw->fw.chunk_list.next;
		c = list_entry(e, struct ipw2100_fw_chunk, list);
		list_del(e);
		vfree(c->buf);
		vfree(c);
	}

	while (!list_empty(&fw->uc.chunk_list)) {
		e = fw->uc.chunk_list.next;
		c = list_entry(e, struct ipw2100_fw_chunk, list);
		list_del(e);
		vfree(c->buf);
		vfree(c);
	}
}


static int ipw2100_fw_load(int fd, struct ipw2100_fw_chunk_set *cs, long size)
{
	struct ipw2100_fw_chunk *c;
	int i = 0;
	
	/* Break firmware image into chunks of 128k */
	cs->size = size;
	cs->chunks = cs->size >> 17;
	
	if (cs->size & 0x1FFFF)
		cs->chunks++;
	
	IPW2100_DEBUG_FW("Loading %ld bytes from %u chunks\n",
			 cs->size, cs->chunks);
	
	/* Load the chunks */
	while (size > 0) {
		i++;
		
		c = (struct ipw2100_fw_chunk *)vmalloc(
			sizeof(struct ipw2100_fw_chunk));
		if (c == NULL) {
			printk(KERN_INFO "Out of memory loading firmware "
			       "chunk %d.\n", i);
			goto fail;
		}
		c->pos = 0;
		
		if (size >= 0x20000)
			c->len = 0x20000;
		else
			c->len = size;
		
		c->buf = (unsigned char *)vmalloc(c->len);
		if (c->buf == NULL) {
			printk(KERN_INFO "Out of memory loading firmware "
			       "chunk %d.\n", i);
			goto fail;
			
		}
		if (read(fd, c->buf, c->len) != c->len) {
			printk(KERN_INFO "Failed to read chunk firmware "
			       "chunk %d.\n", i);
			goto fail;
		}
		
		list_add_tail(&c->list, &cs->chunk_list);
		
		IPW2100_DEBUG_FW("Chunk %d loaded: %lu bytes\n",
				 i, c->len);
		size -= c->len;
	}
	
	return 0;
	
 fail:
	return 1;
}

static int errno;
static int ipw2100_do_mod_firmware_load(const char *fn, struct ipw2100_fw *fw)
{
	int fd;
	long l;
	struct ipw2100_fw_header h;

	/* Make sure the lists are init'd so that error paths can safely walk
        * them to free potentially allocated storage */
	INIT_LIST_HEAD(&fw->fw.chunk_list);
	INIT_LIST_HEAD(&fw->uc.chunk_list);
	
	fd = open(fn, 0, 0);
	if (fd == -1) {
		printk(KERN_INFO "Unable to load '%s'.\n", fn);
		return 1;
	}
	l = lseek(fd, 0L, 2);
	lseek(fd, 0L, 0);
	
	IPW2100_DEBUG_FW("Loading %ld bytes for firmware '%s'\n", l, fn);
	
	if (read(fd, (char *)&h, sizeof(h)) != sizeof(h)) {
		printk(KERN_INFO "Failed to read '%s'.\n", fn);
		goto fail;
	}

	
	if (IPW2100_FW_MAJOR(h.version) != IPW2100_FW_MAJOR_VERSION) {
		printk(KERN_WARNING "Firmware image not compatible "
		       "(detected version id of %u). "
		       "See Documentation/networking/README.ipw2100\n",
		       h.version);
		goto fail;
	}

	fw->version = h.version;

	if (ipw2100_fw_load(fd, &fw->fw, h.fw_size))
		goto fail;

	if (ipw2100_fw_load(fd, &fw->uc, h.uc_size))
		goto fail;

	close(fd);
	return 0;

 fail:
	ipw2100_fw_free(fw);
	close(fd);
	return 1;
}


static int ipw2100_mod_firmware_load(const char *fn, struct ipw2100_fw *fw)
{
	int r;
	mm_segment_t fs = get_fs();
	set_fs(get_ds());
	r = ipw2100_do_mod_firmware_load(fn, fw);
	set_fs(fs);
	return r;
}

static inline struct list_head *ipw2100_fw_read(
	struct list_head *e, struct ipw2100_fw_chunk_set *cs,
	unsigned char *data, size_t len)
{
	struct ipw2100_fw_chunk *c = list_entry(e, struct ipw2100_fw_chunk, 
						list);
	unsigned int avail = c->len - c->pos;
	if (avail <= len) {
		struct ipw2100_fw_chunk *tmp;

		memcpy(data, c->buf + c->pos, avail);
		list_del(e);

		IPW2100_DEBUG_INFO("advancing to next chunk...\n");

		e = cs->chunk_list.next;
		tmp = list_entry(e, struct ipw2100_fw_chunk, list);

		if (avail != len) {
			memcpy(data + avail, 
			       tmp->buf + tmp->pos, 
			       len - avail);
			tmp->pos += len - avail;
		}

		vfree(c->buf);
		vfree(c);

		return e;
	} 

	memcpy(data, c->buf + c->pos, len);
	c->pos += len;

	return e;
}

static inline struct list_head *ipw2100_fw_readw(
	struct list_head *e, struct ipw2100_fw_chunk_set *cs, 
	unsigned short *data)
{
	return ipw2100_fw_read(e, cs, (unsigned char *)data, sizeof(*data));
}

static inline struct list_head *ipw2100_fw_readl(
	struct list_head *e, struct ipw2100_fw_chunk_set *cs,
	unsigned int *data)
{
	return ipw2100_fw_read(e, cs, (unsigned char *)data, sizeof(*data));
}

#else /* CONFIG_IPW2100_LEGACY_FW_LOAD */

static int ipw2100_mod_firmware_load(struct ipw2100_fw *fw)
{
	struct ipw2100_fw_header *h = 
		(struct ipw2100_fw_header *)fw->fw_entry->data;

	if (IPW2100_FW_MAJOR(h->version) != IPW2100_FW_MAJOR_VERSION) {
		printk(KERN_WARNING "Firmware image not compatible "
		       "(detected version id of %u). "
		       "See Documentation/networking/README.ipw2100\n",
		       h->version);
		return 1;
	}

	fw->version = h->version;
	fw->fw.data = fw->fw_entry->data + sizeof(struct ipw2100_fw_header);
	fw->fw.size = h->fw_size;
	fw->uc.data = fw->fw.data + h->fw_size;
	fw->uc.size = h->uc_size;

	return 0;
}

#endif /* CONFIG_IPW2100_LEGACY_FW_LOAD */


int ipw2100_get_firmware(struct ipw2100_priv *priv, struct ipw2100_fw *fw)
{
	char *fw_name;

#ifdef CONFIG_IPW2100_LEGACY_FW_LOAD
	int err = 0;

	printk(KERN_DEBUG "%s: Using legacy firmware load.\n",
	       priv->ndev->name);

	if (!firmware) {
		switch (priv->ctx->port_type) {
		case IBSS:
			fw_name = "/etc/firmware/" IPW2100_FW_NAME("i");
			break;
#ifdef CONFIG_IPW2100_PROMISC
		case MONITOR:
			fw_name = "/etc/firmware/" IPW2100_FW_NAME("p");
			break;
#endif
		case BSS:
		default:
			fw_name = "/etc/firmware/" IPW2100_FW_NAME("");
			break;
		}
	} else 
		fw_name = firmware;

	err = ipw2100_mod_firmware_load(fw_name, fw);
	if (err) {
		printk(KERN_ERR "%s: Firmware not available. "
		       "See Documentation/networking/README.ipw2100\n",
		       priv->ndev->name);
		return -EIO;
	}

#else /* CONFIG_IPW2100_LEGACY_FW_LOAD */
	int rc;

	printk(KERN_DEBUG "%s: Using hotplug firmware load.\n",
	       priv->ndev->name);
	
	switch (priv->ctx->port_type) {
	case IBSS:
		fw_name = IPW2100_FW_NAME("i");
		break;
#ifdef CONFIG_IPW2100_PROMISC
	case MONITOR:
		fw_name = IPW2100_FW_NAME("p");
		break;
#endif
	case BSS:
	default:
		fw_name = IPW2100_FW_NAME("");
		break;
	}

#if LINUX_VERSION_CODE < KERNEL_VERSION(2,6,0)
	rc = request_firmware(&fw->fw_entry, fw_name, pci_name(priv->pdev));
#else
	rc = request_firmware(&fw->fw_entry, fw_name, &priv->pdev->dev);
#endif /* LINUX_VERSION_CODE */

	if (rc < 0) {
		printk(KERN_ERR
		       "%s: Firmware '%s' not available or load failed.\n",
		       priv->ndev->name, fw_name);
		return rc;
	}
	IPW2100_DEBUG_INFO("firmware data %p size %d", fw->fw_entry->data,
			   fw->fw_entry->size);

	ipw2100_mod_firmware_load(fw);

#endif /* CONFIG_IPW2100_LEGACY_FW_LOAD */

	return 0;
}

void ipw2100_release_firmware(struct ipw2100_priv *priv,
			      struct ipw2100_fw *fw)
{
#ifdef CONFIG_IPW2100_LEGACY_FW_LOAD

	ipw2100_fw_free(fw);

#else /* CONFIG_IPW2100_LEGACY_FW_LOAD */

	if (fw->fw_entry)
		release_firmware(fw->fw_entry);
	fw->fw_entry = NULL;

#endif /* CONFIG_IPW2100_LEGACY_FW_LOAD */
}


int ipw2100_get_fwversion(struct ipw2100_priv *priv, char *buf, size_t max)
{
	char ver[MAX_FW_VERSION_LEN];
	u32 len = MAX_FW_VERSION_LEN;
	u32 tmp;
	int i;
	/* firmware version is an ascii string (max len of 14) */
	if (ipw2100_get_ordinal(priv, IPW_ORD_STAT_FW_VER_NUM,
				ver, &len)) 
		return -EIO;
	tmp = max;
	if (len >= max)
		len = max - 1;
	for (i = 0; i < len; i++)
		buf[i] = ver[i];
	buf[i] = '\0';
	return tmp;
}

int ipw2100_get_ucodeversion(struct ipw2100_priv *priv, char *buf, size_t max)
{
	u32 ver;
	u32 len = sizeof(ver);
	/* microcode version is a 32 bit integer */
	if (ipw2100_get_ordinal(priv, IPW_ORD_UCODE_VERSION,
				&ver, &len))
		return -EIO;
	return snprintf(buf, max, "%08X", ver);
}

/* 
 * On exit, the firmware will have been freed from the fw list 
 */
int ipw2100_fw_download(struct ipw2100_priv *priv, struct ipw2100_fw *fw)
{
	/* firmware is constructed of N contiguous entries, each entry is
	 * structured as:
	 * 
	 * offset    sie         desc
	 * 0         4           address to write to
	 * 4         2           length of data run
         * 6         length      data
	 */
	unsigned int addr;
	unsigned short len;

#ifdef CONFIG_IPW2100_LEGACY_FW_LOAD
	unsigned char data[32];
	
	struct ipw2100_fw_chunk_set *cs = &fw->fw;
	struct list_head *e = cs->chunk_list.next;

	while (cs->size > 0) {
		e = ipw2100_fw_readl(e, cs, &addr);
		cs->size -= sizeof(addr);

		e = ipw2100_fw_readw(e, cs, &len);
		cs->size -= sizeof(len);

		if (len > 32) {
			printk(KERN_ERR 
			       "Invalid firmware run-length of %d bytes\n",
			       len);
			return -EINVAL;
		}

		e = ipw2100_fw_read(e, cs, data, len);
		cs->size -= len;

		write_nic_memory(priv->ndev, addr, len, data);
	}

#else /* CONFIG_IPW2100_LEGACY_FW_LOAD */

	const unsigned char *firmware_data = fw->fw.data;
	unsigned int firmware_data_left = fw->fw.size;

	while (firmware_data_left > 0) {
	   	addr = *(u32 *)(firmware_data);
		firmware_data      += 4;
		firmware_data_left -= 4;

	   	len = *(u16 *)(firmware_data);
		firmware_data      += 2;
		firmware_data_left -= 2;

		if (len > 32) {
			printk(KERN_ERR
			       "Invalid firmware run-length of %d bytes\n",
			       len);
			return -EINVAL;
		}

		write_nic_memory(priv->ndev, addr, len, firmware_data);
		firmware_data      += len;
		firmware_data_left -= len;
	}
#endif /* CONFIG_IPW2100_LEGACY_FW_LOAD */

	return 0;
}

struct symbol_alive_response {
	u8 cmd_id;
	u8 seq_num;
	u8 ucode_rev;
	u8 eeprom_valid;
	u16 valid_flags;
	u8 IEEE_addr[6];
	u16 flags;
	u16 pcb_rev;
	u16 clock_settle_time;	// 1us LSB
	u16 powerup_settle_time;	// 1us LSB
	u16 hop_settle_time;	// 1us LSB
	u8 date[3];		// month, day, year
	u8 time[2];		// hours, minutes
	u8 ucode_valid;
};

int ipw2100_ucode_download(struct ipw2100_priv *priv, struct ipw2100_fw *fw)
{
	struct net_device *dev = priv->ndev;

#ifdef CONFIG_IPW2100_LEGACY_FW_LOAD

	struct ipw2100_fw_chunk_set *cs = &fw->uc;
	struct list_head *e = cs->chunk_list.next;
	unsigned short uc_data;

#else /* CONFIG_IPW2100_LEGACY_FW_LOAD */

	const unsigned char *microcode_data = fw->uc.data;
	unsigned int microcode_data_left = fw->uc.size;

#endif /* CONFIG_IPW2100_LEGACY_FW_LOAD */

	struct symbol_alive_response response;
	int i, j;
	u8 data;

	/* Symbol control */
	write_nic_word(dev, IPW2100_CONTROL_REG, 0x703);
	write_nic_word(dev, IPW2100_CONTROL_REG, 0x707);

	/* HW config */
	write_nic_byte(dev, 0x210014, 0x72);	/* fifo width =16 */
	write_nic_byte(dev, 0x210014, 0x72);	/* fifo width =16 */

	/* EN_CS_ACCESS bit to reset control store pointer */
	write_nic_byte(dev, 0x210000, 0x40);
	write_nic_byte(dev, 0x210000, 0x0);
	write_nic_byte(dev, 0x210000, 0x40);

	/* copy microcode from buffer into Symbol */
#ifdef CONFIG_IPW2100_LEGACY_FW_LOAD

	while (cs->size > 0) {

		e = ipw2100_fw_readw(e, cs, &uc_data);
		cs->size -= sizeof(uc_data);
		write_nic_byte(dev, 0x210010, uc_data & 0xFF);
		write_nic_byte(dev, 0x210010, (uc_data >> 8) & 0xFF);

	}

#else /* CONFIG_IPW2100_LEGACY_FW_LOAD */

	while (microcode_data_left > 0) {

		write_nic_byte(dev, 0x210010, *microcode_data++);
		write_nic_byte(dev, 0x210010, *microcode_data++);
		microcode_data_left -= 2;

	}

#endif /* CONFIG_IPW2100_LEGACY_FW_LOAD */

	/* EN_CS_ACCESS bit to reset the control store pointer */
	write_nic_byte(dev, 0x210000, 0x0);

	/* Enable System (Reg 0)
	 * first enable causes a garbage in RX FIFO */
	write_nic_byte(dev, 0x210000, 0x0);
	write_nic_byte(dev, 0x210000, 0x80);

	/* Reset External Baseband Reg */
	write_nic_word(dev, IPW2100_CONTROL_REG, 0x703);
	write_nic_word(dev, IPW2100_CONTROL_REG, 0x707);

	/* HW Config (Reg 5) */
	write_nic_byte(dev, 0x210014, 0x72);	// fifo width =16
	write_nic_byte(dev, 0x210014, 0x72);	// fifo width =16

	/* Enable System (Reg 0)
	 * second enable should be OK 
	 */
	write_nic_byte(dev, 0x210000, 0x0);	// clear enable system
	write_nic_byte(dev, 0x210000, 0x80);	// set enable system

	/* check Symbol is enabled - upped this from 5 as it wasn't always
	 * catching the update */
	for (i = 0; i < 10; i++) {
		udelay(10);

		/* check Dino is enabled bit */
		read_nic_byte(dev, 0x210000, &data);
		if (data & 0x1)
			break;
	}

	if (i == 10) {
		printk(KERN_ERR "%s: Error initializing Symbol\n",
		       dev->name);
		return -EIO;
	}

	/* Get Symbol alive response */
	for (i = 0; i < 30; i++) {
		/* Read alive response structure */
		for (j = 0; 
		     j < (sizeof(struct symbol_alive_response) >> 1); 
		     j++)
			read_nic_word(dev, 0x210004,
				      ((u16 *)&response) + j);

		if ((response.cmd_id == 1) &&
		    (response.ucode_valid == 0x1))
			break;
		udelay(10);
	}

	if (i == 30) {
		printk(KERN_ERR "%s: No response from Symbol - hw not alive\n",
		       dev->name);
		printk_buf((u8*)&response, sizeof(response));
		return -EIO;
	}

	return 0;
}
