/*
 * ALPS touchpad PS/2 mouse driver
 *
 * Copyright (c) 2003 Neil Brown <neilb@cse.unsw.edu.au>
 * Copyright (c) 2003 Peter Osterlund <petero2@telia.com>
 * Copyright (c) 2004 Dmitry Torokhov <dtor@mail.ru>
 *
 * ALPS detection, tap switching and status querying info is taken from
 * tpconfig utility (by C. Scott Ananian and Bruce Kall).
 *
 * 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.
 */

#include <linux/input.h>
#include <linux/serio.h>
#include <linux/libps2.h>

#include "psmouse.h"
#include "alps.h"

#undef DEBUG
#ifdef DEBUG
#define dbg(format, arg...) printk(KERN_INFO "alps.c: " format "\n", ## arg)
#else
#define dbg(format, arg...) do {} while (0)
#endif

#define ALPS_MODEL_GLIDEPOINT	1
#define ALPS_MODEL_DUALPOINT	2

struct alps_model_info {
	unsigned char signature[3];
	unsigned char model;
} alps_model_data[] = {
	{ { 0x33, 0x02, 0x0a },	ALPS_MODEL_GLIDEPOINT },
	{ { 0x53, 0x02, 0x0a },	ALPS_MODEL_GLIDEPOINT },
	{ { 0x53, 0x02, 0x14 },	ALPS_MODEL_GLIDEPOINT },
	{ { 0x63, 0x02, 0x0a },	ALPS_MODEL_GLIDEPOINT },
	{ { 0x63, 0x02, 0x14 },	ALPS_MODEL_GLIDEPOINT },
	{ { 0x73, 0x02, 0x0a },	ALPS_MODEL_GLIDEPOINT },
	{ { 0x73, 0x02, 0x14 },	ALPS_MODEL_GLIDEPOINT },
	{ { 0x63, 0x02, 0x28 },	ALPS_MODEL_GLIDEPOINT },
	{ { 0x63, 0x02, 0x3c },	ALPS_MODEL_GLIDEPOINT },
	{ { 0x63, 0x02, 0x50 },	ALPS_MODEL_GLIDEPOINT },
	{ { 0x63, 0x02, 0x64 },	ALPS_MODEL_GLIDEPOINT },
	{ { 0x20, 0x02, 0x0e },	ALPS_MODEL_DUALPOINT },
	{ { 0x22, 0x02, 0x0a },	ALPS_MODEL_DUALPOINT },
	{ { 0x22, 0x02, 0x14 }, ALPS_MODEL_DUALPOINT },
	{ { 0x63, 0x03, 0xc8 },	ALPS_MODEL_DUALPOINT },
};

/*
 * ALPS abolute Mode
 * byte 0:  1    1    1    1    1  mid0 rig0 lef0
 * byte 1:  0   x6   x5   x4   x3   x2   x1   x0
 * byte 2:  0   x10  x9   x8   x7  up1  fin  ges
 * byte 3:  0   y9   y8   y7    1  mid1 rig1 lef1
 * byte 4:  0   y6   y5   y4   y3   y2   y1   y0
 * byte 5:  0   z6   z5   z4   z3   z2   z1   z0
 *
 * On a dualpoint, {mid,rig,lef}0 are the stick, 1 are the pad.
 * We just 'or' them together for now.
 *
 * We used to send 'ges'tures as BTN_TOUCH but this made it impossible
 * to disable tap events in the synaptics driver since the driver
 * was unable to distinguish a gesture tap from an actual button click.
 * A tap gesture now creates an emulated touch that the synaptics
 * driver can interpret as a tap event, if MaxTapTime=0 and
 * MaxTapMove=0 then the driver will ignore taps.
 *
 * The touchpad on an 'Acer Aspire' has 4 buttons:
 *   left,right,up,down.
 * This device always sets {mid,rig,lef}0 to 1 and
 * reflects left,right,down,up in lef1,rig1,mid1,up1.
 */

