/*
 * Stephen Evanchik <evanchsa@gmail.com>
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 as published by
 * the Free Software Foundation.
 *
 * Trademarks are the property of their respective owners.
 *
 */

#include <linux/delay.h>
#include <linux/serio.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/input.h>
#include <linux/proc_fs.h>
#include <asm/uaccess.h>
#include "psmouse.h"
#include "trackpoint.h"


int tp_sens = TP_DEF_SENS;
module_param_named(sens, tp_sens, uint, 0);
MODULE_PARM_DESC(sens, "Sensitivity");

int tp_speed = TP_DEF_SPEED;
module_param_named(speed, tp_speed, uint, 0);
MODULE_PARM_DESC(speed, "Speed of pointer");

int tp_backup = TP_DEF_BACKUP;
module_param_named(backup, tp_backup, uint, 0);
MODULE_PARM_DESC(backup, "Backup Range");

int tp_draghyst = TP_DEF_DRAG_HYST;
module_param_named(draghyst, tp_draghyst, uint, 0);
MODULE_PARM_DESC(draghyst, "Resistance to dragging");

int tp_mindrag = TP_DEF_MIN_DRAG;
module_param_named(mindrag, tp_mindrag, uint, 0);
MODULE_PARM_DESC(mindrag, "Drag threshold");

int tp_thresh = TP_DEF_THRESH;
module_param_named(thresh, tp_thresh, uint, 0);
MODULE_PARM_DESC(thresh, "Force necessary to trigger a press or release");

int tp_upthresh = TP_UP_THRESH;
module_param_named(upthresh, tp_upthresh, uint, 0);
MODULE_PARM_DESC(upthresh, "Force necessary to trigger a click");

int tp_ztime = TP_DEF_Z_TIME;
module_param_named(ztime, tp_ztime, uint, 0);
MODULE_PARM_DESC(ztime, "Determines how sharp a press is");

int tp_jenks = TP_DEF_JENKS_CURV;
module_param_named(jenks, tp_jenks, uint, 0);
MODULE_PARM_DESC(jenks, "Double click sensitivity");


/* Toggles */
int tp_pts = TP_DEF_PTS;
module_param_named(pts, tp_pts, uint, 0);
MODULE_PARM_DESC(pts, "Press to Select");

int tp_mb = TP_DEF_MB;
module_param_named(mb, tp_mb, uint, 0);
MODULE_PARM_DESC(pts, "Middle button is disabled");

/*
 * Device IO: read, write and toggle bit
 */
static void trackpoint_read(struct psmouse *psmouse, unsigned char loc, unsigned char *results)
{
	psmouse_command(psmouse, NULL, MAKE_PS2_CMD(0, 0, TP_COMMAND));
	psmouse_command(psmouse, results, MAKE_PS2_CMD(0, 1, loc));
}

static void trackpoint_write(struct psmouse *psmouse, unsigned char loc, unsigned char val)
{	
	psmouse_command(psmouse, NULL, MAKE_PS2_CMD(0, 0, TP_COMMAND));
	psmouse_command(psmouse, NULL, MAKE_PS2_CMD(0,0, TP_WRITE_MEM));
	psmouse_command(psmouse, &val, MAKE_PS2_CMD(1,0, loc));
}

static int trackpoint_toggle_bit(struct psmouse *psmouse, unsigned char loc, unsigned char mask)
{
	/* Bad things will happen if the loc param isn't in this range */
	if(loc < 0x20 || loc >= 0x2F)
		return -1;
	
	psmouse_command(psmouse, NULL, MAKE_PS2_CMD(0, 0, TP_COMMAND));
	psmouse_command(psmouse, NULL, MAKE_PS2_CMD(0, 0, TP_TOGGLE));
	psmouse_command(psmouse, &mask, MAKE_PS2_CMD(1, 0, loc));
	
	return 0;
}


#ifdef CONFIG_PROC_FS

#define PROC_READ_NAME(name) name##_read_func
#define PROC_WRITE_NAME(name) name##_write_func
#define PROC_TOGGLE_NAME(name) name##_toggle_func

