#include <sys/stat.h>
#include <sys/sysmacros.h>
#include <sys/time.h>
#include <dirent.h>
#include <stdio.h>
#include <errno.h>
#include <signal.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <termios.h>
#include <fcntl.h>

#include "mdetect.h"

void really_giveup(int signum);

static void		usage(int);
static struct device *	stage1(struct device *);
static void		stage2(struct device *);
static int		handle_device(struct device *, fd_set *, unsigned long);
static struct device *	open_devices(void);
static struct device *	open_device(const char *name);
static void restore_serial_settings(int code, void *data);

static int		opt_repeater = 0;
int			opt_verbose = 0;
int			opt_nopnp = 0;
int			opt_xf86config = 0;
int			xf86format = 0;

static fd_set		mouse_set;

int
main(int argc, char **argv)
{
	struct device	*dev, *tmp, *found;
	int		c;

	while ((c = getopt(argc, argv, "hnorvx")) != -1) {
		switch (c) {
		case 'v':
			opt_verbose++;
			break;
		case 'r':
			opt_repeater = 1;
			break;
		case 'n':
			opt_nopnp = 1;
			break;
		case 'o':
			xf86format = 3;
			opt_xf86config = 1;
			break;
		case 'x':
			xf86format = 4;
			opt_xf86config = 1;
			break;
		case 'h':
			usage(0);
		default:
			usage(1);
		}
	}

	/* set up signal handler */
	if (signal(SIGALRM, really_giveup) == SIG_ERR) {
		fprintf(stderr, "mdetect: can't set up signal handler!\n");
		exit(3);
	}

	alarm(5); /* don't try to find a mouse for more than a few seconds */

	if (opt_verbose)
		printf("The mouse police never sleep.\n");
	if (!(dev = open_devices()))
		return 1;

	if (opt_verbose) {
		printf("Found the following devices:\n");
		for (tmp = dev; tmp; tmp = tmp->next)
			printf("   %s\n", tmp->name);
	}

	/* Register a fucntion to restore all serial port settings on any
	 * still open ports.
	 */
	on_exit(restore_serial_settings, (void *)dev);

	/* Now listen on all devices */
	found = stage1(dev);

	if (found)
		stage2(found);

	while (dev) {
		if (dev->fd >= 0) {
			tcflush(dev->fd, TCIOFLUSH);
			if (dev->flags & DEV_SERIAL_PORT)
				tcsetattr(dev->fd, TCSAFLUSH, &dev->tty);
			close(dev->fd);
			dev->fd = -1;
		}
		dev = dev->next;
	}

	return found ? 0 : 1;
}

/* Normally this is called once if mdetect's overall 5 second timer expires.
 * When that happens, it sets another alarm for 1 second, and calls exit(2).
 * exit(2) will call restore_serial_settings(), previously registered via
 * on_exit(), and exit.  The 1 second timer is to catch any hangs in the
 * exit() call, and if that expires we call _exit(2) bypassing the on_exit()
 * stuff.
 */

void
really_giveup(int signum)
{
	static int abort_now = 0;

	if (abort_now)
		_exit(2);
	else {
		/* set up signal handler again in case it was reset to SIG_DFL */
		if (signal(SIGALRM, really_giveup) == SIG_ERR) {
			fprintf(stderr, "mdetect: can't set up signal handler second time\n");
			_exit(2);
		}
		abort_now = 1;
		alarm(1);
		exit(2);
	}
}

static void
restore_serial_settings(int code, void *data)
{
	struct device *dev = (struct device *)data;

	while (dev) {
		if (dev->fd >= 0) {
			if (dev->flags & DEV_SERIAL_PORT)
				tcsetattr(dev->fd, TCSANOW, &dev->tty);
			close(dev->fd);
		}
		dev = dev->next;
	}
}

static void
usage(int exitval)
{
	fprintf(stderr, "Usage: mdetect [-hnrvox]\n");
	fprintf(stderr, "\t-h\tdisplay this help message and exit\n"
			"\t-n\tdo not search for PnP (Plug 'n' Play) mice\n"
			"\t-r\t[not yet implemented]\n"
			"\t-v\tbe verbose; re-use for increasing verbosity\n"
			"\t-o\tproduce output appropriate for XFree86 3.x configuration\n"
			"\t-x\tproduce output appropriate for XFree86 4.x configuration\n");
	exit(exitval);
}

