/*
 * hdaps.c - driver for IBM HDAPS (HardDisk Active Protection system)
 *
 * Based on the document by Mark A. Smith available at
 * http://www.almaden.ibm.com/cs/people/marksmith/tpaps.html
 *
 * Copyright (C) 2005 Jesper Juhl <jesper.juhl@gmail.com>
 * Copyright (C) 2005 Robert Love <rml@novell.com>
 *
 * 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/config.h>
#include <linux/delay.h>
#include <linux/device.h>
#include <linux/fs.h>
#include <linux/input.h>
#include <linux/ioport.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/miscdevice.h>
#include <linux/timer.h>
#include <linux/spinlock.h>
#include <asm/io.h>
#include <asm/uaccess.h>

#include "hdaps.h"

#define HDAPS_LOW_PORT	0x1600	/* first port used by accelerometer */
#define HDAPS_NR_PORTS	0x30	/* nr of ports total - 0x1600 through 0x162f */

#define STATE_STALE	0x00	/* accelerometer data is stale */
#define STATE_FRESH	0x50	/* accelerometer data is fresh */

#define REFRESH_ASYNC	0x00	/* do asynchronous refresh */
#define REFRESH_SYNC	0x01	/* do synchronous refresh */

/* 
 * where to find the various accelerometer data
 * these map to the members of struct hdaps_data
 */
#define HDAPS_PORT_STATE	0x1611
#define	HDAPS_PORT_XPOS		0x1612
#define HDAPS_PORT_YPOS		0x1614
#define HDAPS_PORT_TEMP		0x1616
#define HDAPS_PORT_XVAR		0x1617
#define HDAPS_PORT_YVAR		0x1619
#define HDAPS_PORT_TEMP2	0x161b
#define HDAPS_PORT_UNKNOWN	0x161c
#define HDAPS_PORT_KMACCT	0x161d

/*
 * __get_latch - Get the value from a given port latch.  Callers must hold
 * hdaps_data_lock.
 */
static inline unsigned short __get_latch(unsigned short port)
{
	return inb(port) & 0xff;
}

/*
 * __check_latch - Check a port latch for a given value.  Callers must hold
 * hdaps_data_lock.
 */
static inline unsigned int __check_latch(unsigned short port, unsigned char val)
{
	if (__get_latch(port) == val)
		return 1;
	return 0;
}

/*
 * __wait_latch - Wait up to 500us for a port latch to get a certain value,
 * returning nonzero if the value is obtained and zero otherwise.  Callers
 * must hold hdaps_data_lock.
 */
static unsigned int __wait_latch(unsigned short port, unsigned char val)
{
	unsigned long i;

	for (i = 0; i < 100; i++) {
		if (__check_latch(port, val))
			return 1;
		udelay(5);
	}

	printk(KERN_DEBUG "hdaps: %s(%04x, %02x) failed (%02x)\n",
	       __FUNCTION__, port, val, __check_latch(port, val));
	return 0;
}

/*
 * __check_state - Check the refresh state of the accelerometer, returning
 * STATE_FRESH if up-to-date and STATE_STALE otherwise.  Callers must hold
 * hdaps_data_lock.
 */
static unsigned int __check_state(void)
{
	unsigned state;

	state = inb(0x1604);
	if (state != STATE_FRESH)
		state = STATE_STALE;

	return state;
}

/*
 * __request_refresh - Request a refresh from the accelerometer.
 *
 * If sync is REFRESH_SYNC, we perform a synchronous refresh and will wait for
 * the refresh.  Returns nonzero if successful or zero on error.
 *
 * If sync is REFRESH_ASYNC, we merely kick off a new refresh if the device is
 * not up-to-date.  Always returns true.  On the next read from the device, the
 * data should be up-to-date but a synchronous wait should be performed to be
 * sure.
 * 
 * Callers must hold hdaps_data_lock.
 */
static int __request_refresh(int sync)
{
	unsigned int state;

	state = __check_state();
	if (state == STATE_FRESH)
		return 1;
	else {
		outb(0x11, 0x1610);
		outb(0x01, 0x161f);
		if (sync == REFRESH_ASYNC)
			return 1;
	}

	return __wait_latch(0x1604, STATE_FRESH);
}

/*
 * __tell_accelerometer_done - Indicate to the accelerometer that we are done
 * reading data.  Callers must hold hdaps_data_lock.
 */
static void __tell_accelerometer_done(void)
{
	inb(0x161f);
	inb(0x1604);
}

/*
 * __accellerometer_read - Read accelerometer data into provided structure,
 * returning zero on success and nonzero on failure.  Callers must hold
 * hdaps_data_lock.
 */
