#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/sched.h>
#include <linux/string.h>
#include <linux/timer.h>
#include <linux/delay.h>
#include <linux/errno.h>
#include <linux/malloc.h>

#include "i2c.h"
#include "videodev.h"
#include "tuner.h"

static int debug =  0; /* insmod parameter */
static int type  = -1; /* tuner type */

#define dprintk     if (debug) printk

#if LINUX_VERSION_CODE > 0x020100
MODULE_PARM(debug,"i");
MODULE_PARM(type,"i");
#endif

#if LINUX_VERSION_CODE < 0x02017f
void schedule_timeout(int j)
{
	current->state   = TASK_INTERRUPTIBLE;
	current->timeout = jiffies + j;
	schedule();
}
#endif

struct tuner 
{
	int 		     addr;

	int type;            /* chip type */
	int freq;            /* keep track of the current settings */
	int radio;
	int mode;            /* PAL(0)/SECAM(1) mode (PHILIPS_SECAM only) */
};

static struct i2c_driver driver;
static struct i2c_client client_template;

/* ---------------------------------------------------------------------- */

struct tunertype 
{
	char *name;
	unsigned char Vendor;
	unsigned char Type;
  
	unsigned short thresh1; /* frequency Range for UHF,VHF-L, VHF_H */   
	unsigned short thresh2;  
	unsigned char VHF_L;
	unsigned char VHF_H;
	unsigned char UHF;
	unsigned char config; 
	unsigned char I2C;
	unsigned short IFPCoff;
	unsigned char mode; /* mode change value (tested PHILIPS_SECAM only) */
			/* 0x01 -> ??? no change ??? */
			/* 0x02 -> PAL BDGHI / SECAM L */
			/* 0x04 -> ??? PAL others / SECAM others ??? */
};

/*
 *	The floats in the tuner struct are computed at compile time
 *	by gcc and cast back to integers. Thus we don't violate the
 *	"no float in kernel" rule.
 */
static struct tunertype tuners[] = {
        {"Temic PAL", TEMIC, PAL,
                16*140.25,16*463.25,0x02,0x04,0x01,0x8e,0xc2,623},
	{"Philips PAL_I", Philips, PAL_I,
	        16*140.25,16*463.25,0xa0,0x90,0x30,0x8e,0xc0,623},
	{"Philips NTSC", Philips, NTSC,
	        16*157.25,16*451.25,0xA0,0x90,0x30,0x8e,0xc0,732},
	{"Philips SECAM", Philips, SECAM,
	        16*168.25,16*447.25,0xA7,0x97,0x37,0x8e,0xc0,623,0x02},
	{"NoTuner", NoTuner, NOTUNER,
	         0        ,0        ,0x00,0x00,0x00,0x00,0x00,000},
	{"Philips PAL", Philips, PAL,
	        16*168.25,16*447.25,0xA0,0x90,0x30,0x8e,0xc0,623},
	{"Temic NTSC", TEMIC, NTSC,
	        16*157.25,16*463.25,0x02,0x04,0x01,0x8e,0xc2,732},
	{"TEMIC PAL_I", TEMIC, PAL_I,
	      //  16*170.00,16*450.00,0xa0,0x90,0x30,0x8e,0xc2,623},
	        16*170.00,16*450.00,0x02,0x04,0x01,0x8e,0xc2,623},
};

/* ---------------------------------------------------------------------- */

static int tuner_getstatus (struct i2c_client *client)
{
	return 0;
}

#define TUNER_POR       0x80
#define TUNER_FL        0x40
#define TUNER_MODE      0x38
#define TUNER_AFC       0x07

static int tuner_islocked (struct i2c_client *c)
{
        return (tuner_getstatus (c) & TUNER_FL);
}

static int tuner_afcstatus (struct i2c_client *c)
{
        return (tuner_getstatus (c) & TUNER_AFC) - 2;
}

static int tuner_mode (struct i2c_client *c)
{
        return (tuner_getstatus (c) & TUNER_MODE) >> 3;
}

static void set_tv_freq(struct i2c_client *c, int freq)
{
	u8 config;
	u16 div;
	struct tunertype *tun;
	struct tuner *t = (struct tuner*)c->data;
        unsigned char buffer[4];
	int rc, swap=0;

	if (t->type == -1) {
		printk("tuner: tuner type not set\n");
		return;
	}

	tun=&tuners[t->type];
	if (freq < tun->thresh1) 
		config = tun->VHF_L;
	else if (freq < tun->thresh2) 
		config = tun->VHF_H;
	else
		config = tun->UHF;

	if (t->type == TUNER_PHILIPS_SECAM && t->mode)
		config |= tun->mode;
	else
		config &= ~tun->mode;

	div=freq + tun->IFPCoff;

    /*
     * Philips FI1216MK2 remark from specification :
     * for channel selection involving band switching, and to ensure
     * smooth tuning to the desired channel without causing
     * unnecessary charge pump action, it is recommended to consider
     * the difference between wanted channel frequency and the
     * current channel frequency.  Unnecessary charge pump action
     * will result in very low tuning voltage which may drive the
     * oscillator to extreme conditions.
     */
    /*
     * Progfou: specification says to send config data before
     * frequency in case (wanted frequency < current frequency).
     */

	if (t->type == TUNER_PHILIPS_SECAM && freq < t->freq)
		swap = 2;
	
	/* some trickery which data is sent first */
        buffer[0] = buffer[2] = tun->config;
        buffer[1] = buffer[3] = config;

        buffer[swap]     =  (div>>8) & 0x7f;
        buffer[swap + 1] =  div      & 0xff;

        if (4 != (rc = i2c_master_send(c,buffer,4)))
                printk("tuner: i2c i/o error: rc == %d (should be 4)\n",rc);

}

