/*
Copyright 2011-2013 Timothy Pearson <kb9vqf@pearsoncomputing.net>

This file is part of tdekbdledsync, the TDE Keyboard LED Synchronization Daemon

tdekbdledsync 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 3
of the License, or (at your option) any later version.

tdekbdledsync 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 tdekbdledsync. If not, see http://www.gnu.org/licenses/.

*/

// The idea here is to periodically read the Xorg core keyboard state, and then forcibly set the physical LED states on all attached keyboards to match (via the event interface)
// Once every half second should work well enough on most systems

#include <stdio.h>
#include <stdlib.h>
#include <exception>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <dirent.h>
#include <linux/vt.h>
#include <linux/input.h>
#include <linux/uinput.h>
#include <sys/file.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/select.h>
#include <sys/time.h>
#include <termios.h>
#include <signal.h>
#include <stdint.h>
extern "C" {
#include <libudev.h>
#include "getfd.h"
}
#include <libgen.h>

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

using namespace std;

// WARNING
// MAX_KEYBOARDS must be greater than or equal to MAX_INPUT_NODE
#define MAX_KEYBOARDS 128
#define MAX_INPUT_NODE 128

#define TestBit(bit, array) (array[(bit) / 8] & (1 << ((bit) % 8)))

typedef unsigned char byte;

char filename[32];
char key_bitmask[(KEY_MAX + 7) / 8];

int keyboard_fd_num;
int keyboard_fds[MAX_KEYBOARDS];

Display* display = NULL;

int lockfd = -1;
char lockFileName[256];

// --------------------------------------------------------------------------------------
// Useful function from Stack Overflow
// http://stackoverflow.com/questions/874134/find-if-string-endswith-another-string-in-c
// --------------------------------------------------------------------------------------
/*  returns 1 iff str ends with suffix  */
int str_ends_with(const char * str, const char * suffix) {

  if( str == NULL || suffix == NULL )
    return 0;

  size_t str_len = strlen(str);
  size_t suffix_len = strlen(suffix);

  if(suffix_len > str_len)
    return 0;

  return 0 == strncmp( str + str_len - suffix_len, suffix, suffix_len );
}
// --------------------------------------------------------------------------------------

// --------------------------------------------------------------------------------------
// Useful function from Stack Overflow
// http://stackoverflow.com/questions/1599459/optimal-lock-file-method
// --------------------------------------------------------------------------------------
int tryGetLock(char const *lockName) {
	mode_t m = umask( 0 );
	int fd = open( lockName, O_RDWR|O_CREAT, 0666 );
	umask( m );
	if( fd >= 0 && flock( fd, LOCK_EX | LOCK_NB ) < 0 ) {
		close( fd );
		fd = -1;
	}
	return fd;
}

// --------------------------------------------------------------------------------------
// Useful function from Stack Overflow
// http://stackoverflow.com/questions/1599459/optimal-lock-file-method
// --------------------------------------------------------------------------------------
void releaseLock(int fd, char const *lockName) {
	if( fd < 0 ) {
		return;
	}
	remove( lockName );
	close( fd );
}


// --------------------------------------------------------------------------------------
// Get the VT number X is running on
// (code taken from GDM, daemon/getvt.c, GPLv2+)
// --------------------------------------------------------------------------------------
int get_x_vtnum(Display *dpy)
{
	Atom prop;
	Atom actualtype;
	int actualformat;
	unsigned long nitems;
	unsigned long bytes_after;
	unsigned char *buf;
	int num;
	
	prop = XInternAtom (dpy, "XFree86_VT", False);
	if (prop == None)
	return -1;
	
	if (XGetWindowProperty (dpy, DefaultRootWindow (dpy), prop, 0, 1,
				False, AnyPropertyType, &actualtype, &actualformat,
				&nitems, &bytes_after, &buf)) {
		return -1;
	}
	
	if (nitems != 1) {
		XFree (buf);
		return -1;
	}
	
	switch (actualtype) {
		case XA_CARDINAL:
		case XA_INTEGER:
		case XA_WINDOW:
		switch (actualformat) {
			case 8:
				num = (*(uint8_t  *)(void *)buf);
			break;
			case 16:
				num = (*(uint16_t *)(void *)buf);
			break;
			case 32:
				num = (*(uint32_t *)(void *)buf);
			break;
			default:
				XFree (buf);
				return -1;
			}
		break;
		default:
		XFree (buf);
		return -1;
	}
	
	XFree (buf);
	
	return num;
}
// --------------------------------------------------------------------------------------