static unsigned int __accelerometer_read(struct hdaps_data *data)
{
	/* do a sync refresh - we need to be sure we read fresh data */
	if (!__request_refresh(REFRESH_SYNC))
		return 1;

	data->x_pos = inw(HDAPS_PORT_XPOS);
	data->y_pos = inw(HDAPS_PORT_YPOS);
	data->x_variation = inw(HDAPS_PORT_XVAR);
	data->y_variation = inw(HDAPS_PORT_YVAR);
	data->state = inb(HDAPS_PORT_STATE);	
	data->temp = inb(HDAPS_PORT_TEMP);
	data->temp2 = inb(HDAPS_PORT_TEMP2);
	data->unknown = inb(HDAPS_PORT_UNKNOWN);
	data->km_activity = inb(HDAPS_PORT_KMACCT);

	__tell_accelerometer_done();

	if (!__request_refresh(REFRESH_ASYNC))
		return 1;
	return 0;
}

/*
 * Serializes hardware access.
 */
static spinlock_t hdaps_data_lock = SPIN_LOCK_UNLOCKED;

/*
 * accelerometer_read - Reads from the accelerometer into the provided struct.
 */
static unsigned int accelerometer_read(struct hdaps_data *data)
{
	unsigned int ret;

	spin_lock(&hdaps_data_lock);
	ret = __accelerometer_read(data);
	spin_unlock(&hdaps_data_lock);

	return ret;
}

static int accelerometer_read_one(unsigned int port)
{
	int ret;

	spin_lock(&hdaps_data_lock);

	/* do a sync refresh - we need to be sure we read fresh data */
	if (!__request_refresh(REFRESH_SYNC)) {
		ret = -EIO;
		goto out;
	}

	ret = inw(port);

	__tell_accelerometer_done();

	if (!__request_refresh(REFRESH_ASYNC))
		ret = -EINVAL;

out:
	spin_unlock(&hdaps_data_lock);
	return ret;
}

/* initialize the accelerometer, wait up to `timeout' seconds for success */
static int accelerometer_init(unsigned int timeout_secs)
{
	unsigned long total_wait_msecs = timeout_secs * 1000;
	unsigned long msecs_per_wait = 200;
	int ret = -EIO;

	outb(0x13, 0x1610);
	outb(0x01, 0x161f);
	if (!__wait_latch(0x161f, 0x00))
		return ret;
	if (!__wait_latch(0x1611, 0x03)) {
		/*
		 * The 0x3 value appears to only work on some
		 * thinkpads, like T41s.  T42s return 0x1.
		 *
		 * The 0x2 value happens sometimes when the
		 * chip has previously been initialized.
		 */
		if (!__check_latch(0x1611, 0x02) &&
		    !__check_latch(0x1611, 0x01))
			return ret;
	}
	printk(KERN_DEBUG "%s() 0x1611 latch check OK: %02x\n",
	       __FUNCTION__, __get_latch(0x1611));
	outb(0x17, 0x1610);
	outb(0x81, 0x1611);
	outb(0x01, 0x161f);
	if (!__wait_latch(0x161f, 0x00))
		return ret;
	if (!__wait_latch(0x1611, 0x00))
		return ret;
	if (!__wait_latch(0x1612, 0x60))
		return ret;
	if (!__wait_latch(0x1613, 0x00))
		return ret;
	outb(0x14, 0x1610);
	outb(0x01, 0x1611);
	outb(0x01, 0x161f);
	if (!__wait_latch(0x161f, 0x00))
		return ret;
	outb(0x10, 0x1610);
	outb(0xc8, 0x1611);
	outb(0x00, 0x1612);
	outb(0x02, 0x1613);
	outb(0x01, 0x161f);
	if (!__wait_latch(0x161f, 0x00))
		return ret;
	if (!__request_refresh(REFRESH_SYNC))
		return ret;
	if (!__wait_latch(0x1611, 0x00))
		return ret;

	ret = -ENXIO;
	while (total_wait_msecs > 0) {
		struct hdaps_data data;

		/*
		 * The extra reads here seem to kick the
		 * accelerometer into being initialized
		 */
		__accelerometer_read(&data);

		if (!__wait_latch(0x1611, 0x02)) {
			msleep(msecs_per_wait);
			total_wait_msecs -= msecs_per_wait;
			continue;
		}

		ret = 0;
		break;
	}

	return ret;
}


/* device node interfaces */

