/* kpx-17s.c, a driver for the ALPS KPX-17S serial numeric keypad
 * Copyright (C) 2012 Mike Mammarella
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of version 2 of the GNU General Public License as
 * published by the Free Software Foundation.
 *
 * 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.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/param.h>
#include <sys/select.h>
#include <termios.h>
#include <unistd.h>

#include <X11/Xlib.h>
#include <X11/keysym.h>
#include <X11/extensions/XTest.h>

/* These are all the buttons on the KPX-17S. */
enum button_code
{
	INVALID = 0,  ENTER,        ZERO_NL_ON,    ZERO_NL_OFF,  ONE_NL_ON,
	ONE_NL_OFF,   TWO_NL_ON,    TWO_NL_OFF,    THREE_NL_ON,  THREE_NL_OFF,
	FOUR_NL_ON,   FOUR_NL_OFF,  FIVE_NL_ON,    FIVE_NL_OFF,  SIX_NL_ON,
	SIX_NL_OFF,   SEVEN_NL_ON,  SEVEN_NL_OFF,  EIGHT_NL_ON,  EIGHT_NL_OFF,
	NINE_NL_ON,   NINE_NL_OFF,  DOT_NL_ON,     DOT_NL_OFF,   PLUS_NL_ON,
	PLUS_NL_OFF,  SLASH_NL_ON,  SLASH_NL_OFF,  STAR_NL_ON,   STAR_NL_OFF,
	DASH_NL_ON,   DASH_NL_OFF,  NUMLOCK_ON,    NUMLOCK_OFF,  MAX_BUTTON
};

/* This is where we store the actual knowledge of what the KPX-17S sends. */
static const enum button_code button_codes[256] =
{
	['A'] = NUMLOCK_OFF,   ['P'] = ONE_NL_ON,     ['Q'] = TWO_NL_ON,
	['R'] = THREE_NL_ON,   ['S'] = FOUR_NL_ON,    ['T'] = FIVE_NL_ON,
	['U'] = SIX_NL_ON,     ['V'] = SEVEN_NL_ON,   ['W'] = EIGHT_NL_ON,
	['X'] = NINE_NL_ON,    ['Z'] = SLASH_NL_ON,   ['['] = STAR_NL_ON,
	['\\'] = DASH_NL_ON,   [']'] = PLUS_NL_ON,    ['^'] = DOT_NL_ON,
	['_'] = ZERO_NL_ON,    ['`'] = ONE_NL_OFF,    ['a'] = TWO_NL_OFF,
	['b'] = THREE_NL_OFF,  ['c'] = FOUR_NL_OFF,   ['d'] = FIVE_NL_OFF,
	['e'] = SIX_NL_OFF,    ['f'] = SEVEN_NL_OFF,  ['g'] = EIGHT_NL_OFF,
	['h'] = NINE_NL_OFF,   ['j'] = SLASH_NL_OFF,  ['k'] = STAR_NL_OFF,
	['l'] = DASH_NL_OFF,   ['m'] = PLUS_NL_OFF,   ['n'] = DOT_NL_OFF,
	['o'] = ZERO_NL_OFF,   ['t'] = ENTER,         ['y'] = NUMLOCK_ON
};

/* The names of the buttons in the config file and log messages. */
static const char * button_names[] =
{
	[ENTER]        = "Enter",  [ZERO_NL_ON]   = "0",     [ZERO_NL_OFF]  = "Ins",
	[ONE_NL_ON]    = "1",      [ONE_NL_OFF]   = "End",   [TWO_NL_ON]   = "2",
	[TWO_NL_OFF]   = "Down",   [THREE_NL_ON]  = "3",     [THREE_NL_OFF] = "PgDn",
	[FOUR_NL_ON]   = "4",      [FOUR_NL_OFF]  = "Left",  [FIVE_NL_ON]  = "5",
	[FIVE_NL_OFF]  = "5-",     [SIX_NL_ON]    = "6",     [SIX_NL_OFF]   = "Right",
	[SEVEN_NL_ON]  = "7",      [SEVEN_NL_OFF] = "Home",  [EIGHT_NL_ON] = "8",
	[EIGHT_NL_OFF] = "Up",     [NINE_NL_ON]   = "9",     [NINE_NL_OFF]  = "PgUp",
	[DOT_NL_ON]    = ".",      [DOT_NL_OFF]   = "Del",   [PLUS_NL_ON]  = "++",
	[PLUS_NL_OFF]  = "+",      [SLASH_NL_ON]  = "//",    [SLASH_NL_OFF] = "/",
	[STAR_NL_ON]   = "**",     [STAR_NL_OFF]  = "*",     [DASH_NL_ON]  = "--",
	[DASH_NL_OFF]  = "-",      [NUMLOCK_ON]   = "NL+",   [NUMLOCK_OFF]  = "NL-"
};