static struct device *
stage1(struct device *dev)
{
	struct device	*tmp;
	unsigned long	timeout;
	struct timeval	then;
	fd_set		set;
	int		max = -1;

	FD_ZERO(&mouse_set);
	FD_ZERO(&set);
	for (tmp = dev; tmp; tmp = tmp->next) {
		FD_SET(tmp->fd, &mouse_set);
		if (max < tmp->fd)
			max = tmp->fd;
	}

	/* Initialize time-keeping */
	gettimeofday(&then, NULL);
	while (1) {
		struct timeval	now, delta, tv, *tvp = NULL;
		unsigned long	elapsed;

		gettimeofday(&now, NULL);
		timersub(&now, &then, &delta);
		elapsed = delta.tv_sec * 1000 + delta.tv_usec / 1000;
		timeout = 0;
		for (tmp = dev; tmp; tmp = tmp->next) {
			handle_device(tmp, &set, elapsed);
			if (timeout == 0 || tmp->timeout < timeout)
				timeout = tmp->timeout;
			if (tmp->flags & DEV_DRIVER_FOUND)
				return tmp;
		}

		memcpy(&set, &mouse_set, sizeof(mouse_set));
		if (timeout) {
			tv.tv_sec  = 0;
			tv.tv_usec = timeout * 1000;
			tvp = &tv;
		}

		then = now;
		if (select(max + 1, &set, NULL, NULL, tvp) < 0) {
			perror("select");
			exit(1);
		}
	}

	return NULL;
}

static void
stage2(struct device *dev)
{
	struct mouse	*driver = dev->driver;
	const char	*name;
	unsigned char	c;

	name = driver->name;
	if (driver->report)
		name = driver->report(driver);
	if (!opt_xf86config) {
		if (opt_verbose) {
			printf("\nDetected %s mouse on %s\n", driver->name, dev->name);
			fflush(stdout);
		} else {
			printf("%s\n%s\n", dev->name, driver->name);
			fflush(stdout);
		}
	} else {
		/* nasty, nasty special case logic for XFree86 */
		if ((strcmp(dev->name, "/dev/psaux") == 0) || (strcmp(dev->name, "/dev/misc/psaux") == 0)
#ifdef DEV_PSAUX_PREFIX
		   || (strncmp(dev->name, "/dev/" DEV_PSAUX_PREFIX, sizeof("/dev/" DEV_PSAUX_PREFIX) - 1) == 0)
#endif
		   ) {
			if (strcasecmp(driver->xfree86_protocol_name, "IntelliMouse") == 0) {
				driver->xfree86_protocol_name = "ImPS/2";
			}
		}
		if (opt_verbose) {
			switch (xf86format) {
				case 3:
					printf("Section \"Pointer\"\n");
					printf("\tProtocol\t\"%s\"\n", driver->xfree86_protocol_name);
					printf("\tDevice\t\t\"%s\"\n", dev->name);
					printf("EndSection\n");
					break;
				case 4:
					printf("Section \"InputDevice\"\n");
					printf("\tIdentifier\t\"Generic Mouse\"\n");
					printf("\tDriver\t\t\"mouse\"\n");
					printf("\tOption\t\t\"CorePointer\"\n");
					printf("\tOption\t\t\"Protocol\"\t\"%s\"\n", driver->xfree86_protocol_name);
					printf("\tOption\t\t\"Device\"\t\"%s\"\n", dev->name);
					printf("EndSection\n");
					break;
				default:
					fprintf(stderr, "Unknown XF86Config output format!\n");
					break;
			}
		} else {
			printf("%s\n%s\n", dev->name, driver->xfree86_protocol_name);
		}
		fflush(stdout);
	}

	/* Repeater code not yet complete */
	if (opt_repeater) {
		while (1) {
			if (read(dev->fd, &c, 1) >= 0)
				driver->packet(dev, driver, c);
		}
	}
}

static int
handle_device(struct device *dev, fd_set *set, unsigned long elapsed)
{
	struct mouse	*driver = dev->driver;

	if (dev->fd >= 0 && FD_ISSET(dev->fd, set)) {
		unsigned char	c;

		dev->timeout = 0;
		if (read(dev->fd, &c, 1) < 0)
			goto bad;
		if (opt_verbose > 2)
			printf("    %-12s read 0x%02x\n", dev->name, c);
		if (driver->packet(dev, driver, c) < 0)
			goto bad;
	} else if (dev->timeout) {
		if (opt_verbose)
			printf("    %-12s timeout\n", dev->name);
		if (elapsed > dev->timeout) {
			if (!driver->timeout || driver->timeout(dev) < 0)
				goto bad;
		}
		dev->timeout = 0;
	}
	if (driver->good > MIN_GOOD_EVENTS)
		dev->flags |= DEV_DRIVER_FOUND;

	return 0;

bad:
	if (opt_verbose)
		printf("\nClosing garbage device %s.\n", dev->name);
	FD_CLR(dev->fd, &mouse_set);
	if (dev->flags & DEV_SERIAL_PORT)
		tcsetattr(dev->fd, TCSAFLUSH, &dev->tty);
	close(dev->fd);
	dev->fd = -1;
	dev->timeout = 0;
	return -1;
}

/*
 * Send an event
 */