// --------------------------------------------------------------------------------------
// Get the specified xkb mask modifier
// (code taken from numlockx)
// --------------------------------------------------------------------------------------
unsigned int xkb_mask_modifier(XkbDescPtr xkb, const char *name) {
	int i;
	if( !xkb || !xkb->names ) {
		return 0;
	}
	for( i = 0; i < XkbNumVirtualMods; i++ ) {
		char* modStr = XGetAtomName( xkb->dpy, xkb->names->vmods[i] );
		if( modStr == NULL ) {
			continue;
		}
		if( strcmp(name, modStr) == 0 ) {
			unsigned int mask;
			XkbVirtualModsToReal( xkb, 1 << i, &mask );
			XFree(modStr);
			return mask;
		}
		XFree(modStr);
	}
	return 0;
}
// --------------------------------------------------------------------------------------

// --------------------------------------------------------------------------------------
// Get the capslock xkb mask modifier
// --------------------------------------------------------------------------------------
unsigned int xkb_capslock_mask() {
	return LockMask;
}
// --------------------------------------------------------------------------------------

// --------------------------------------------------------------------------------------
// Get the numlock xkb mask modifier
// (code taken from numlockx)
// --------------------------------------------------------------------------------------
unsigned int xkb_numlock_mask() {
	XkbDescPtr xkb;
	if(( xkb = XkbGetKeyboard(display, XkbAllComponentsMask, XkbUseCoreKbd )) != NULL ) {
		unsigned int mask = xkb_mask_modifier( xkb, "NumLock" );
		XkbFreeKeyboard( xkb, 0, True );
		return mask;
	}
	return 0;
}
// --------------------------------------------------------------------------------------

// --------------------------------------------------------------------------------------
// Get the scroll lock xkb mask modifier
// (code taken from numlockx and modified)
// --------------------------------------------------------------------------------------
unsigned int xkb_scrolllock_mask() {
	XkbDescPtr xkb;
	if(( xkb = XkbGetKeyboard(display, XkbAllComponentsMask, XkbUseCoreKbd )) != NULL ) {
		unsigned int mask = xkb_mask_modifier( xkb, "ScrollLock" );
		XkbFreeKeyboard( xkb, 0, True );
		return mask;
	}
	return 0;
}
// --------------------------------------------------------------------------------------


int find_keyboards() {
	int i, j;
	int fd;
	char name[256] = "Unknown";

	keyboard_fd_num = 0;
	for (i=0; i<MAX_KEYBOARDS; i++) {
		keyboard_fds[i] = 0;
	}

	for (i=0; i<MAX_INPUT_NODE; i++) {
		snprintf(filename, sizeof(filename), "/dev/input/event%d", i);

		fd = open(filename, O_RDWR|O_SYNC);
		if (fd >= 0) {
			ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(key_bitmask)), key_bitmask);

			// Ensure that we do not detect tsak faked keyboards
			ioctl (fd, EVIOCGNAME(sizeof(name)), name);
			if (str_ends_with(name, "+tsak") == 0) {
				struct input_id input_info;
				ioctl (fd, EVIOCGID, &input_info);
				if ((input_info.vendor != 0) && (input_info.product != 0)) {
					/* We assume that anything that has an alphabetic key in the
						QWERTYUIOP range in it is the main keyboard. */
					for (j = KEY_Q; j <= KEY_P; j++) {
						if (TestBit(j, key_bitmask)) {
							keyboard_fds[keyboard_fd_num] = fd;
						}
					}
				}
			}
	
			if (keyboard_fds[keyboard_fd_num] == 0) {
				close(fd);
			}
			else {
				keyboard_fd_num++;
			}
		}
	}
	return 0;
}