#define MAKE_PROC_READ(name, item) \
	static int name(char* page, char** start, off_t off, int count, int* eof, void* data) \
	{ \
		int len; \
		struct psmouse *psmouse = (struct psmouse *)data; \
		struct trackpoint_data *tp = (struct trackpoint_data*)psmouse->private; \
		len = sprintf(page, "%d\n", tp->item); \
		*eof = 1; \
		return len; \
	}

#define MAKE_PROC_WRITE(name, item, command) \
	static int name(struct file *file, const char __user *buffer, unsigned long count, void *data) \
	{ \
		int len = count; \
		unsigned char tmp[5]; \
		struct psmouse *psmouse = (struct psmouse *)data; \
		struct trackpoint_data *tp = (struct trackpoint_data*)psmouse->private; \
		if(count > sizeof(tmp) - 1) \
			len = sizeof(tmp) - 1; \
		if(copy_from_user(tmp, buffer, len)) \
			return -EFAULT; \
		tmp[len] = '\0'; \
		tp->item = simple_strtoul(tmp, 0, 10); \
		trackpoint_write((struct psmouse *)data, command, tp->item); \
		return len; \
	}

#define MAKE_PROC_TOGGLE(name, item, command, mask) \
	static int name(struct file *file, const char __user *buffer, unsigned long count, void *data) \
	{ \
		int len = count; \
		unsigned char toggle, tmp[5]; \
		struct psmouse *psmouse = (struct psmouse *)data; \
		struct trackpoint_data *tp = (struct trackpoint_data*)psmouse->private; \
		if(count > sizeof(tmp) - 1) \
			len = sizeof(tmp) - 1; \
		if(copy_from_user(tmp, buffer, len)) \
			return -EFAULT; \
		tmp[len] = '\0'; \
		toggle = (tmp[0] == '1') ? 1 : 0; \
		if( toggle != tp->item) { \
			tp->item = toggle; \
			trackpoint_toggle_bit(psmouse, command, mask); \
		} \
		return len; \
	}


MAKE_PROC_WRITE(PROC_WRITE_NAME(sensitivity), sens, TP_SENS);
MAKE_PROC_READ(PROC_READ_NAME(sensitivity), sens);

MAKE_PROC_WRITE(PROC_WRITE_NAME(speed), speed, TP_SPEED);
MAKE_PROC_READ(PROC_READ_NAME(speed), speed);

MAKE_PROC_WRITE(PROC_WRITE_NAME(neg_inertia), inertia, TP_NEG_INERT);
MAKE_PROC_READ(PROC_READ_NAME(neg_inertia), inertia);

MAKE_PROC_WRITE(PROC_WRITE_NAME(backup), reach, TP_BACKUP);
MAKE_PROC_READ(PROC_READ_NAME(backup), reach);

MAKE_PROC_WRITE(PROC_WRITE_NAME(drag_hyst), draghys, TP_DRAG_HYST);
MAKE_PROC_READ(PROC_READ_NAME(drag_hyst), draghys);

MAKE_PROC_WRITE(PROC_WRITE_NAME(min_drag), mindrag, TP_MIN_DRAG);
MAKE_PROC_READ(PROC_READ_NAME(min_drag), mindrag);

MAKE_PROC_WRITE(PROC_WRITE_NAME(thresh), thresh, TP_THRESH);
MAKE_PROC_READ(PROC_READ_NAME(thresh), thresh);

MAKE_PROC_WRITE(PROC_WRITE_NAME(up_thresh), up_thresh, TP_UP_THRESH);
MAKE_PROC_READ(PROC_READ_NAME(up_thresh), up_thresh);

MAKE_PROC_WRITE(PROC_WRITE_NAME(z_time), z_time, TP_Z_TIME);
MAKE_PROC_READ(PROC_READ_NAME(z_time), z_time);

MAKE_PROC_WRITE(PROC_WRITE_NAME(jenks_curv), jenks_curv, TP_JENKS_CURV);
MAKE_PROC_READ(PROC_READ_NAME(jenks_curv), jenks_curv);

MAKE_PROC_TOGGLE(PROC_TOGGLE_NAME(ptson), ptson, TP_TOGGLE_PTS, TP_MASK_PTS);
MAKE_PROC_READ(PROC_READ_NAME(ptson), ptson);

