/*
    PIP - Platform Independent Primitives
    Keyboard grabber for console
	Copyright (C) 2016  Ivan Pelipenko peri4ko@gmail.com

    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 3 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, see <http://www.gnu.org/licenses/>.
*/

#include "pikbdlistener.h"
#ifndef WINDOWS
#  include <termios.h>
#else
#  include <wincon.h>
#endif


/** \class PIKbdListener
 * \brief Keyboard console input listener
 * \details This class provide listening of console keyboard input.
 * There is two ways to receive pressed key:
 * * external static function with format "void func(char key, void * data_)"
 * * event \a keyPressed()
 * 
 * Also there is static variable \a exiting which by default is set to 
 * \b false. If \a enableExitCapture() was called and listener was started
 * with function \a start(), this variable will be set to \b true if exit
 * key will be pressed. By default exit key is 'Q' = shift + 'q'.
 * To wait for this variable changes to \b true there is WAIT_FOR_EXIT macro
 * \snippet pikbdlistener.cpp main
 * */

bool PIKbdListener::exiting;
PIKbdListener * PIKbdListener::_object = 0;


#ifndef WINDOWS
struct EscSeq {
	const char * seq;
	int key;
	int mod;
	// 1 - shift
	// 2 - alt
	// 4 - ctrl
};
// unix
const EscSeq esc_seq[] = {
	{"[1A",  PIKbdListener::UpArrow,    0},
	{"[A",   PIKbdListener::UpArrow,    0},
	{"OA",   PIKbdListener::UpArrow,    4},
	{"[1B",  PIKbdListener::DownArrow,  0},
	{"[B",   PIKbdListener::DownArrow,  0},
	{"OB",   PIKbdListener::DownArrow,  4},
	{"[1C",  PIKbdListener::RightArrow, 0},
	{"[C",   PIKbdListener::RightArrow, 0},
	{"OC",   PIKbdListener::RightArrow, 4},
	{"[1D",  PIKbdListener::LeftArrow,  0},
	{"[D",   PIKbdListener::LeftArrow,  0},
	{"OD",   PIKbdListener::LeftArrow,  0},
	{"[H",   PIKbdListener::Home,       0},
	{"[1H",  PIKbdListener::Home,       0},
	{"[1~",  PIKbdListener::Home,       0},
	{"[F",   PIKbdListener::End,        0},
	{"[1F",  PIKbdListener::End,        0},
	{"[4~",  PIKbdListener::End,        0},
	{"[2~",  PIKbdListener::Insert,     0},
	{"[3~",  PIKbdListener::Delete,     0},
	{"[5~",  PIKbdListener::PageUp,     0},
	{"[6~",  PIKbdListener::PageDown,   0},
	{"[Z",   PIKbdListener::Tab,        1},
	{"OP",   PIKbdListener::F1,         0},
	{"[[A",  PIKbdListener::F1,         0},
	{"[11~", PIKbdListener::F1,         0},
	{"OQ",   PIKbdListener::F2,         0},
	{"[[B",  PIKbdListener::F2,         0},
	{"[12~", PIKbdListener::F2,         0},
	{"OR",   PIKbdListener::F3,         0},
	{"[[C",  PIKbdListener::F3,         0},
	{"[13~", PIKbdListener::F3,         0},
	{"[25~", PIKbdListener::F3,         1},
	{"OS",   PIKbdListener::F4,         0},
	{"[[D",  PIKbdListener::F4,         0},
	{"[14~", PIKbdListener::F4,         0},
	{"[26~", PIKbdListener::F4,         1},
	{"[[E",  PIKbdListener::F5,         0},
	{"OT",   PIKbdListener::F5,         0},
	{"[15~", PIKbdListener::F5,         0},
	{"[28~", PIKbdListener::F5,         1},
	{"OU",   PIKbdListener::F6,         0},
	{"[17~", PIKbdListener::F6,         0},
	{"[29~", PIKbdListener::F6,         1},
	{"OV",   PIKbdListener::F7,         0},
	{"[18~", PIKbdListener::F7,         0},
	{"[31~", PIKbdListener::F7,         1},
	{"OW",   PIKbdListener::F8,         0},
	{"[19~", PIKbdListener::F8,         0},
	{"[32~", PIKbdListener::F8,         1},
	{"OX",   PIKbdListener::F9,         0},
	{"[20~", PIKbdListener::F9,         0},
	{"[33~", PIKbdListener::F9,         1},
	{"OY",   PIKbdListener::F10,        0},
	{"[21~", PIKbdListener::F10,        0},
	{"[34~", PIKbdListener::F10,        1},
	{"OZ",   PIKbdListener::F11,        0},
	{"[23~", PIKbdListener::F11,        0},
	{"O[",   PIKbdListener::F12,        0},
	{"[24~", PIKbdListener::F12,        0},
	// End
	{0, 0, 0},
};
#endif