/* The comma-separated list of X11 Keysyms to send for each button. These
 * default values don't send multiple keystrokes, and act exactly like a real
 * numeric keypad. They can be changed via the config file though. Note that
 * since we translate these to Keycodes, which are interpreted relative to
 * server state like NumLock and CapsLock, they may not end up being processed
 * as the same Keysyms that were originally specified. */
/* These codes can be found in: /usr/include/X11/keysymdef.h */
static const char * button_keysyms[] =
{
	[ENTER]       = "KP_Enter",
	[ZERO_NL_ON]  = "KP_0",        [ZERO_NL_OFF]  = "KP_Insert",
	[ONE_NL_ON]   = "KP_1",        [ONE_NL_OFF]   = "KP_End",
	[TWO_NL_ON]   = "KP_2",        [TWO_NL_OFF]   = "KP_Down",
	[THREE_NL_ON] = "KP_3",        [THREE_NL_OFF] = "KP_Page_Down",
	[FOUR_NL_ON]  = "KP_4",        [FOUR_NL_OFF]  = "KP_Left",
	[FIVE_NL_ON]  = "KP_5",        [FIVE_NL_OFF]  = "KP_Begin",
	[SIX_NL_ON]   = "KP_6",        [SIX_NL_OFF]   = "KP_Right",
	[SEVEN_NL_ON] = "KP_7",        [SEVEN_NL_OFF] = "KP_Home",
	[EIGHT_NL_ON] = "KP_8",        [EIGHT_NL_OFF] = "KP_Up",
	[NINE_NL_ON]  = "KP_9",        [NINE_NL_OFF]  = "KP_Page_Up",
	[DOT_NL_ON]   = "KP_Decimal",  [DOT_NL_OFF]   = "KP_Delete",
	[PLUS_NL_ON]  = "KP_Add",      [PLUS_NL_OFF]  = "KP_Add",
	[SLASH_NL_ON] = "KP_Divide",   [SLASH_NL_OFF] = "KP_Divide",
	[STAR_NL_ON]  = "KP_Multiply", [STAR_NL_OFF]  = "KP_Multiply",
	[DASH_NL_ON]  = "KP_Subtract", [DASH_NL_OFF]  = "KP_Subtract",
	[NUMLOCK_ON]  = "Num_Lock",    [NUMLOCK_OFF]  = "Num_Lock"
};

static int verbose = 0;
static Display * display;

static void read_config(void)
{
	int count = 0;
	char line[MAXPATHLEN];
	snprintf(line, sizeof(line), "%s/.kpxrc", getenv("HOME"));
	FILE * config = fopen(line, "r");
	if(!config)
	{
		if(verbose)
			printf("Config file not found: %s\n", line);
		return;
	}
	while(fgets(line, sizeof(line), config))
	{
		char * keysyms = line;
		char * name = strsep(&keysyms, " ");
		while(keysyms && *keysyms == ' ')
			strsep(&keysyms, " ");
		count++;
		if(*name == '#')
			continue;
		if(keysyms)
		{
			enum button_code code;
			for(code = INVALID + 1; code < MAX_BUTTON; code++)
				if(!strcmp(name, button_names[code]))
					break;
			if(code < MAX_BUTTON)
			{
				char * eol = keysyms;
				strsep(&eol, "\r\n");
				if(verbose)
					printf("Keysyms for button %s set to: %s\n", name, keysyms);
				button_keysyms[code] = strdup(keysyms);
			}
			else
				fprintf(stderr, "Unknown button on line %d in config file: %s\n", count, name);
		}
		else
			fprintf(stderr, "Invalid format on line %d in config file.\n", count);
	}
	fclose(config);
}

