/*
    cx25840 firmware functions

    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
 */

#include <linux/i2c.h>
#include <linux/i2c-algo-bit.h>
#include <linux/mm.h>
#include <linux/vmalloc.h>
#include <asm/uaccess.h>

#include "compat.h"

#include "cx25840-firmware.h"
#include "cx25840-driver.h"
#include "cx25840-registers.h"

static inline void cx25840_i2c_set_delay(struct i2c_client *client, int delay)
{
	struct i2c_algo_bit_data *algod = client->adapter->algo_data;
	algod->udelay = delay;
}

static inline void start_fw_load(struct i2c_client *client)
{
	CX25840_SET_DL_ADDR_LB(0x00);
	CX25840_SET_DL_ADDR_HB(0x00);
	CX25840_SET_DL_MAP(0x03);
	CX25840_SET_DL_AUTO_INC(0x00);
	CX25840_SET_DL_ENABLE(0x01);
	CX25840_SET_AUTO_INC_DIS(0x01);

	if (cx25840_fast_fw_load)
		cx25840_i2c_set_delay(client, 3);
}

static inline void end_fw_load(struct i2c_client *client)
{
	if (cx25840_fast_fw_load)
		cx25840_i2c_set_delay(client, 10);

	CX25840_SET_AUTO_INC_DIS(0x00);
	CX25840_SET_DL_ENABLE(0x00);
}

static inline int check_fw_load(struct i2c_client *client, int size, const char *file)
{
	size -= cx25840_read_setting(client, DL_ADDR_HB) << 8;
	size -= cx25840_read_setting(client, DL_ADDR_LB);
	if (size != 0) {
		CX25840_ERR("firmware load failed\n");
		return -EINVAL;
	}

	return 0;
}

static inline int fw_write(struct i2c_client *client, u8 * data, int size)
{
	if (i2c_master_send(client, data, size) < 0) {

		if (cx25840_fast_fw_load) {
			CX25840_ERR("fast fw load failed\n");
			cx25840_fast_fw_load = 0;
			cx25840_i2c_set_delay(client, 10);

			if (i2c_master_send(client, data, size) < 0) {
				CX25840_ERR("fw load totally failed\n");
				return -ENOSYS;
			}

		} else {
			CX25840_ERR("fw load i2c failure\n");
			return -ENOSYS;
		}

	}

	return 0;
}

int cx25840_load_fw_hp(struct i2c_client *client)
{
	const struct firmware *fw = NULL;
	const char *file = cx25840_fw ? cx25840_fw : FWFILE;
	u8 buffer[4], *ptr;
	int size, retval;

	CX25840_INFO("requesting %s from hotplug\n", file);
	if (request_firmware(&fw, file, FWDEV(client)) != 0) {
		CX25840_ERR("firmware read failed\n");
		return -EINVAL;
	}

	start_fw_load(client);

	buffer[0] = 0x08;
	buffer[1] = 0x02;
	buffer[2] = fw->data[0];
	buffer[3] = fw->data[1];
	retval = fw_write(client, buffer, 4);

	if (retval < 0) {
		release_firmware(fw);
		return retval;
	}

	size = fw->size - 2;
	ptr = fw->data;
	while (size > 0) {
		ptr[0] = 0x08;
		ptr[1] = 0x02;
		retval =
		    fw_write(client, ptr,
			     size > (FWSEND - 2) ? FWSEND : size + 2);

		if (retval < 0) {
			release_firmware(fw);
			return retval;
		}

		size -= FWSEND - 2;
		ptr += FWSEND - 2;
	}

	end_fw_load(client);

	size = fw->size;
	release_firmware(fw);

	return check_fw_load(client, size, file);
}

int cx25840_load_fw_nohp(struct i2c_client *client)
{
	const char *file = cx25840_fw ? cx25840_fw : FWFILE;
	loff_t file_offset = 0;
	mm_segment_t fs;
	kernel_filep filep;
	int size, tsize, retval;
	u8 *buffer;

	CX25840_INFO("loading %s\n", file);

	buffer = vmalloc(FWSEND);
	if (buffer == 0) {
		CX25840_ERR("firmware load failed (out of memory)\n");
		return -ENOMEM;
	}

	buffer[0] = 0x08;
	buffer[1] = 0x02;

	fs = get_fs();
	set_fs(get_ds());

	filep = kernel_file_open(file, 0, 0);
	if (kernel_file_is_err(filep)) {
		CX25840_ERR("unable to open firmware\n");
		vfree(buffer);
		set_fs(fs);
		return -EINVAL;
	}

	start_fw_load(client);

	tsize = 0;
	do {
		size =
		    kernel_file_read(filep, &buffer[2], FWSEND - 2,
				     &file_offset);
		if (size < 0) {
			CX25840_ERR("firmware read failed\n");
			vfree(buffer);
			kernel_file_close(filep);
			set_fs(fs);
			return -EINVAL;
		}

		tsize += size;

		if (size) {
			retval = fw_write(client, buffer, size + 2);

			if (retval < 0) {
				vfree(buffer);
				kernel_file_close(filep);
				set_fs(fs);
				return retval;
			}
		}

	} while (size == FWSEND - 2);

	end_fw_load(client);

	vfree(buffer);
	kernel_file_close(filep);
	set_fs(fs);

	return check_fw_load(client, tsize, file);
}
