/*
  Production Data Area API for Conexant CX3110x chipset.

  Copyright (C) 2004, 2005, 2006 Jean-Baptiste Note <jean-baptiste.note@m4x.org>

  This file written with information gathered from code written for the
  Nokia 770 and released at maemo.org, which is

  Copyright (C) 2005 Nokia Corporation
  Author: Samuel Ortiz <samuel.ortiz@nokia.com>

  This file is part of islsm.

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

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

*/

#include <linux/types.h>
#include <linux/unistd.h>
#include <linux/string.h>
#include <asm/errno.h>

#include <linux/crc-ccitt.h>

#include "isl_sm.h"
#include "islsm_pda.h"
#include "islsm_log.h"

static int pa_cal_output_pwr_option(struct islsm *islsm,
				    struct wlan_pda_output_limit_rev0_s *dat,
				    unsigned str_length);
static int pa_cal_curve_data_option(struct islsm *islsm,
				    struct wlan_pda_pa_curve_data_old_s *dat,
				    unsigned str_length);
static int tx_iq_cal_option(struct islsm *islsm,
			    struct wlan_pda_iq_autocal_s *dat,
			    unsigned str_length);

static void pda_print_country(struct wlan_pda_country_s *country) {
	islog(L_PDA, "Country %.2s, regdomain %02x\n",
	      country->code, country->regulatory_domain);
}

static inline size_t pda_size(pda_entry_t *pda_rec) {
	unsigned length = le16_to_cpu(pda_rec->size);
	return (length + 1) * sizeof(uint16_t);
}

static inline pda_entry_t* next_pda(pda_entry_t *pda_rec) {
	unsigned length = le16_to_cpu(pda_rec->size);
	return (pda_entry_t*) (((uint16_t *) pda_rec) + length + 1);
}

static void pda_dump(pda_entry_t *pda_rec) {
	uint16_t code = le16_to_cpu(pda_rec->code);
	uint16_t size = le16_to_cpu(pda_rec->size);
	islog(L_PDA, "Dump of option magic %04x\n", code);
	islog_bytes(L_PDA, &pda_rec->code, size * sizeof (uint16_t));
}

static pda_entry_t *
find_pda_start(eeprom_pda_wrap_t *mgmt_h) {
	unsigned int pda_offset = le16_to_cpu(mgmt_h->data_length);
	pda_entry_t *pda_e;
	islog(L_PDA, "eeprom pda wrapper of %zd bytes\n", pda_offset + sizeof(*mgmt_h));
	pda_e = (pda_entry_t *) &mgmt_h->data[pda_offset];
	return pda_e;
}

static int pda_entry_end(pda_entry_t *pda_e) {
	unsigned int code = le16_to_cpu(pda_e->code);
	return (code == PDR_END);
}

