/*
    Copyright (C) 2001 Paul Davis

    Parts based on source code taken from the "kbd" suite that is

    Copyright (C) 1993 Risto Kankunen
    
    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., 675 Mass Ave, Cambridge, MA 02139, USA.

    $Id: physical_keyboard.cc,v 1.15 2003/01/26 05:36:21 trutkin Exp $
*/

#include <stdlib.h>
#include <unistd.h>
#include <getopt.h>
#include <signal.h>
#include <fcntl.h>
#include <termios.h>
#include <linux/kd.h>
#include <linux/keyboard.h>
#include <linux/vt.h>
#include <sys/ioctl.h>
#include <errno.h>
#include <poll.h>
#include <string.h>

#include <pbd/atomic.h>
#include <pbd/error.h>
#include <pbd/failed_constructor.h>

#include "i18n.h"
#include "physical_keyboard.h"
#include "compat.h"

extern void die (void);

PhysicalKeyboard::PhysicalKeyboard ()

{
	if (manage_hardware()) {
		throw failed_constructor ();
	}

	keyboard_state = 0;
	isproxy = false;
}

PhysicalKeyboard::PhysicalKeyboard (int socketfd)
{
	if (manage_hardware()) {
		throw failed_constructor ();
	}

	if (unmanage_hardware()) {
		throw failed_constructor ();
	}

	fd = socketfd;
	keyboard_state = 0;
	isproxy = true;
}

PhysicalKeyboard::~PhysicalKeyboard () 
{
	unmanage_hardware ();
}

int
PhysicalKeyboard::manage_hardware ()

{
	struct termios settings;

	if ((fd = getfd()) < 0) {
		return -1;
	}

	if (get_mode ()) {
		return -1;
	}

	if (tcgetattr(fd, &old) == -1) {
		error << compose(_("tcgetattr failed (%1)"), strerror (errno)) << endmsg;
		return -1;
	}

	if (ioctl(fd, KDSKBMODE, K_MEDIUMRAW)) {
		error << _("Cannot set keyboard into \"medium-raw\" mode.\n If you are running under X, this may be because\n the X server has changed the access permissions on /dev/console") << endmsg;
		return -1;
	}

	if (tcgetattr(fd, &settings) == -1) {
		error << compose(_("tcgetattr failed (%1)"), strerror (errno)) << endmsg;
		return -1;
	}

	settings.c_lflag &= ~ (ICANON | ECHO | ISIG);
	settings.c_iflag = 0;
	settings.c_cc[VMIN] = 1;
	settings.c_cc[VTIME] = 0;

	if (tcsetattr(fd, TCSAFLUSH, &settings) == -1) {
		error << compose(_("tcsetattr failed (%1)"), strerror (errno)) << endmsg;
		return -1;
	}

	if (fcntl (fd, F_SETFL, fcntl (fd, F_GETFL) | O_NONBLOCK)) {
		error << _("cannot set nonblock on tty fd") << endmsg;
		return -1;
	}

	if (get_keybindings ()) {
		return -1;
	}

	return 0;
}

int
PhysicalKeyboard::unmanage_hardware ()
{
	if (!isproxy) {
		if (ioctl(fd, KDSKBMODE, oldkbmode)) {
			error << compose(_("cannot restore keyboard mode (%1)"), strerror (errno)) << endmsg;
			return -1;
		}
		
		if (tcsetattr(fd, 0, &old) == -1) {
			error << compose(_("tcsetattr failed (%1)"), strerror (errno)) << endmsg;
			return -1;
		}
	}

	close (fd);
	return 0;
}

bool
PhysicalKeyboard::is_a_console(int fd) 
{
    char arg;

    arg = 0;
    return (ioctl(fd, KDGKBTYPE, &arg) == 0 && ((arg == KB_101) || (arg == KB_84)));
}

int
PhysicalKeyboard::open_a_console(const char *fnam) 
{
	int fd;
	
	fd = open(fnam, O_RDONLY);
	
	if (fd < 0 && errno == EACCES) {
		fd = open (fnam, O_WRONLY);
	}

	if (fd < 0)
		return -1;

	if (!is_a_console(fd)) {
		close(fd);
		return -1;
	}

	return fd;
}

int 
PhysicalKeyboard::getfd() 
{
	int fd;
	
	fd = open_a_console("/dev/tty");
	if (fd >= 0)
		return fd;
	
	fd = open_a_console("/dev/tty0");
	if (fd >= 0)
		return fd;
	
	fd = open_a_console("/dev/console");
	if (fd >= 0)
		return fd;
	
	for (fd = 0; fd < 3; fd++)
		if (is_a_console(fd))
			return fd;
	
	error << _("Couldnt get a file descriptor referring to the console")	      << endmsg;
	
	return -1;
}


int
PhysicalKeyboard::get_mode(void) 
{
	if (ioctl(fd, KDGKBMODE, &oldkbmode)) {
		error << compose(_("KDGKBMODE failed (%1)"), strerror (errno)) << endmsg;
		return -1;
	}
	return 0;
}

