/*         ______   ___    ___
 *        /\  _  \ /\_ \  /\_ \
 *        \ \ \L\ \\//\ \ \//\ \      __     __   _ __   ___
 *         \ \  __ \ \ \ \  \ \ \   /'__`\ /'_ `\/\`'__\/ __`\
 *          \ \ \/\ \ \_\ \_ \_\ \_/\  __//\ \L\ \ \ \//\ \L\ \
 *           \ \_\ \_\/\____\/\____\ \____\ \____ \ \_\\ \____/
 *            \/_/\/_/\/____/\/____/\/____/\/___L\ \/_/ \/___/
 *                                           /\____/
 *                                           \_/__/
 *
 *      Linux console internal mouse driver for PS/2.
 *
 *      By George Foot.
 *
 *      See readme.txt for copyright information.
 */

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <sys/time.h>

#include "allegro.h"
#include "allegro/aintern.h"
#include "allegro/aintunix.h"
#include "linalleg.h"


static int intellimouse = FALSE;
static int packet_size = 3;


/* processor:
 *  Processes the first packet in the buffer, if any, returning the number
 *  of bytes eaten.
 *
 *  PS/2 mouse protocol is pretty simple; packets consist of three bytes,
 *  one header byte and two data bytes.  In the header, the bottom three
 *  bits show the current button state.  The next bit should always be
 *  set, but apparently some mice don't do this.  The next two bits are 
 *  the X and Y sign bits, and the last two are the X and Y overflow 
 *  flags.  The two data bytes are the X and Y deltas; prepend the 
 *  corresponding sign bit to get a nine-bit two's complement number.
 *
 *  Finding the header byte can be tricky in you lose sync; bit 3 isn't
 *  that useful, not only because some mice don't set it, but also 
 *  because a data byte could have it set.  The overflow bits are very
 *  rarely set, so we just test for these being zero.  Of course the 
 *  data bytes can have zeros here too, but as soon as you move the 
 *  mouse down or left the data bytes become negative and have ones 
 *  here instead.
 */
static int processor (unsigned char *buf, int buf_size, struct mouse_info *info)
{
   info->updated = 0;
   if (buf_size < packet_size) return 0;  /* not enough data, spit it out for now */

   /* if data is invalid, return no motion and all buttons released */
   info->l = info->r = info->m = info->x = info->y = 0;
   info->updated = 1;
   if ((buf[0] & 0xc0) != 0x00) return 1; /* invalid byte, eat it */

   /* data is valid, process it */
   info->l = !!(buf[0] & 1);
   info->r = !!(buf[0] & 2);
   info->m = !!(buf[0] & 4);

   info->x = buf[1];
   info->y = buf[2];
   if (buf[0] & 0x10) info->x -= 256;
   if (buf[0] & 0x20) info->y -= 256;

   if (intellimouse) {
      info->z = buf[3] & 0xf;
      if (info->z) 
	 info->z = (info->z-7) >> 3;
   }
   else
      info->z = 0;

   info->updated = 1;
   return packet_size; /* yum */
}

/* analyse_data:
 *  Analyses the given data, returning 0 if it is unparsable, nonzero
 *  if there's a reasonable chance that this driver can work with that
 *  data.
 */
static int analyse_data (AL_CONST char *buffer, int size)
{
	int pos = 0;
	int packets = 0, errors = 0;

	int step = 0;

	for (pos = 0; pos < size; pos++)
		switch (step) {
			case 3:
				packets++;
				step = 0;
			case 0:
				if (buffer[pos] & 0xC0) {
					errors++;
				} else {
					step++;
				}
				break;

			case 1:
			case 2:
				step++;
				break;
		}

	return (errors <= 5) || (errors < size / 20);    /* 5% error allowance */
}


static INTERNAL_MOUSE_DRIVER intdrv = {
   -1,
   processor,
   3
};

/* sync_mouse:
 *  To find the start of a packet, we just read all the data that's 
 *  waiting.  This isn't a particularly good way, obviously. :)
 */
static void sync_mouse (int fd)
{
	fd_set set;
	int result;
	struct timeval tv;
	char bitbucket;

	do {
		FD_ZERO (&set);
		FD_SET (fd, &set);
		tv.tv_sec = tv.tv_usec = 0;
		result = select (FD_SETSIZE, &set, NULL, NULL, &tv);
		if (result > 0) read (fd, &bitbucket, 1);
	} while (result > 0);
}

/* wakeup_im:
 *  Intellimouse needs some special initialisation.  Unfortunately this
 *  requires write permissions to the device, which probably only root has.
 *  In that case the mouse can be initialised by root with gpm (using the
 *  `imps2' driver).
 */
static void wakeup_im(int fd)
{
	unsigned char init[] = { 243, 200, 243, 100, 243, 80, 246, 230, 244, 243, 100, 232, 3 };
	int ret;
	do {
		ret = write(fd, init, sizeof(init));
		if ((ret < 0) && (errno != EINTR))
			break;
	} while (ret < sizeof(init));
}

/* mouse_init:
 *  Here we open the mouse device, initialise anything that needs it, 
 *  and chain to the framework init routine.
 */
static int mouse_init (void)
{
	char tmp[80], tmp2[80];
	AL_CONST char *device, *ext;
	int i;

	/* Find the device filename */
	device = get_config_string(NULL, uconvert_ascii("mouse_device", tmp), uconvert_ascii("/dev/mouse", tmp2));

	/* See if Intellimouse extensions (middle button + wheel) are enabled */
	ext = get_config_string(NULL, uconvert_ascii("intellimouse", tmp), NULL);
	if ((ext) && ((i = ugetc(ext)) != 0) &&
	    ((i == 'y') || (i == 'Y') || (i == '1'))) {
		intellimouse = TRUE;
		packet_size = 4;
		mousedrv_linux_ps2.name = mousedrv_linux_ps2.desc = get_config_text("Linux PS/2 Intellimouse");

		i = open (device, O_WRONLY);
		if (i > 0) {
			wakeup_im (i);
			close (i);
		}
	}

	/* Open mouse device.  Devices are cool. */
	intdrv.device = open (device, O_RDONLY | O_NONBLOCK);
	if (intdrv.device < 0) {
		usprintf (allegro_error, get_config_text ("Unable to open %s: %s"), uconvert_ascii ("/dev/mouse", tmp), ustrerror (errno));
		return -1;
	}

	/* Discard any garbage, so the next thing we read is a packet header */
	sync_mouse (intdrv.device);

	return __al_linux_mouse_init (&intdrv);
}

/* mouse_exit:
 *  Chain to the framework, then uninitialise things.
 */
static void mouse_exit (void)
{
	__al_linux_mouse_exit();
	close (intdrv.device);
}

MOUSE_DRIVER mousedrv_linux_ps2 =
{
	MOUSEDRV_LINUX_PS2,
	empty_string,
	empty_string,
	"Linux PS/2 mouse",
	mouse_init,
	mouse_exit,
	NULL, /* poll() */
	NULL, /* timer_poll() */
	__al_linux_mouse_position,
	__al_linux_mouse_set_range,
	__al_linux_mouse_set_speed,
	__al_linux_mouse_get_mickeys,
	analyse_data
};