static int pda_process_entry(struct islsm *islsm, pda_entry_t *pda_e) {
#ifdef MADWIFI
	struct ieee80211com *ic = &islsm->sc_ic;
#endif
	struct net_device *netdev = NETDEV_OF_ISLSM(islsm);
	unsigned int size = le16_to_cpu(pda_e->size);
	unsigned int code = le16_to_cpu(pda_e->code);
	void *payload = pda_e->data;
	unsigned str_length = (size - 1) * sizeof(uint16_t);

	switch (code) {
	case PDR_MANUFACTURING_PART_NUMBER:
		islog(L_PDA, "Manufacturing part number: %.*s\n",
		      str_length, (char *)payload);
		break;
	case PDR_PDA_VERSION:
		islog(L_PDA, "PDA VERSION, never seen, please report\n");
		pda_dump(pda_e);
		break;
	case PDR_NIC_SERIAL_NUMBER:
		islog(L_PDA, "NIC S/N: %.*s\n",
		      str_length, (char *)payload);
		break;
	case PDR_MAC_ADDRESS:
		islog(L_PDA, "Device reported its MAC address\n");
#ifdef MADWIFI
		IEEE80211_ADDR_COPY(ic->ic_myaddr, payload);
		IEEE80211_ADDR_COPY(netdev->dev_addr, payload);
#else
		memcpy(netdev->dev_addr, payload, ETH_ALEN);
#endif				/* MADWIFI */
		break;
	case PDR_REGULATORY_DOMAIN_LIST:
		islog(L_PDA, "Regulatory domain list, please report\n");
		pda_dump(pda_e);
		break;
	case PDR_TEMPERATURE_TYPE:
		islog(L_PDA, "Temperature type, please report\n");
		pda_dump(pda_e);
		break;
	case PDR_PRISM_PCI_IDENTIFIER:
		islog(L_PDA, "Prism PCI identifier, please report\n");
		pda_dump(pda_e);
		break;
	case PDR_INTERFACE_LIST:
	{
		struct s_bootrec_exp_if *exp_if = payload;
		int i;
		int no = str_length / sizeof(struct s_bootrec_exp_if);

		/* loop through, and display, the interfaces */
		for(i = 0; i < no; i++) {
			uint16_t if_id = le16_to_cpu(exp_if->if_id);
			uint16_t if_variant = le16_to_cpu(exp_if->variant);
#define IF_ID_F 0xf
			if (if_id == IF_ID_F) {
				/* I'm really at a loss to understand
				   what's in there -- but it seems to
				   work */
				islsm->smpar.rxhw = (if_variant & 0x7);
				islog(L_PDA,
				      "Hardware rx informations:\n"
				      "RX filter field %08x\n",
				      islsm->smpar.rxhw);
			}
			islsm_print_exp_if(exp_if++,L_PDA);
		}
		break;
	}
	case PDR_HARDWARE_PLATFORM_COMPONENT_ID:
	{
		union wlan_pda_hw_platform *option = payload;

		if (isl_debug & L_PDA) {
			unsigned int hwtype = le32_to_cpu(option->hw);
			islog(L_PDA,
			      "Device reports hardware type: %08x\n",
			      hwtype);
			islog(L_PDA, "Device is an isl38%02x\n",
			      option->split.isl);
		}

		break;
	}
	case PDR_COUNTRY_LIST:
	{
		int i;
		struct wlan_pda_country_s *country = payload;
		/* once we have this facilty in the stack, print actual
		   country names */
		islog(L_PDA,"List of domains for the device:\n");
		for(i = 0; i < str_length / sizeof(uint32_t); i++)
			pda_print_country(&country[i]);
		break;
	}
	case PDR_DEFAULT_COUNTRY:
	{
		islog(L_PDA,"Default domain for the device:\n");
		pda_print_country(payload);
		break;
	}
	case PDR_ANTENNA_GAIN:
	{
		islog(L_PDA,"Antenna gain option for the device:\n");
		pda_dump(pda_e);
		break;
	}
	case PDR_PRISM_PA_CAL_OUTPUT_POWER_LIMITS:
		return pa_cal_output_pwr_option(islsm, payload, str_length);
	case PDR_PRISM_PA_CAL_CURVE_DATA:
		return pa_cal_curve_data_option(islsm, payload, str_length);
	case PDR_RSSI_LINEAR_APPROXIMATION_DUAL_BAND:
	{
		islog(L_PDA,"RSSI linear approximation dual band:\n");
		/* 8 bytes, I don't understand them -- certainly 4 * 16
		 * bits, two scales, linear interpolation from the
		 * custom value on 8 bits to Db. The first value is for
		 * 0, the last for 255 (maybe rather the reverse).
		 */
		pda_dump(pda_e);
		break;
	}
	case PDR_PRISM_ZIF_TX_IQ_CALIBRATION:
		return tx_iq_cal_option(islsm, payload, str_length);
	case PDR_RSSI_LINEAR_APPROXIMATION_EXTENDED:
		/* do something */
		pda_dump(pda_e);
		break;
	case PDR_END:
	{
		u16 crc = le16_to_cpu(*(u16 *)payload);
		islog(L_PDA, "End of options, CRC is %04x\n", crc);
		break;
	}
	default:
		islog(L_PDA, "unknown option, please report your PDA\n");
		pda_dump(pda_e);
	}

	return 0;
}

/*
 * pda parsing function
 */