MAKE_PROC_TOGGLE(PROC_TOGGLE_NAME(skip_back), skipback, TP_TOGGLE_SKIP_BACK, TP_MASK_SKIP_BACK);
MAKE_PROC_READ(PROC_READ_NAME(skip_back), skipback);

MAKE_PROC_TOGGLE(PROC_TOGGLE_NAME(mb), mb, TP_TOGGLE_MB, TP_MASK_MB);
MAKE_PROC_READ(PROC_READ_NAME(mb), mb);



#define NEW_PROC_ENTRY(name) \
        entry = create_proc_entry(#name, 0644, tp_dir); \
        if(!entry) \
                goto no_##name; \
        entry->owner = THIS_MODULE; \
        entry->data = psmouse; \
        entry->read_proc = PROC_READ_NAME(name); \
        entry->write_proc = PROC_WRITE_NAME(name);


#define NEW_PROC_TOGGLE_ENTRY(name) \
        entry = create_proc_entry(#name, 0644, tp_dir); \
        if(!entry) \
                goto no_##name; \
        entry->owner = THIS_MODULE; \
        entry->data = psmouse; \
        entry->read_proc = PROC_READ_NAME(name); \
        entry->write_proc = PROC_TOGGLE_NAME(name);
	


static struct proc_dir_entry *tp_dir = NULL;


static int trackpoint_proc_init(struct psmouse *psmouse) 
{ 
	struct proc_dir_entry *entry = NULL;

	tp_dir = proc_mkdir("trackpoint", NULL);
	if(!tp_dir) 
		return -ENOMEM;

	tp_dir->owner = THIS_MODULE;

	NEW_PROC_ENTRY(sensitivity);
	NEW_PROC_ENTRY(speed);
	NEW_PROC_ENTRY(neg_inertia);
	NEW_PROC_ENTRY(backup);
	NEW_PROC_ENTRY(drag_hyst);
	NEW_PROC_ENTRY(min_drag);
	NEW_PROC_ENTRY(thresh);
	NEW_PROC_ENTRY(up_thresh);
	NEW_PROC_ENTRY(z_time);
	NEW_PROC_ENTRY(jenks_curv);

	NEW_PROC_TOGGLE_ENTRY(ptson);
	NEW_PROC_TOGGLE_ENTRY(skip_back);
	NEW_PROC_TOGGLE_ENTRY(mb);

	return 0;

no_mb:
	remove_proc_entry("skip_back", tp_dir);

no_skip_back:
	remove_proc_entry("ptson", tp_dir);

no_ptson:
	remove_proc_entry("jenks_curv", tp_dir);

no_jenks_curv:
	remove_proc_entry("z_time", tp_dir);

no_z_time:
	remove_proc_entry("up_thresh", tp_dir);

no_up_thresh:
	remove_proc_entry("thresh", tp_dir);

no_thresh:
	remove_proc_entry("min_drag", tp_dir);

no_min_drag:
	remove_proc_entry("drag_hyst", tp_dir);

no_drag_hyst:
	remove_proc_entry("backup", tp_dir);

no_backup:
	remove_proc_entry("neg_inertia", tp_dir);

no_neg_inertia:
	remove_proc_entry("speed", tp_dir);

no_speed:
	remove_proc_entry("sensitivity", tp_dir);

no_sensitivity:
	remove_proc_entry("trackpoint", NULL);

	return -ENOMEM;
}

static void trackpoint_proc_remove(void)
{
        remove_proc_entry("mb", tp_dir);
        remove_proc_entry("skip_back", tp_dir);
        remove_proc_entry("ptson", tp_dir);
        remove_proc_entry("jenks_curv", tp_dir);
        remove_proc_entry("z_time", tp_dir);
        remove_proc_entry("up_thresh", tp_dir);
        remove_proc_entry("thresh", tp_dir);
        remove_proc_entry("min_drag", tp_dir);
        remove_proc_entry("drag_hyst", tp_dir);
        remove_proc_entry("backup", tp_dir);
        remove_proc_entry("neg_inertia", tp_dir);
        remove_proc_entry("speed", tp_dir);
        remove_proc_entry("sensitivity", tp_dir);
        remove_proc_entry("trackpoint", NULL);
}

#else
static int trackpoint_proc_init(struct psmouse *psmouse) { return 0; }
static void trackpoint_proc_remove(void) { do {} while(0); }
#endif


void trackpoint_disconnect(struct psmouse *psmouse)
{
	trackpoint_proc_remove();

	kfree(psmouse->private);
}

int trackpoint_reconnect(struct psmouse *psmouse)
{
	unsigned char toggle;
	struct trackpoint_data *tp = psmouse->private;

	/* Push the config to the device */
	
	trackpoint_write(psmouse, TP_SENS, tp->sens);
	trackpoint_write(psmouse, TP_NEG_INERT, tp->inertia);
	trackpoint_write(psmouse, TP_SPEED, tp->speed);

	trackpoint_write(psmouse, TP_BACKUP, tp->reach);
	trackpoint_write(psmouse, TP_DRAG_HYST, tp->draghys);
	trackpoint_write(psmouse, TP_MIN_DRAG, tp->mindrag);

	trackpoint_write(psmouse, TP_THRESH, tp->thresh);
	trackpoint_write(psmouse, TP_UP_THRESH, tp->up_thresh);

	trackpoint_write(psmouse, TP_Z_TIME, tp->z_time);
	trackpoint_write(psmouse, TP_JENKS_CURV, tp->jenks_curv);


	trackpoint_read(psmouse, TP_TOGGLE_PTS, &toggle);
	if(((toggle & TP_MASK_PTS) == TP_MASK_PTS)!= tp->ptson)
		 trackpoint_toggle_bit(psmouse, TP_TOGGLE_PTS, TP_MASK_PTS);

	trackpoint_read(psmouse, TP_TOGGLE_MB, &toggle);
	if(((toggle & TP_MASK_MB) == TP_MASK_MB) != tp->mb)
		 trackpoint_toggle_bit(psmouse, TP_TOGGLE_MB, TP_MASK_MB);

	trackpoint_read(psmouse, TP_TOGGLE_SKIP_BACK, &toggle);
	if(((toggle & TP_MASK_SKIP_BACK) == TP_MASK_SKIP_BACK) != tp->skipback)
		trackpoint_toggle_bit(psmouse, TP_TOGGLE_SKIP_BACK, TP_MASK_SKIP_BACK);

	/* TODO: Add external device pass through */

	return 0;
}

static void trackpoint_defaults(struct trackpoint_data *tp)
{
	tp->sens = tp_sens;
	tp->speed = tp_speed;
	tp->reach = tp_backup;

	tp->draghys = tp_draghyst;
	tp->mindrag = tp_mindrag;

	tp->thresh = tp_thresh;
	tp->up_thresh = tp_upthresh;

	tp->z_time = tp_ztime;
	tp->jenks_curv = tp_jenks;

	tp->ptson = tp_pts;
	tp->mb = tp_mb;
	tp->skipback = TP_DEF_SKIP_BACK;
}



int trackpoint_init(struct psmouse *psmouse)
{
	unsigned char param[2];
	struct trackpoint_data *priv;

	param[0] = param[1] = 0;

	/* The real driver disables, queries and 
	   then enables so we'll do that too
	*/
	psmouse_command(psmouse, param, MAKE_PS2_CMD(0, 2, TP_DISABLE));

	psmouse_command(psmouse, param, MAKE_PS2_CMD(0, 2, TP_READ_ID));
	if(param[0] != TP_MAGIC_IDENT) 
		return -1;

	psmouse_command(psmouse, param, MAKE_PS2_CMD(0, 2, TP_ENABLE));

	psmouse->private = priv = kmalloc(sizeof(struct trackpoint_data), GFP_KERNEL);

	if(!priv) 
		return -1;

	memset(priv, 0, sizeof(struct trackpoint_data));

	priv->firmware_id = param[1];


	psmouse->reconnect = trackpoint_reconnect;
	psmouse->disconnect = trackpoint_disconnect;

	/*
	 * Initialize the device's default settings
	 */
	trackpoint_defaults(priv);
	trackpoint_reconnect(psmouse);

	trackpoint_proc_init(psmouse);

	printk("IBM TrackPoint firmware: 0x%02X\n", param[1]);

	return 0;
}