PRIVATE_DEFINITION_START(PIKbdListener)
#ifdef WINDOWS
	void * hIn;
	DWORD smode, tmode;
#else
	struct termios sterm, tterm;
#endif
PRIVATE_DEFINITION_END(PIKbdListener)


PIKbdListener::PIKbdListener(KBFunc slot, void * _data, bool startNow): PIThread() {
	setName("keyboard_listener");
	_object = this;
#ifdef WINDOWS
	PRIVATE->hIn = GetStdHandle(STD_INPUT_HANDLE);
	GetConsoleMode(PRIVATE->hIn, &PRIVATE->smode);
#else
	tcgetattr(0, &PRIVATE->sterm);
#endif
	is_active = true;
	ret_func = slot;
	data_ = _data;
	PIKbdListener::exiting = exit_enabled = false;
	if (startNow) start();
}


PIKbdListener::~PIKbdListener() {
	terminate();
	end();
}


void PIKbdListener::begin() {
#ifdef WINDOWS
	GetConsoleMode(PRIVATE->hIn, &PRIVATE->tmode);
	SetConsoleMode(PRIVATE->hIn, ENABLE_PROCESSED_INPUT);
#else
	struct termios term;
	tcgetattr(0, &term);
	term.c_lflag &= ~(ECHO | ICANON);
	term.c_lflag |= NOFLSH | IEXTEN;
	PRIVATE->tterm = term;
	tcsetattr(0, TCSANOW, &term);
#endif
}