static ssize_t hdaps_read(struct file *filp, char __user *buf,
			  size_t count, loff_t *pos)
{
	struct hdaps_data *data = filp->private_data;

	/* only perform whole reads */
	if (unlikely(count < sizeof (struct hdaps_data)))
		return 0;

	if (accelerometer_read(data))
		return -EIO;

	if (copy_to_user(buf, data, sizeof (struct hdaps_data)))
		return -EFAULT;

	/* we always read the exact same thing: one structure */
	return sizeof (struct hdaps_data);
}

static int hdaps_open(struct inode *inode, struct file *filp)
{
	struct hdaps_data *data;
	int ret;

	ret = nonseekable_open(inode, filp);
	if (unlikely(ret))
		return ret;

	data = kmalloc(sizeof (struct hdaps_data), GFP_KERNEL);
	if (unlikely(!data))
		return -ENOMEM;

	/* we hand this back to user-space, so zero it out just in case */
	/* XXX: this can use the kzalloc in 2.6-mm once merged */
	memset(data, 0, sizeof (struct hdaps_data));

	filp->private_data = data;

	return 0;
}

static int hdaps_release(struct inode *inode, struct file *filp)
{
	kfree(filp->private_data);
	return 0;
}

static struct file_operations hdaps_ops = {
	.owner = THIS_MODULE,
	.read = hdaps_read,
	.open = hdaps_open,
	.release = hdaps_release,
};

static DECLARE_COMPLETION(hdaps_obj_is_free);
static void hdaps_release_dev(struct device *dev)
{
	complete(&hdaps_obj_is_free);
}

struct platform_device hdaps_plat_dev = {
	.name = "hdaps",
	.id = -1,
	.dev = { .release = hdaps_release_dev }
};


/* Input device stuff */

static struct input_dev hdaps_idev;
static struct timer_list hdaps_poll_timer;
static unsigned int hdaps_mousedev_fuzz = 4;
static unsigned long hdaps_poll_int_ms = 25;
static int hdaps_mousedev_registered;
static u16 rest_x;
static u16 rest_y;

static void hdaps_calibrate(void)
{
	struct hdaps_data accel_data;
	accelerometer_read(&accel_data);
	rest_x = accel_data.x_pos;
	rest_y = accel_data.y_pos;
}

static struct miscdevice hdaps_dev = {
	.minor = MISC_DYNAMIC_MINOR,
	.name = "hdaps",
	.fops = &hdaps_ops,
};

static void hdaps_mousedev_poll(unsigned long unused)
{
	int movex, movey;
	struct hdaps_data accel_data;

	accelerometer_read(&accel_data);
	movex = rest_x - accel_data.x_pos;
	movey = rest_y - accel_data.y_pos;
	if (abs(movex) > hdaps_mousedev_fuzz)
		input_report_rel(&hdaps_idev, REL_Y, movex);
	if (abs(movey) > hdaps_mousedev_fuzz)
		input_report_rel(&hdaps_idev, REL_X, movey);
	input_sync(&hdaps_idev);

	mod_timer(&hdaps_poll_timer,
		  jiffies + msecs_to_jiffies(hdaps_poll_int_ms));
}

static void hdaps_mousedev_enable(void)
{
	hdaps_idev.dev = &hdaps_plat_dev.dev;
	init_input_dev(&hdaps_idev);
	hdaps_idev.evbit[0] = BIT(EV_KEY) | BIT(EV_REL);
	hdaps_idev.relbit[0] = BIT(REL_X) | BIT(REL_Y);
	hdaps_idev.keybit[LONG(BTN_LEFT)] = BIT(BTN_LEFT);
	input_register_device(&hdaps_idev);
	hdaps_mousedev_registered = 1;

	init_timer(&hdaps_poll_timer);
	hdaps_poll_timer.function = hdaps_mousedev_poll;
	hdaps_poll_timer.expires =
	    jiffies + msecs_to_jiffies(hdaps_poll_int_ms);
	add_timer(&hdaps_poll_timer);

	printk(KERN_DEBUG "hdaps: input device enabled\n");
}

static void hdaps_mousedev_disable(void)
{
	if (!hdaps_mousedev_registered)
		return;

	del_timer_sync(&hdaps_poll_timer);
	input_unregister_device(&hdaps_idev);
}


/* Sysfs Files */

static ssize_t hdaps_x_show(struct device *dev, struct device_attribute *attr,
			    char *buf)
{
	int ret;

	ret = accelerometer_read_one(HDAPS_PORT_XPOS);
	if (ret < 0)
		return ret;

	return sprintf(buf, "%d\n", ret);
}
static DEVICE_ATTR(x, 0444, hdaps_x_show, NULL);