static void alps_process_packet(struct psmouse *psmouse, struct pt_regs *regs)
{
	unsigned char *packet = psmouse->packet;
	struct input_dev *dev = &psmouse->dev;
	int x, y, z;
	int left = 0, right = 0, middle = 0;

	input_regs(dev, regs);

	if ((packet[0] & 0xc8) == 0x08) {   /* 3-byte PS/2 packet */
		x = packet[1];
		if (packet[0] & 0x10)
			x = x - 256;
		y = packet[2];
		if (packet[0] & 0x20)
			y = y - 256;
		left  = (packet[0]     ) & 1;
		right = (packet[0] >> 1) & 1;

		input_report_rel(dev, REL_X, x);
		input_report_rel(dev, REL_Y, -y);
		input_report_key(dev, BTN_A, left);
		input_report_key(dev, BTN_B, right);
		input_sync(dev);
		return;
	}

	x = (packet[1] & 0x7f) | ((packet[2] & 0x78)<<(7-3));
	y = (packet[4] & 0x7f) | ((packet[3] & 0x70)<<(7-4));
	z = packet[5];

	if (z == 127) {	/* DualPoint stick is relative, not absolute */
		if (x > 383)
			x = x - 768;
		if (y > 255)
			y = y - 512;
		left  = packet[3] & 1;
		right = (packet[3] >> 1) & 1;

		input_report_rel(dev, REL_X, x);
		input_report_rel(dev, REL_Y, -y);
		input_report_key(dev, BTN_LEFT, left);
		input_report_key(dev, BTN_RIGHT, right);
		input_sync(dev);
		return;
	}

	if (z > 30) input_report_key(dev, BTN_TOUCH, 1);
	if (z < 25) input_report_key(dev, BTN_TOUCH, 0);

	if (z > 0) {
		input_report_abs(dev, ABS_X, x);
		input_report_abs(dev, ABS_Y, y);
	}
	input_report_abs(dev, ABS_PRESSURE, z);
	input_report_key(dev, BTN_TOOL_FINGER, z > 0);

	left  |= (packet[2]     ) & 1;
	left  |= (packet[3]     ) & 1;
	right |= (packet[3] >> 1) & 1;
	if (packet[0] == 0xff) {
		int back    = (packet[3] >> 2) & 1;
		int forward = (packet[2] >> 2) & 1;
		if (back && forward) {
			middle = 1;
			back = 0;
			forward = 0;
		}
		input_report_key(dev, BTN_BACK,    back);
		input_report_key(dev, BTN_FORWARD, forward);
	} else {
		left   |= (packet[0]     ) & 1;
		right  |= (packet[0] >> 1) & 1;
		middle |= (packet[0] >> 2) & 1;
		middle |= (packet[3] >> 2) & 1;
	}

	input_report_key(dev, BTN_LEFT, left);
	input_report_key(dev, BTN_RIGHT, right);
	input_report_key(dev, BTN_MIDDLE, middle);

	input_sync(dev);
}

static psmouse_ret_t alps_process_byte(struct psmouse *psmouse, struct pt_regs *regs)
{
	if ((psmouse->packet[0] & 0xc8) == 0x08) { /* PS/2 packet */
		if (psmouse->pktcnt == 3) {
			alps_process_packet(psmouse, regs);
			return PSMOUSE_FULL_PACKET;
		}
		return PSMOUSE_GOOD_DATA;
	}

	/* ALPS absolute mode packets start with 0b11111mrl */
	if ((psmouse->packet[0] & 0xf8) != 0xf8)
		return PSMOUSE_BAD_DATA;

	/* Bytes 2 - 6 should have 0 in the highest bit */
	if (psmouse->pktcnt >= 2 && psmouse->pktcnt <= 6 &&
	    (psmouse->packet[psmouse->pktcnt-1] & 0x80))
		return PSMOUSE_BAD_DATA;

	if (psmouse->pktcnt == 6) {
		alps_process_packet(psmouse, regs);
		return PSMOUSE_FULL_PACKET;
	}

	return PSMOUSE_GOOD_DATA;
}