static int open_serial(const char * dev)
{
	struct termios attr;
	int fd = open(dev, O_RDONLY);
	if(fd < 0)
	{
		perror(dev);
		return -1;
	}
	if(tcgetattr(fd, &attr) < 0)
	{
		perror(dev);
		close(fd);
		return -1;
	}
	cfmakeraw(&attr);
	attr.c_cflag |= CLOCAL;
	cfsetspeed(&attr, B1200);
	if(tcsetattr(fd, TCSANOW, &attr) < 0)
	{
		perror(dev);
		close(fd);
		return -1;
	}
	return fd;
}

static void send_key_events(char * keysym_names)
{
	int keycode;
	char * keysym_name = strsep(&keysym_names, ",");
	KeySym keysym = XStringToKeysym(keysym_name);
	if(keysym == NoSymbol)
	{
		fprintf(stderr, "No Keysym for: %s\n", keysym_name);
		/* Recurse anyway, to get later keys. */
		if(keysym_names)
			send_key_events(keysym_names);
		return;
	}
	if(verbose)
		printf("Keysym: 0x%04x (%s)\n", (unsigned) keysym, XKeysymToString(keysym));
	keycode = XKeysymToKeycode(display, keysym);
	if(verbose)
		printf("Keycode: 0x%04x\n", keycode);
	XTestFakeKeyEvent(display, keycode, True, CurrentTime);
	if(keysym_names)
		send_key_events(keysym_names);
	XTestFakeKeyEvent(display, keycode, False, CurrentTime);
}

int main(int argc, char * argv[])
{
	int fd;
	fd_set set;
	const char * dev = "/dev/ttyS0";
	if(argc > 1 && !strcmp(argv[1], "--default"))
	{
		/* Print the default config file and exit. */
		enum button_code code;
		int width = 0;
		for(code = INVALID + 1; code < MAX_BUTTON; code++)
		{
			int length = strlen(button_names[code]);
			if(length > width)
				width = length;
		}
		for(code = INVALID + 1; code < MAX_BUTTON; code++)
			printf("%-*s %s\n", width + 1, button_names[code], button_keysyms[code]);
		return 0;
	}
	/* Check for verbose mode. */
	if(argc > 1 && (!strcmp(argv[1], "-v") || !strcmp(argv[1], "--verbose")))
		verbose = 1;
	/* Initialize everything. */
	if(argc > 1 + verbose)
		dev = argv[1 + verbose];
	fd = open_serial(dev);
	if(fd < 0)
		return 1;
	read_config();
	display = XOpenDisplay(NULL);
	if(!display)
	{
		close(fd);
		return 2;
	}
	FD_ZERO(&set);
	/* Process bytes from the device. */
	for(;;)
	{
		FD_SET(fd, &set);
		if(select(fd + 1, &set, NULL, NULL, NULL) > 0)
		{
			/* We don't expect many at a time, so use a small buffer. */
			unsigned char data[16];
			ssize_t i, c = read(fd, data, sizeof(data));
			for(i = 0; i < c; i++)
			{
				char keysyms[64];
				enum button_code button = button_codes[data[i]];
				if(!button)
				{
					fprintf(stderr, "Unexpected data byte: 0x%02X\n", data[i]);
					continue;
				}
				if(verbose)
					printf("Button: %s\n", button_names[button]);
				/* We copy the string since we'll modify it while parsing. */
				snprintf(keysyms, sizeof(keysyms), "%s", button_keysyms[button]);
				send_key_events(keysyms);
				/* Make sure the server gets the key events right away. */
				XFlush(display);
			}
		}
		else if(errno != EINTR)
		{
			if(errno)
				perror("select()");
			break;
		}
	}
	XCloseDisplay(display);
	close(fd);
	return 0;
}