static ssize_t hdaps_y_show(struct device *dev, struct device_attribute *attr,
			    char *buf)
{
	int ret;

	ret = accelerometer_read_one(HDAPS_PORT_YPOS);
	if (ret < 0)
		return ret;

	return sprintf(buf, "%d\n", ret);
}
static DEVICE_ATTR(y, 0444, hdaps_y_show, NULL);

static ssize_t hdaps_mousedev_show(struct device *dev,
				   struct device_attribute *attr, char *buf)
{
	return sprintf(buf, "%d\n", hdaps_mousedev_registered);
}

static ssize_t hdaps_mousedev_store(struct device *dev,
				    struct device_attribute *attr,
				    const char *buf, size_t count)
{
	int enable;

	if (sscanf(buf, "%d\n", &enable) != 1)
		return -EINVAL;

	if (enable == 1) {
		hdaps_calibrate();
		hdaps_mousedev_enable();
	} else if (enable == 0)
		hdaps_mousedev_disable();

	return count;
}

static DEVICE_ATTR(mousedev, 0644, hdaps_mousedev_show, hdaps_mousedev_store);

static ssize_t hdaps_calibrate_store(struct device *dev,
				     struct device_attribute *attr,
				     const char *buf, size_t count)
{
	hdaps_calibrate();
	return count;
}
static DEVICE_ATTR(calibrate, 0644, NULL, hdaps_calibrate_store);

static ssize_t hdaps_fuzz_show(struct device *dev,
			       struct device_attribute *attr, char *buf)
{
	return sprintf(buf, "%u\n", hdaps_mousedev_fuzz);
}

static ssize_t hdaps_fuzz_store(struct device *dev,
				struct device_attribute *attr,
				const char *buf, size_t count)
{
	unsigned int fuzz;

	if (sscanf(buf, "%u\n", &fuzz) != 1 || fuzz == 0)
		return -EINVAL;
	hdaps_mousedev_fuzz = fuzz;

	return count;
}

static DEVICE_ATTR(fuzz, 0644, hdaps_fuzz_show, hdaps_fuzz_store);

static ssize_t hdaps_poll_int_show(struct device *dev,
				   struct device_attribute *attr, char *buf)
{
	return sprintf(buf, "%lu\n", hdaps_poll_int_ms);
}

static ssize_t hdaps_poll_int_store(struct device *dev,
				    struct device_attribute *attr,
				    const char *buf, size_t count)
{
	unsigned int poll;

	if (sscanf(buf, "%u\n", &poll) != 1 || poll == 0)
		return -EINVAL;
	hdaps_poll_int_ms = poll;

	return count;
}

static DEVICE_ATTR(poll_interval_ms, 0644, hdaps_poll_int_show,
		   hdaps_poll_int_store);


/* module stuff */

static int __init hdaps_init(void)
{
	int ret;

	if (!request_region(HDAPS_LOW_PORT, HDAPS_NR_PORTS, "hdaps"))
		return -ENXIO;

	ret = accelerometer_init(10);
	if (ret)
		goto out_release;

	ret = misc_register(&hdaps_dev);
	if (ret)
		goto out_release;

	ret = platform_device_register(&hdaps_plat_dev);
	if (ret)
		goto out_misc;

	device_create_file(&hdaps_plat_dev.dev, &dev_attr_x);
	device_create_file(&hdaps_plat_dev.dev, &dev_attr_y);		
	device_create_file(&hdaps_plat_dev.dev, &dev_attr_calibrate);
	device_create_file(&hdaps_plat_dev.dev, &dev_attr_mousedev);
	device_create_file(&hdaps_plat_dev.dev, &dev_attr_fuzz);
	device_create_file(&hdaps_plat_dev.dev, &dev_attr_poll_interval_ms);

	printk(KERN_DEBUG "hdaps: initialized.\n");

	return 0;

out_misc:
	misc_deregister(&hdaps_dev);
out_release:
	release_region(HDAPS_LOW_PORT, HDAPS_NR_PORTS);
	printk(KERN_WARNING "hdaps: initilization failed!\n");
	return ret;
}

static void __exit hdaps_exit(void)
{
	hdaps_mousedev_disable();

	device_remove_file(&hdaps_plat_dev.dev, &dev_attr_calibrate);
	device_remove_file(&hdaps_plat_dev.dev, &dev_attr_mousedev);
	platform_device_unregister(&hdaps_plat_dev);

	misc_deregister(&hdaps_dev);
	release_region(HDAPS_LOW_PORT, HDAPS_NR_PORTS);
}

module_init(hdaps_init);
module_exit(hdaps_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Jesper Juhl");