int
send_event(struct mouse *driver)
{
	static struct mouse	*last = NULL;
	int			ddx, ddy;

#if 0
	if (last != driver && opt_verbose)
		printf("\n");
#endif
	last = driver;

	if ((ddx = driver->lx - driver->dx) < 0)
		ddx = -ddx;
	if ((ddy = driver->ly - driver->dy) < 0)
		ddy = -ddy;
	if (opt_verbose) {
		unsigned int	b =driver->buttons;

		printf("%3d %-12s %c%c%c %4d %4d %4d (der %3d) %3d%%   \r",
			driver->total, driver->name,
			(b & BUTTON_LEFT)?   'L' : ' ',
			(b & BUTTON_MIDDLE)? 'M' : ' ',
			(b & BUTTON_RIGHT)?  'R' : ' ',
			driver->dx, driver->dy, driver->dz,
			ddx + ddy,
			100 * driver->good / MIN_GOOD_EVENTS);
		fflush(stdout);
		if (opt_verbose > 1)
			printf("\n");
	}
	driver->lx = driver->dx;
	driver->ly = driver->dy;
	driver->lz = driver->dz;

	/* Consistency checks for the first 20 events.
	 * Note that the button check is the most effective one.
	 * Motion values aren't that far off most of the time,
	 * but buttons frequently are.
	 * XXX: Tue Jun 22 11:12:25 MET DST 1999
	 *  I disabled motion checks. All they effected was requiring
	 *  the user to move the mouse `slowly' according to various
	 *  reports.
	 *
	 *  Without motion checks, mdetect still seems to work reliably,
	 *  plus you can jerk the mouse as much as you please.
	 *			--okir
	 */
	if (driver->good < MIN_GOOD_EVENTS) {
		if ((driver->buttons & (BUTTON_MIDDLE|BUTTON_RIGHT))
#if 0
		 || ddx > 30
		 || (driver->dx < -200 || driver->dx > 200)
		 || (driver->dy < -200 || driver->dy > 200)
#endif
		) {
			if (opt_verbose)
				printf("\n");
			driver->bad++;
			return -1;
		}
	} else {
		/* Deliver the event to the fifo */
	}

	driver->good++;
	return 0;
}

/* Open all devices */
static struct device *
open_devices(void)
{
	struct device	*dev = NULL, *tmp;
	DIR		*dir;
	struct dirent	*de;

	/* look for a USB mouse first */
	if ((tmp = open_device("input/mouse0")) != 0) {
		tmp->next = dev;
		dev = tmp;
	}

	/* search /dev for something works */
	/* XXX is this *really* a good idea? */
	if (!(dir = opendir("/dev"))) {
		perror("/dev");
		return NULL;
	}

	while ((de = readdir(dir)) != 0) {
		if ((tmp = open_device(de->d_name)) != 0) {
			tmp->next = dev;
			dev = tmp;
		}
	}
	closedir(dir);

	return dev;
}

static struct device *
open_device(const char *name)
{
	struct device	temp, *dev;
	struct stat	stb;
	struct mouse	*driver = NULL;

	if (opt_verbose) {
		fprintf(stderr, "Opening device %s...\n", name);
	}

	memset(&temp, 0, sizeof(temp));
	strcat(strcpy(temp.name, "/dev/"), name);

	if (!strncmp(name, DEV_SERIAL_PREFIX, sizeof(DEV_SERIAL_PREFIX) - 1)) {
		driver = get_mouse("serial");
		temp.flags |= DEV_SERIAL_PORT;

	}
#ifdef DEV_PSAUX_PREFIX
	else if (!strncmp(name, DEV_PSAUX_PREFIX, sizeof(DEV_PSAUX_PREFIX) - 1)) {
		driver = get_mouse("psaux");
	}
#endif
#ifdef DEV_USB_PREFIX
	else if (!strncmp(name, DEV_USB_PREFIX, sizeof(DEV_USB_PREFIX) - 1)) {
		driver = get_mouse("usb");
	}
#endif
#ifdef DEV_BUSMOUSE_PREFIX
	else if (!strncmp(name, DEV_BUSMOUSE_PREFIX, sizeof(DEV_BUSMOUSE_PREFIX) - 1)) {
		driver = get_mouse("sunmouse");
	}
#endif
#ifdef __linux__
	else {
		if (stat(temp.name, &stb) < 0 || !S_ISCHR(stb.st_mode))
			return NULL;

		switch (major(stb.st_rdev)) {
		case 10:	/* bus mice */
		case 13:	/* 2.4 kernel USB mice (really just 32-63) */
			driver = get_mouse(name);
			break;
		}
	}
#endif

	if (driver == NULL)
		return NULL;

	temp.driver = driver;
	if ((temp.fd = open(temp.name, O_RDWR)) < 0)
		return NULL;
	dev = (struct device *) malloc(sizeof(*dev));
	memcpy(dev, &temp, sizeof(*dev));
	if (dev->flags & DEV_SERIAL_PORT)
		tcgetattr(dev->fd, &dev->tty);

	if (driver->init) {
		if (dev->driver->init(dev, driver) < 0) {
			if (dev->flags & DEV_SERIAL_PORT)
				tcsetattr(dev->fd, TCSAFLUSH, &dev->tty);
			close(dev->fd);
			free(dev);
			return NULL;
		}
	}
	return dev;
}