/* This slow function should be called when the pda, containing
 * (among other things) calibration data for the RF stage, has been
 * properly readback. On the N770, this is from flash in the main
 * memory, for USB / PCI cards, this is in an eeprom chip located next
 * to the ARM chip, and conneted to it on an i2c bus. See freemac for
 * more information.
 */

int islsm_parse_pda(struct islsm *const islsm) {
	pda_entry_t * const pda_e = (void *) islsm->pda;
	const size_t length = islsm->pda_length;

	pda_entry_t *current_pda = pda_e;
	size_t pda_length = pda_size(current_pda);
	uint16_t crc;
	int err;

	/* allow room for the next size lookup */
	while (pda_length + sizeof(pda_entry_t) < length) {
		err = pda_process_entry(islsm,current_pda);
		if (err) {
			printk(KERN_ERR "islsm: error parsing the pda; your device may be too recent. "
			       "Please contact the developers.\n");
			return err;
		}
		if (pda_entry_end(current_pda))
			break;
		current_pda = next_pda(current_pda);
		pda_length += pda_size(current_pda);
	}

	/* Calculate CRC for the PDA -- on all but the crc field itself */
	crc = ~crc_ccitt(0xffff, (char *)pda_e, pda_length - sizeof(uint16_t));

	/* check it */
	islog(L_PDA, "Computed CRC is %02x\n", crc);
	if (crc != le16_to_cpu(*(uint16_t *)current_pda->data)) {
		printk(KERN_ERR "PDA CRC Check failed\n");
		return -EINVAL;
	}

	islsm->pda_length = pda_length;

	return 0;
}

int
islsm_parse_eeprom(struct islsm *islsm)
{
	eeprom_pda_wrap_t *eeprom = (void *) islsm->eeprom;
	size_t length = islsm->eeprom_size;
	pda_entry_t *pda_e;
	size_t offset;
	/* FIXME : should pass length, so that the function does not go
	   too far */
	pda_e = find_pda_start(eeprom);
	offset = (uint8_t *) pda_e - (uint8_t *) eeprom;
	islsm->pda_offset = offset;
	islsm->pda = (void *)pda_e;
	islsm->pda_length = length - offset;
	return 0;
}

/* option-specific parsing functions */

static int pa_cal_output_pwr_option(struct islsm *islsm,
				    struct wlan_pda_output_limit_rev0_s *dat,
				    unsigned str_length) {
	uint8_t pdr_revision = dat->pdr_revision;
	unsigned int nfreqs = dat->n_channels;
	int i;

	/* TODO: we have the information to use with the  */
	if (pdr_revision != ORIGINAL_PDR1903_REV) {
		printk(KERN_ERR "Unknown PDR1903 revision %i, please report your PDA\n",pdr_revision);
		return -EINVAL;
	}

	islog(L_PDA, "PA calibration power limit option\n");
	if (nfreqs > str_length / sizeof(struct wlan_pda_output_limits_channel_rev0_s)) {
		printk(KERN_ERR "too many channels %i for %i bytes\n", nfreqs, str_length);
		return -EINVAL;
	}

	for (i = 0; i < nfreqs ; i++) {
		unsigned frequency = le16_to_cpu(dat->power[i].frequency);
		unsigned channel = islsm_ref_to_chan(frequency);

		islog(L_PDA, "power limit for %iMHz, channel %i\n", frequency, channel);

		if (channel >= ARRAY_SIZE(islsm->output_pwr_limits)) {
			printk(KERN_ERR "channel %i out of range!\n", channel);
			return -EINVAL;
		}

		islsm->output_pwr_limits[channel] = dat->power[i];
	}
	return 0;
}

static int tx_iq_cal_option(struct islsm *islsm,
			    struct wlan_pda_iq_autocal_s *dat, unsigned str_length) {
	unsigned int nfreqs = str_length / sizeof (struct wlan_pda_iq_autocal_s);
	unsigned int j;
	islog(L_PDA, "IQ calibration data has %i elements, for an option length of %i\n",
	      nfreqs, str_length);
	for (j = 0; j < nfreqs; j++) {
		unsigned frequency = le16_to_cpu(dat[j].frequency);
		unsigned channel = islsm_ref_to_chan(frequency);

		islog(L_PDA, "calibration data for %iMHz, channel %i\n", frequency, channel);

		if (channel >= ARRAY_SIZE(islsm->finfo6)) {
			printk(KERN_ERR "channel %i out of range!\n", channel);
			return -EIO;
		}
		islsm->finfo6[channel] = dat[j];
	}
	return 0;
}