int 
PhysicalKeyboard::get_keybindings ()
{
	int i, j;
	struct kbentry ke;

	/* check the plain keymap */

	ke.kb_index = 0;
	ke.kb_table = 0;

	j = ioctl(fd, KDGKBENT, (unsigned long)&ke);

	if (j && errno != EINVAL) {
		error << compose(_("KDGKBENT" "failed (%1)"), strerror (errno)) << endmsg;
		return -1;
	}
	if (j || ke.kb_value == K_NOSUCHMAP) {
		error << _("the plain keymap is undefined") << endmsg;
		return -1;
	}

	/* use the plain keymap */

	for (i = 0; i < NR_KEYS; i++) {
		ke.kb_index = i;
		ke.kb_table = 0;
		
		if (ioctl(fd, KDGKBENT, (unsigned long)&ke)) {
			error << _("cannot lookup keycode ") << i << endmsg;
		} else {
			keys[i] = ke.kb_value;
		}
	}

	return 0;
}
void
    PhysicalKeyboard::print_key(unsigned char key) {
	int binding;
	int keycode = key & 0x7f;
	bool press = !(key & (1<<7));

	binding = keys[keycode];
	cerr << "JHALL: ";
	if (press) {
		cerr << "key_pressed: ";
	} else {
		cerr << "key_released";
	}
	cerr << " Binding Type: ";
	switch (KTYP(binding)) {
		case KT_LETTER:
		cerr << "KT_LETTER ";
		break;
		case KT_LATIN:
		cerr << "KT_LATIN ";
		break;
		case KT_FN:
		cerr << "KT_FN";
		break;
		case KT_SPEC:
		cerr << "KT_SPEC ";
		break;
		case KT_PAD:
		cerr << "KT_PAD ";
		break;
		case KT_DEAD:
		cerr << "KT_DEAD ";
		break;
		case KT_CONS:
		cerr << "KT_CONS ";
		break;
		case KT_CUR:
		cerr << "KT_CUR ";
		break;
		case KT_SHIFT:
		cerr << "KT_SHIFT ";
		break;
		case KT_META:
		cerr << "KT_META ";
		break;
		case KT_ASCII:
		cerr << "KT_ASCII ";
		break;
		case KT_LOCK:
		cerr << "KT_LOCK ";
		break;
		case KT_SLOCK:
		cerr << "KT_SLOCK ";
		break;
		case 13: // CONFIG_SPEAKUP
		cerr << "KT_SPKUP ";
		break;
		default:
		cerr << "KT_UNKNOWN_KEYTYPE ";
	}
	if (keyboard_state & PhysicalKeyboard::Alt_L) {
		cerr << "Alt_L, ";
	}
	if (keyboard_state & PhysicalKeyboard::Alt_R) {
		cerr << "Alt_R, ";
	}
	if (keyboard_state & PhysicalKeyboard::Control_L) {
		cerr << "Control_L, ";
	}
	if (keyboard_state & PhysicalKeyboard::Control_R) {
		cerr << "Control_R, ";
	}
	if (keyboard_state & PhysicalKeyboard::Shift_L) {
		cerr << "Shift_L, ";
	}
	if (keyboard_state & PhysicalKeyboard::Shift_R) {
		cerr << "Shift_R, ";
	}
	cerr << "Key: " << KVAL(binding) << endl;
}