void PIKbdListener::readKeyboard() {
	ke.key = 0;
	ke.modifiers = 0;
	memset(rc, 0, 8);
#ifdef WINDOWS
	INPUT_RECORD ir;
	ReadConsoleInput(PRIVATE->hIn, &ir, 1, &ret);
	if (ir.EventType == KEY_EVENT) {
		KEY_EVENT_RECORD ker = ir.Event.KeyEvent;
		if (ker.bKeyDown) {
			bool ctrl = ker.dwControlKeyState & (LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED);
			bool shift = ker.dwControlKeyState & SHIFT_PRESSED;
			bool alt = ker.dwControlKeyState & (LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED);
			if (ctrl) ke.modifiers |= Ctrl;
			if (shift) ke.modifiers |= Shift;
			if (alt) ke.modifiers |= Alt;
			//if (meta) ke.modifiers |= Meta;
			if (ker.dwControlKeyState & CAPSLOCK_ON) shift = !shift;
			//cout << "key " << int(ker.wVirtualKeyCode) << endl;
			switch (ker.wVirtualKeyCode) {
			case 8: ret = 1; ke.key = Backspace; break;
			case 33: ret = 1; ke.key = PageUp; break;
			case 34: ret = 1; ke.key = PageDown; break;
			case 35: ret = 1; ke.key = End; break;
			case 36: ret = 1; ke.key = Home; break;
			case 37: ret = 1; ke.key = LeftArrow; break;
			case 38: ret = 1; ke.key = UpArrow; break;
			case 39: ret = 1; ke.key = RightArrow; break;
			case 40: ret = 1; ke.key = DownArrow; break;
			case 45: ret = 1; ke.key = Insert; break;
			case 46: ret = 1; ke.key = Delete; break;
			case '\r':
			case '\n': ret = 1; ke.key = Return; break;
			case ' ': ret = 1; ke.key = Space; break;
			case '\t': ret = 1; ke.key = Tab; break;
			case 112: ret = 1; ke.key = F1; break;
			case 113: ret = 1; ke.key = F2; break;
			case 114: ret = 1; ke.key = F3; break;
			case 115: ret = 1; ke.key = F4; break;
			case 116: ret = 1; ke.key = F5; break;
			case 117: ret = 1; ke.key = F6; break;
			case 118: ret = 1; ke.key = F7; break;
			case 119: ret = 1; ke.key = F8; break;
			case 120: ret = 1; ke.key = F9; break;
			case 121: ret = 1; ke.key = F10; break;
			case 122: ret = 1; ke.key = F11; break;
			case 123: ret = 1; ke.key = F12; break;
			default:
				ret = 1;
				rc[0] = 1;
				{
				PIChar ch = PIChar::fromConsole(ker.uChar.AsciiChar);
				if (shift) ch = ch.toUpper();
				ke.key = ch.unicode16Code();
				}
				break;
			}
			if (ke.key == 0) {piMSleep(10); return;}
		} else {piMSleep(10); return;}
	} else {piMSleep(10); return;}
	/*if (lc == 0) {
		ReadConsole(hIn, &rc, 1, &ret, 0);
		//cout << "read console" << endl;
		lc = char(rc);
	}*/
	/*if (ret < 0 || ret > 3) return;
	lc = char(((uchar * )&rc)[ret - 1]);
	for (int i = 0; i < ret; ++i)
		cout << std::hex << int(((uchar * )&rc)[i]) << ' ';
	cout << endl << std::hex << rc << endl;*/
#else
	tcsetattr(0, TCSANOW, &PRIVATE->tterm);
	ret = read(0, rc, 8);
	/*piCout << NewLine << "read" << ret;
	for (int i = 0; i < ret; ++i)
		cout << std::hex << int(((uchar * )&rc)[i]) << ' ';
	cout << endl;
	for (int i = 0; i < ret; ++i)
		cout << "'" << (char)(rc[i]) << "' ";
	cout << endl;*/
	if (rc[0] == 0) {piMSleep(10); return;}
	if (ret < 0 || ret > 7) {piMSleep(10); return;}
	if (ret == 1) ke.key = PIChar::fromConsole(rc[0]).unicode16Code();
	int mod(0);
	// 2 - shift			1
	// 3 - alt				2
	// 4 - alt+shift		3
	// 5 - ctrl				4
	// 8 - ctrl+alt+shift	7
	if (rc[0] == '\e') { // search for Alt
		if (ret == 2) {
			mod = 2;
			ke.key = PIChar::fromConsole(rc[1]).unicode16Code();
		} else {// escape-seq
			if (rc[1] == '\e') { // search for Alt
				for (int i = 1; i < 7; ++i) rc[i] = rc[i + 1];
				mod = 2;
				ret--;
			}
			if (rc[1] == '[') {
				for (int i = 2; i < 7; ++i) // search for modifier
					if (rc[i] == ';') {
						mod |= rc[i + 1] - '0' - 1;
						for (int j = i; j < 6; ++j) rc[j] = rc[j + 2];
						rc[6] = rc[7] = 0;
						ret -= 2;
						break;
					}
			}
			if (ret >= 3 && rc[1] == 'O') { // search for modifier (F1-F4)
				if (rc[2] >= '1' && rc[2] <= '8') {
					mod |= rc[2] - '0' - 1;
					for (int j = 2; j < 6; ++j) rc[j] = rc[j + 1];
					rc[7] = 0;
					ret--;
				}
			}
			for (int i = 0; ; ++i) {
				if (!esc_seq[i].seq) break;
				if (strcmp(esc_seq[i].seq, &(rc[1])) == 0) {
					ke.key = esc_seq[i].key;
					mod |= esc_seq[i].mod;
					break;
				}
			}
		}
		if (mod >= 0 && mod <= 15) {
			if (mod & 0x1) ke.modifiers |= Shift;
			if (mod & 0x2) ke.modifiers |= Alt;
			if (mod & 0x4) ke.modifiers |= Ctrl;
			//if (mod & 0x8) ke.modifiers |= Meta;
		}
	/*cout << "wo mods (" << mod << ")\n";
	for (int i = 0; i < ret; ++i)
		cout << "'" << (char)(rc[i]) << "' ";
	cout << endl;*/
	}
	if (ke.key == 0 && ret > 1)
		ke.key = PIChar(rc).unicode16Code();
#endif
	if ((rc[0] == '\n' || rc[0] == '\r') && ret == 1)
		ke.key = Return;
	if (exit_enabled && ke.key == exit_key) {
		PIKbdListener::exiting = true;
		return;
	}
	if (ret > 0) {
		keyPressed(ke, data_);
		if (ret_func != 0) ret_func(ke, data_);
	}
}


void PIKbdListener::end() {
	//cout << "list end" << endl;
#ifdef WINDOWS
	SetConsoleMode(PRIVATE->hIn, PRIVATE->smode);
#else
	tcsetattr(0, TCSANOW, &PRIVATE->sterm);
#endif
}


void PIKbdListener::setActive(bool yes) {
	is_active = yes;
	if (is_active) {
#ifdef WINDOWS
		SetConsoleMode(PRIVATE->hIn, PRIVATE->tmode);
#else
		tcsetattr(0, TCSANOW, &PRIVATE->tterm);
#endif
	} else {
#ifdef WINDOWS
		SetConsoleMode(PRIVATE->hIn, PRIVATE->smode);
#else
		tcsetattr(0, TCSANOW, &PRIVATE->sterm);
#endif
	}
}