static void pa_cal_rev0_to_rev1(struct wlan_pda_pa_curve_data_sample_rev1_s *to,
				struct wlan_pda_pa_curve_data_sample_rev0_s *from)
{
	to->rf_power = from->rf_power;
	to->pa_detector = from->pa_detector;
	to->data_64qam = from->pcv;
	/* "invent" the points for the other modulations */
#define SUB(x,y) (uint8_t)((x) - (y)) > (x) ? 0 : (x) - (y)
	to->data_16qam = SUB(from->pcv, 12);
	to->data_qpsk  = SUB(to->data_16qam, 12);
	to->data_bpsk  = SUB(to->data_qpsk, 12);
	to->data_barker= SUB(to->data_bpsk, 14);
#undef SUB
}

static int pa_cal_curve_data_option(struct islsm *islsm,
				    struct wlan_pda_pa_curve_data_old_s *dat, unsigned str_length) {
	struct wlan_pda_pa_curve_data_channel_old_s *entry = dat->curve_data;
	unsigned int nfreqs = dat->n_channels;
	unsigned int ppcurve = dat->points_per_channel;
	unsigned cal_method = dat->cal_method_revision;
	unsigned int i,j;

	islog(L_PDA, "PA calibration curve data\n");
	islog(L_PDA, "%i channels, %i points per curve per channel\n",
	      nfreqs, ppcurve);

	if (cal_method != ORIGINAL_PA_CAL_METHOD &&
	    cal_method != GW_3887_PA_CAL_METHOD) {
		printk(KERN_ERR "Unknown calibration method %i, please report your PDA\n",cal_method);
		return -EINVAL;
	}

	/* FIXME : move to a dynamic point per curve, rather than our
	 * fixed thing */
	BUG_ON(ppcurve > ISLSM_POINTS_PER_CURVE);
	islsm->pa_points_per_curve = ppcurve;

	/* loop over all channels */
	for (j = 0; j < nfreqs; j++) {
		/* FIXME: depends on */
		unsigned frequency   = le16_to_cpu(entry->frequency);
		unsigned channel = islsm_ref_to_chan(frequency);

		/* TODO: simply filter out unknown channels -- some
		 * devices report 49 channels ! -- a,b,g are 30 channels
		 */

		if (channel >= ARRAY_SIZE(islsm->finfo4)) {
			printk(KERN_ERR "channel %i out of range\n", channel);
			return -EINVAL;
		}

		islog(L_PDA, "curve data for freq %iMHz, channel %i\n", frequency, channel);

		if (cal_method == ORIGINAL_PA_CAL_METHOD) {
			struct wlan_pda_pa_curve_data_sample_rev0_s *samples = &entry->curve_samples[0].rev0;
			/* loop over all samples */
			for (i = 0; i < ppcurve; i++) {
				/* Invent some points for calibration curves */
				pa_cal_rev0_to_rev1(&islsm->finfo4[channel][i].sample, &samples[i]);
				islsm->finfo4[channel][i].padding = 0;
			}
			entry = (struct wlan_pda_pa_curve_data_channel_old_s *) &samples[ppcurve];
		} else {
			char *data;
			struct wlan_pda_pa_curve_data_sample_rev1_s *samples = &entry->curve_samples[0].rev1;
			for (i = 0; i < ppcurve; i++) {
				islsm->finfo4[channel][i].sample = samples[i];
				islsm->finfo4[channel][i].padding = 0;
			}
			/* get to the next frequency entry */
			/* this will work with my device, but is this
			 * solid ? -- skip one padding byte */
			data = (char *) &samples[ppcurve];
			data++;
			entry = (struct wlan_pda_pa_curve_data_channel_old_s *) data;
		}
	}
	return 0;
}