int
PhysicalKeyboard::process_key (unsigned char key)
{
	int binding;
	int keycode = key & 0x7f;
	bool press = !(key & (1<<7));
	int retcode = 0;

	binding = keys[keycode];
	EMIT_SIGNAL key_event (key);

	switch (KTYP(binding)) {
	case KT_SHIFT:
		switch (binding) {
		case K_SHIFT:
			if (press) {
				keyboard_state |= Shift_L;
			} else {
				keyboard_state &= ~Shift_L;
			}
			break;
		case K_CTRL:
			if (press) {
				keyboard_state |= Control_L;
			} else {
				keyboard_state &= ~Control_L;
			}
			break;
		case K_ALT:
			if (press) {
				keyboard_state |= Alt_L;
			} else {
				keyboard_state &= ~Alt_L;
			}
			break;
		case K_ALTGR:
			break;
		case K_SHIFTL:
			if (press) {
				keyboard_state |= Shift_L;
			} else {
				keyboard_state &= ~Shift_L;
			}
			break;
		case K_SHIFTR:
			if (press) {
				keyboard_state |= Shift_R;
			} else {
				keyboard_state &= ~Shift_R;
			}
			break;
		case K_CTRLL:
			if (press) {
				keyboard_state |= Control_L;
			} else {
				keyboard_state &= ~Control_L;
			}
			break;
		case K_CTRLR:
			if (press) {
				keyboard_state |= Control_R;
			} else {
				keyboard_state &= ~Control_R;
			}
			break;
		case K_CAPSSHIFT:
		case KT_META:
		case KT_ASCII:
		case KT_LOCK:
		case KT_SLOCK:
			break;
		}
		break;

	case KT_FN:
		switch (binding) {
		case K_F1:
			if (keyboard_state & PhysicalKeyboard::Alt) {
				switch_vt (1);
				return 0;
			}
			break;

		case K_F2:
			if (keyboard_state & PhysicalKeyboard::Alt) {
				switch_vt (2);
				return 0;
			}
			break;
		case K_F3:
			if (keyboard_state & PhysicalKeyboard::Alt) {
				switch_vt (3);
				return 0;
			}
			break;
		case K_F4:
			if (keyboard_state & PhysicalKeyboard::Alt) {
				switch_vt (4);
				return 0;
			}
			break;
		case K_F5:
			if (keyboard_state & PhysicalKeyboard::Alt) {
				switch_vt (5);
				return 0;
			}
			break;
		case K_F6:
			if (keyboard_state & PhysicalKeyboard::Alt) {
				switch_vt (6);
				return 0;
			}
			break;
		case K_F7:
			if (keyboard_state & PhysicalKeyboard::Alt) {
				switch_vt (7);
				return 0;
			}
			break;
		case K_F8:
			if (keyboard_state & PhysicalKeyboard::Alt) {
				switch_vt (8);
				return 0;
			}
			break;
		case K_F9:
			if (keyboard_state & PhysicalKeyboard::Alt) {
				switch_vt (9);
				return 0;
			}
			break;
		case K_F10:
			if (keyboard_state & PhysicalKeyboard::Alt) {
				switch_vt (10);
				return 0;
			}
			break;
		case K_F11:
			if (keyboard_state & PhysicalKeyboard::Alt) {
				switch_vt (11);
				return 0;
			}
			break;
		case K_F12:
			if (keyboard_state & PhysicalKeyboard::Alt) {
				switch_vt (12);
				return 0;
			}
			break;
    		}
		
		if (press) retcode = EMIT_SIGNAL key_press   (keyboard_state, binding);
		else       retcode = EMIT_SIGNAL key_release (keyboard_state, binding);
		break;

	case KT_LATIN:
	case KT_LETTER:
	case KT_SPEC:
		if (press) retcode = EMIT_SIGNAL key_press   (keyboard_state, KVAL(binding));
		else       retcode = EMIT_SIGNAL key_release (keyboard_state, KVAL(binding));
		break;

	case KT_PAD:
	case KT_DEAD:
	case KT_CONS:
		break;
	case KT_CUR:
		switch (binding) {
		case K_UP:
			if (press) retcode = EMIT_SIGNAL key_press (keyboard_state, K_UP);
			else       retcode = EMIT_SIGNAL key_release (keyboard_state, K_UP);
			break;
		case K_DOWN:
			if (press) retcode = EMIT_SIGNAL key_press (keyboard_state, K_DOWN);
			else       retcode = EMIT_SIGNAL key_release (keyboard_state, K_DOWN);
			break;
		case K_LEFT:
			if (press) retcode = EMIT_SIGNAL key_press (keyboard_state, K_LEFT);
			else       retcode = EMIT_SIGNAL key_release (keyboard_state, K_LEFT);
			break;
		case K_RIGHT:
			if (press) retcode = EMIT_SIGNAL key_press (keyboard_state, K_RIGHT);
			else       retcode = EMIT_SIGNAL key_release (keyboard_state, K_RIGHT);
			break;
		}
		break;

		
	/* 0x0d is KT_SPEAKUP */

	}

	return retcode;
}

int
PhysicalKeyboard::main ()
{
	struct pollfd pfd;
	int status = -1;

	while (1) {

		pfd.fd = fd;
		pfd.events = POLLIN | POLLERR | POLLHUP;

		if (poll (&pfd, 1, 1000) < 0) {
			if (errno == EINTR) {
				// this happens mostly when run
				// under gdb, or when exiting due to a signal
				continue;
			}

			error << compose(_("keyboard: poll call failed (%1)"), strerror (errno)) << endmsg;
			break;
		}
		
		if (pfd.revents & (POLLERR|POLLERR)) {
			error << _("keyboard: poll reports error.") << endmsg;
			break;
		}

		if (pfd.revents == 0) {
			// timed out, such as when the device is paused
			continue;
		}

		if ((status = handle_input ()) != 0) {
			break;
		}
	}

	return status;
}

int
PhysicalKeyboard::handle_input ()

{
	unsigned char buf[16];
	int i, n;

	while (1) {
		n = read (fd, buf, sizeof(buf));
		
		if (n == 0) {
			break;
		}
		
		if (n < 0) {
			if (errno != EAGAIN) {
				error << compose(_("keyboard: read error (%1)"), strerror (errno)) << endmsg;
				return -1;
			} else {
				break;
			}
		}
		
		for (i = 0; i < n; i++) {
			n = process_key (buf[i]);
			if (n < 0) {
				return -1;
			} else if (n > 0) {
				return 1;
			}
		}
	}

	return 0;
}	

void
PhysicalKeyboard::switch_vt (int which)

{
	int fd = getfd();

	if (ioctl (fd, VT_ACTIVATE, which)) {
		error << compose(_("cannot switch to VT %1"), which) << endmsg;
	} else {
		if (ioctl (fd, VT_WAITACTIVE, which)) {
			error << compose(_("cannot wait for VT %1 to become active"), which) << endmsg;
		}
	}

	close (fd);
}