int alps_get_model(struct psmouse *psmouse)
{
	struct ps2dev *ps2dev = &psmouse->ps2dev;
	unsigned char param[4];
	int i;

	/*
	 * First try "E6 report".
	 * ALPS should return 0x00,0x00,0x0a or 0x00,0x00,0x64
	 */
	param[0] = 0;
	if (ps2_command(ps2dev, param, PSMOUSE_CMD_SETRES) ||
	    ps2_command(ps2dev,  NULL, PSMOUSE_CMD_SETSCALE11) ||
	    ps2_command(ps2dev,  NULL, PSMOUSE_CMD_SETSCALE11) ||
	    ps2_command(ps2dev,  NULL, PSMOUSE_CMD_SETSCALE11))
		return -1;

	param[0] = param[1] = param[2] = 0xff;
	if (ps2_command(ps2dev, param, PSMOUSE_CMD_GETINFO))
		return -1;

	dbg("E6 report: %2.2x %2.2x %2.2x", param[0], param[1], param[2]);

	if (param[0] != 0x00 || param[1] != 0x00 || (param[2] != 0x0a && param[2] != 0x64))
		return -1;

	/* Now try "E7 report". ALPS should return 0x33 in byte 1 */
	param[0] = 0;
	if (ps2_command(ps2dev, param, PSMOUSE_CMD_SETRES) ||
	    ps2_command(ps2dev,  NULL, PSMOUSE_CMD_SETSCALE21) ||
	    ps2_command(ps2dev,  NULL, PSMOUSE_CMD_SETSCALE21) ||
	    ps2_command(ps2dev,  NULL, PSMOUSE_CMD_SETSCALE21))
		return -1;

	param[0] = param[1] = param[2] = 0xff;
	if (ps2_command(ps2dev, param, PSMOUSE_CMD_GETINFO))
		return -1;

	dbg("E7 report: %2.2x %2.2x %2.2x", param[0], param[1], param[2]);

	for (i = 0; i < ARRAY_SIZE(alps_model_data); i++)
		if (!memcmp(param, alps_model_data[i].signature, sizeof(alps_model_data[i].signature)))
			return alps_model_data[i].model;

	return -1;
}

/*
 * For DualPoint devices select the device that should respond to
 * subsequent commands. It looks like glidepad is behind stickpointer,
 * I'd thought it would be other way around...
 */
static int alps_passthrough_mode(struct psmouse *psmouse, int enable)
{
	struct ps2dev *ps2dev = &psmouse->ps2dev;
	unsigned char param[3];
	int cmd = enable ? PSMOUSE_CMD_SETSCALE21 : PSMOUSE_CMD_SETSCALE11;

	if (ps2_command(ps2dev, NULL, cmd) ||
	    ps2_command(ps2dev, NULL, cmd) ||
	    ps2_command(ps2dev, NULL, cmd) ||
	    ps2_command(ps2dev, NULL, PSMOUSE_CMD_DISABLE))
		return -1;

	/* we may get 3 more bytes, just ignore them */
	ps2_command(ps2dev, param, 0x0300);

	return 0;
}

static int alps_absolute_mode(struct psmouse *psmouse)
{
	struct ps2dev *ps2dev = &psmouse->ps2dev;

	/* Try ALPS magic knock - 4 disable before enable */
	if (ps2_command(ps2dev, NULL, PSMOUSE_CMD_DISABLE) ||
	    ps2_command(ps2dev, NULL, PSMOUSE_CMD_DISABLE) ||
	    ps2_command(ps2dev, NULL, PSMOUSE_CMD_DISABLE) ||
	    ps2_command(ps2dev, NULL, PSMOUSE_CMD_DISABLE) ||
	    ps2_command(ps2dev, NULL, PSMOUSE_CMD_ENABLE))
		return -1;

	/*
	 * Switch mouse to poll (remote) mode so motion data will not
	 * get in our way
	 */
	return ps2_command(&psmouse->ps2dev, NULL, PSMOUSE_CMD_SETPOLL);
}

static int alps_get_status(struct psmouse *psmouse, char *param)
{
	struct ps2dev *ps2dev = &psmouse->ps2dev;

	/* Get status: 0xF5 0xF5 0xF5 0xE9 */
	if (ps2_command(ps2dev, NULL, PSMOUSE_CMD_DISABLE) ||
	    ps2_command(ps2dev, NULL, PSMOUSE_CMD_DISABLE) ||
	    ps2_command(ps2dev, NULL, PSMOUSE_CMD_DISABLE) ||
	    ps2_command(ps2dev, param, PSMOUSE_CMD_GETINFO))
		return -1;

	dbg("Status: %2.2x %2.2x %2.2x", param[0], param[1], param[2]);

	return 0;
}

/*
 * Turn touchpad tapping on or off. The sequences are:
 * 0xE9 0xF5 0xF5 0xF3 0x0A to enable,
 * 0xE9 0xF5 0xF5 0xE8 0x00 to disable.
 * My guess that 0xE9 (GetInfo) is here as a sync point.
 * For models that also have stickpointer (DualPoints) its tapping
 * is controlled separately (0xE6 0xE6 0xE6 0xF3 0x14|0x0A) but
 * we don't fiddle with it.
 */