static void set_radio_freq(struct i2c_client *c, int freq)
{
	u8 config;
	u16 div;
	struct tunertype *tun;
	struct tuner *t = (struct tuner*)c->data;
        unsigned char buffer[4];
	int rc;

	if (t->type == -1) {
		printk("tuner: tuner type not set\n");
		return;
	}

	tun=&tuners[t->type];
	config = 0xa5;
	div=freq + (int)(16*10.7);
  	div&=0x7fff;

        buffer[0] = (div>>8) & 0x7f;
        buffer[1] = div      & 0xff;
        buffer[2] = tun->config;
        buffer[3] = config;
        if (4 != (rc = i2c_master_send(c,buffer,4)))
                printk("tuner: i2c i/o error: rc == %d (should be 4)\n",rc);

	if (debug) {
		current->state   = TASK_INTERRUPTIBLE;
		schedule_timeout(HZ/10);
		
		if (tuner_islocked (c))
			printk ("tuner: PLL locked\n");
		else
			printk ("tuner: PLL not locked\n");
		
		printk ("tuner: AFC: %d\n", tuner_afcstatus (c));
	}
}
/* ---------------------------------------------------------------------- */

static int tuner_attach(struct i2c_adapter *adap)
{
	struct tuner *t;
	struct i2c_client *client;
	int addr;


	/* Only attach to BT848 host adapter */
	if ((adap->id &~ ALGO_MASK) != HW_B_BT848)
		return 0;

        client_template.adapter = adap;

	/* client->addr is probed here */
        if (-1 == (addr = i2c_probe(&client_template, 0xc0>>1, 0xde>>1)))
                return -ENODEV;
         
        printk("tuner: chip found @ 0x%x\n",addr);

        if (NULL == (client = kmalloc(sizeof(struct i2c_client), GFP_KERNEL)))
                return -ENOMEM;
        memcpy(client,&client_template,sizeof(struct i2c_client));
        client->data = t = kmalloc(sizeof(struct tuner),GFP_KERNEL);
        if (NULL == t) {
                kfree(client);
                return -ENOMEM;
        }
        memset(t,0,sizeof(struct tuner));
        t->type = type;
        i2c_attach_client(client);

	return 0;
}

static int tuner_detach(struct i2c_client *client)
{
	struct tuner *t = (struct tuner*)client->data;

	i2c_detach_client(client);
	kfree(t);
	kfree(client);
	return 0;
}

static int
tuner_command(struct i2c_client *client, unsigned int cmd, void *arg)
{
	struct tuner *t = (struct tuner*)client->data;
	int *iarg = (int*)arg;

	switch (cmd) 
	{
		case TUNER_SET_TYPE:
			t->type = *iarg;
			dprintk("tuner: type set to %d (%s)\n",
			t->type,tuners[t->type].name);
			break;

		case TUNER_SET_TVFREQ:
			dprintk("tuner: tv freq set to %d.%02d\n",
				(*iarg)/16,(*iarg)%16*100/16);
			set_tv_freq(client,*iarg);
			t->radio = 0;
			t->freq = *iarg;
			break;
	    
		case TUNER_SET_RADIOFREQ:
			dprintk("tuner: radio freq set to %d.%02d\n",
				(*iarg)/16,(*iarg)%16*100/16);
			set_radio_freq(client,*iarg);
			t->radio = 1;
			t->freq = *iarg;
			break;
		case TUNER_SET_MODE:
			if (t->type != TUNER_PHILIPS_SECAM) {
				dprintk("tuner: trying to change mode for other than TUNER_PHILIPS_SECAM\n");
			} else {
				dprintk("tuner: mode set to %d\n", *iarg);
				t->mode = *iarg;
				set_tv_freq(client,t->freq);
			}
			break;

	    
		default:
			return -EINVAL;
	}
	return 0;
}

/* ----------------------------------------------------------------------- */

static struct i2c_driver driver = {
        "i2c tv tuner driver",
        -1,
        DF_NOTIFY,
        tuner_attach,
        tuner_detach,
        tuner_command,
};

static struct i2c_client client_template =
{
        "i2c tv tuner chip",          /* name       */
        I2C_DRIVERID_TUNER,           /* ID         */
        0,
        0,
        NULL,
        &driver
};

EXPORT_NO_SYMBOLS;

#ifdef MODULE
int init_module(void)
#else
int i2c_tuner_init(void)
#endif
{
	i2c_add_driver(&driver);
	return 0;
}

#ifdef MODULE
void cleanup_module(void)
{
	i2c_del_driver(&driver);
}
#endif

/*
 * Overrides for Emacs so that we follow Linus's tabbing style.
 * ---------------------------------------------------------------------------
 * Local variables:
 * c-basic-offset: 8
 * End:
 */