void handle_sigterm(int signum) {
	if (lockfd >= 0) {
		releaseLock(lockfd, lockFileName);
	}
	exit(0);
}

int main() {
	int current_keyboard;
	char name[256] = "Unknown";
	unsigned int states;
	struct input_event ev;
	struct vt_stat vtstat;
	int vt_fd;
	int x11_vt_num = -1;
//	XEvent xev;
	XkbStateRec state;

	bool num_lock_set = false;
	bool caps_lock_set = false;
	bool scroll_lock_set = false;

	int num_lock_mask;
	int caps_lock_mask;
	int scroll_lock_mask;

	int evBase;
	int errBase;

	// Register cleanup handlers
	struct sigaction action;
	memset(&action, 0, sizeof(struct sigaction));
	action.sa_handler = handle_sigterm;
	sigaction(SIGTERM, &action, NULL);

	// Open X11 display
	display = XOpenDisplay(NULL);
	if (!display) {
		printf ("[tdekbdledsync] Unable to open X11 display!\n");
		return -1;
	}

	// Ensure only one process is running on a given display
	sprintf(lockFileName, "/var/lock/tdekbdledsync-%s.lock", XDisplayString(display));
	lockfd = tryGetLock(lockFileName);
	if (lockfd < 0) {
		printf ("[tdekbdledsync] Another instance of this program is already running on this X11 display!\n[tdekbdledsync] Lockfile detected at '%s'\n", lockFileName);
		return -2;
	}

	// Set up Xkb extension
	int i1, mn, mj;
	mj = XkbMajorVersion;
	mn = XkbMinorVersion;
	if (!XkbQueryExtension(display, &i1, &evBase, &errBase, &mj, &mn)) {
		printf("[tdekbdledsync] Server doesn't support a compatible XKB\n");
		releaseLock(lockfd, lockFileName);
		return -3;
	}
	XkbSelectEvents(display, XkbUseCoreKbd, XkbStateNotifyMask, XkbStateNotifyMask);

	// Get X server VT number
	x11_vt_num = get_x_vtnum(display);

	// Open console socket
	vt_fd = getfd(NULL);

	// Monitor for hotplugged keyboards
	struct udev *udev;
	struct udev_device *dev;
	struct udev_monitor *mon;
	struct timeval tv;

	// Create the udev object
	udev = udev_new();
	if (!udev) {
		printf("[tdekbdledsync] Cannot connect to udev interface\n");
		releaseLock(lockfd, lockFileName);
		return -4;
	}

	// Set up a udev monitor to monitor input devices
	mon = udev_monitor_new_from_netlink(udev, "udev");
	udev_monitor_filter_add_match_subsystem_devtype(mon, "input", NULL);
	udev_monitor_enable_receiving(mon);

	while (1) {
		// Get masks
		num_lock_mask = xkb_numlock_mask();
		caps_lock_mask = xkb_capslock_mask();
		scroll_lock_mask = xkb_scrolllock_mask();

		// Find keyboards
		find_keyboards();
		if (keyboard_fd_num == 0) {
			printf ("[tdekbdledsync] Could not find any usable keyboard(s)!\n");
			releaseLock(lockfd, lockFileName);
			return -5;
		}
		else {
			fprintf(stderr, "[tdekbdledsync] Found %d keyboard(s)\n", keyboard_fd_num);

			for (current_keyboard=0;current_keyboard<keyboard_fd_num;current_keyboard++) {
				// Print device name
				ioctl(keyboard_fds[current_keyboard], EVIOCGNAME (sizeof (name)), name);
				fprintf(stderr, "[tdekbdledsync] Syncing keyboard: (%s)\n", name);
			}

			while (1) {
				// Get current active VT
				if (ioctl(vt_fd, VT_GETSTATE, &vtstat)) {
					fprintf(stderr, "[tdekbdledsync] Unable to get current VT!\n");
					releaseLock(lockfd, lockFileName);
					return -6;
				}

				if (x11_vt_num == vtstat.v_active) {
					// Get Virtual Core keyboard status
					if (XkbGetState(display, XkbUseCoreKbd, &state) != Success) {
						fprintf(stderr, "[tdekbdledsync] Unable to query X11 Virtual Core keyboard!\n");
						releaseLock(lockfd, lockFileName);
						return -7;
					}

					caps_lock_set = (state.mods & caps_lock_mask);
					num_lock_set = (state.mods & num_lock_mask);
					scroll_lock_set = (state.mods & scroll_lock_mask);

					for (current_keyboard=0;current_keyboard<keyboard_fd_num;current_keyboard++) {
						// Set LEDs
						ev.type = EV_LED;
						ev.code = LED_CAPSL;
						ev.value = caps_lock_set;
						if (write(keyboard_fds[current_keyboard], &ev, sizeof(ev)) < 0) {
							fprintf(stderr, "[tdekbdledsync] Unable to set LED state\n");
						}

						ev.type = EV_LED;
						ev.code = LED_NUML;
						ev.value = num_lock_set;
						if (write(keyboard_fds[current_keyboard], &ev, sizeof(ev)) < 0) {
							fprintf(stderr, "[tdekbdledsync] Unable to set LED state\n");
						}

						ev.type = EV_LED;
						ev.code = LED_SCROLLL;
						ev.value = scroll_lock_set;
						if (write(keyboard_fds[current_keyboard], &ev, sizeof(ev)) < 0) {
							fprintf(stderr, "[tdekbdledsync] Unable to set LED state\n");
						}
					}
				}
				else {
					// Ensure the X server is still alive
					// If the X server has terminated, this will fail and the program will terminate
					XSync(display, False);
				}

				// Check the hotplug monitoring process to see if any keyboards were added or removed
				fd_set readfds;
				FD_ZERO(&readfds);
				FD_SET(udev_monitor_get_fd(mon), &readfds);
				tv.tv_sec = 0;
				tv.tv_usec = 0;
				int fdcount = select(udev_monitor_get_fd(mon)+1, &readfds, NULL, NULL, &tv);
				if (fdcount < 0) {
					if (errno == EINTR) {
						fprintf(stderr, "[tdekbdledsync] Signal caught in hotplug monitoring process; ignoring\n");
					}
					else {
						fprintf(stderr, "[tdekbdledsync] Select failed on udev file descriptor in hotplug monitoring process\n");
					}
				}
				else {
					dev = udev_monitor_receive_device(mon);
					if (dev) {
						int reload_keyboards = 0;
						if (strcmp(udev_device_get_action(dev), "add") == 0) {
							reload_keyboards = 1;
						}
						if (strcmp(udev_device_get_action(dev), "remove") == 0) {
							reload_keyboards = 1;
						}
						udev_device_unref(dev);
						if( reload_keyboards ) {
							// Reload keyboards
							break;
						}
					}
				}

				// Poll
				usleep(250*1000);

// 				// Wait for an Xkb event
// 				// FIXME
// 				// This prevents the udev hotplug monitor from working, as XNextEvent does not stop blocking when a keyboard hotplug occurs
// 				while (1) {
// 					XNextEvent(display, &xev);
// 					if (xev.type == evBase + XkbEventCode) {
// 						XkbEvent *xkb_event = reinterpret_cast<XkbEvent*>(&xev);
// 						if (xkb_event->any.xkb_type & XkbStateNotify) {
// 							if ((xkb_event->state.changed & XkbModifierStateMask) || (xkb_event->state.changed & XkbModifierBaseMask)) {
// 								// Modifier state has changed
// 								// Synchronize keyboard indicators
// 								break;
// 							}
// 						}
// 					}
// 				}
			}
		}

		// Close all keyboard file descriptors
		for (int current_keyboard=0;current_keyboard<keyboard_fd_num;current_keyboard++) {
			close(keyboard_fds[current_keyboard]);
		}
	}

	releaseLock(lockfd, lockFileName);
	udev_monitor_unref(mon);
	udev_unref(udev);
	return 0;
}