static int alps_tap_mode(struct psmouse *psmouse, int enable)
{
	struct ps2dev *ps2dev = &psmouse->ps2dev;
	int cmd = enable ? PSMOUSE_CMD_SETRATE : PSMOUSE_CMD_SETRES;
	unsigned char tap_arg = enable ? 0x0A : 0x00;
	unsigned char param[4];

	if (ps2_command(ps2dev, param, PSMOUSE_CMD_GETINFO) ||
	    ps2_command(ps2dev, NULL, PSMOUSE_CMD_DISABLE) ||
	    ps2_command(ps2dev, NULL, PSMOUSE_CMD_DISABLE) ||
	    ps2_command(ps2dev, &tap_arg, cmd))
		return -1;

	if (alps_get_status(psmouse, param))
		return -1;

	return 0;
}

static int alps_reconnect(struct psmouse *psmouse)
{
	int model;
	unsigned char param[4];

	if ((model = alps_get_model(psmouse)) < 0)
		return -1;

	if (model == ALPS_MODEL_DUALPOINT && alps_passthrough_mode(psmouse, 1))
		return -1;

	if (alps_get_status(psmouse, param))
		return -1;

	if (param[0] & 0x04)
		alps_tap_mode(psmouse, 0);

	if (alps_absolute_mode(psmouse)) {
		printk(KERN_ERR "alps.c: Failed to enable absolute mode\n");
		return -1;
	}

	if (model == ALPS_MODEL_DUALPOINT && alps_passthrough_mode(psmouse, 0))
		return -1;

	return 0;
}

static void alps_disconnect(struct psmouse *psmouse)
{
	psmouse_reset(psmouse);
}

int alps_init(struct psmouse *psmouse)
{
	unsigned char param[4];
	int model;

	if ((model = alps_get_model(psmouse)) < 0)
		return -1;

	printk(KERN_INFO "ALPS Touchpad (%s) detected\n",
		model == ALPS_MODEL_GLIDEPOINT ? "Glidepoint" : "Dualpoint");

	if (model == ALPS_MODEL_DUALPOINT && alps_passthrough_mode(psmouse, 1))
		return -1;

	if (alps_get_status(psmouse, param)) {
		printk(KERN_ERR "alps.c: touchpad status report request failed\n");
		return -1;
	}

	if (param[0] & 0x04) {
		printk(KERN_INFO "  Disabling hardware tapping\n");
		if (alps_tap_mode(psmouse, 0))
			printk(KERN_WARNING "alps.c: Failed to disable hardware tapping\n");
	}

	if (alps_absolute_mode(psmouse)) {
		printk(KERN_ERR "alps.c: Failed to enable absolute mode\n");
		return -1;
	}

	if (model == ALPS_MODEL_DUALPOINT && alps_passthrough_mode(psmouse, 0))
		return -1;

	psmouse->dev.evbit[LONG(EV_REL)] |= BIT(EV_REL);
	psmouse->dev.relbit[LONG(REL_X)] |= BIT(REL_X);
	psmouse->dev.relbit[LONG(REL_Y)] |= BIT(REL_Y);
	psmouse->dev.keybit[LONG(BTN_A)] |= BIT(BTN_A);
	psmouse->dev.keybit[LONG(BTN_B)] |= BIT(BTN_B);

	psmouse->dev.evbit[LONG(EV_ABS)] |= BIT(EV_ABS);
	input_set_abs_params(&psmouse->dev, ABS_X, 0, 1023, 0, 0);
	input_set_abs_params(&psmouse->dev, ABS_Y, 0, 1023, 0, 0);
	input_set_abs_params(&psmouse->dev, ABS_PRESSURE, 0, 127, 0, 0);

	psmouse->dev.keybit[LONG(BTN_TOUCH)] |= BIT(BTN_TOUCH);
	psmouse->dev.keybit[LONG(BTN_TOOL_FINGER)] |= BIT(BTN_TOOL_FINGER);
	psmouse->dev.keybit[LONG(BTN_FORWARD)] |= BIT(BTN_FORWARD);
	psmouse->dev.keybit[LONG(BTN_BACK)] |= BIT(BTN_BACK);

	psmouse->protocol_handler = alps_process_byte;
	psmouse->disconnect = alps_disconnect;
	psmouse->reconnect = alps_reconnect;
	psmouse->pktsize = 6;

	return 0;
}

int alps_detect(struct psmouse *psmouse, int set_properties)
{
	if (alps_get_model(psmouse) < 0)
		return -1;

	if (set_properties) {
		psmouse->vendor = "ALPS";
		psmouse->name = "TouchPad";
	}
	return 0;
}