/*
    PIP - Platform Independent Primitives
    Keyboard grabber for console
    Ivan Pelipenko peri4ko@yandex.ru

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Lesser 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 Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
#include "pikbdlistener.h"

#include "piincludes_p.h"
#include "piliterals.h"
#include "piwaitevent_p.h"
// clang-format off
#ifndef WINDOWS
#  include <termios.h>
#else
#  include <wingdi.h>
#  include <wincon.h>
#endif
// clang-format on

/** \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
// unix
const PIKbdListener::EscSeq PIKbdListener::esc_seq[] = {
	{"OA",   PIKbdListener::UpArrow,    0, 0,        1},
	{"OA",   PIKbdListener::UpArrow,    4, 0,        0},
	{"[1A",  PIKbdListener::UpArrow,    0, 0,        0},
	{"[A",   PIKbdListener::UpArrow,    0, vt_all,   0},
	{"OB",   PIKbdListener::DownArrow,  0, 0,        1},
	{"OB",   PIKbdListener::DownArrow,  4, 0,        0},
	{"[1B",  PIKbdListener::DownArrow,  0, 0,        0},
	{"[B",   PIKbdListener::DownArrow,  0, vt_all,   0},
	{"OC",   PIKbdListener::RightArrow, 0, 0,        1},
	{"OC",   PIKbdListener::RightArrow, 4, 0,        0},
	{"[1C",  PIKbdListener::RightArrow, 0, 0,        0},
	{"[C",   PIKbdListener::RightArrow, 0, vt_all,   0},
	{"OD",   PIKbdListener::LeftArrow,  0, 0,        1},
	{"OD",   PIKbdListener::LeftArrow,  4, 0,        0},
	{"[1D",  PIKbdListener::LeftArrow,  0, 0,        0},
	{"[D",   PIKbdListener::LeftArrow,  0, vt_all,   0},
	{"[H",   PIKbdListener::Home,       0, vt_xterm, 0},
	{"[1H",  PIKbdListener::Home,       0, 0,        0},
	{"OH",   PIKbdListener::Home,       0, 0,        1},
	{"[1~",  PIKbdListener::Home,       0, vt_linux, 0},
	{"[F",   PIKbdListener::End,        0, vt_xterm, 0},
	{"[1F",  PIKbdListener::End,        0, 0,        0},
	{"OF",   PIKbdListener::End,        0, 0,        1},
	{"[4~",  PIKbdListener::End,        0, vt_linux, 0},
	{"[2~",  PIKbdListener::Insert,     0, vt_all,   0},
	{"[3~",  PIKbdListener::Delete,     0, vt_all,   0},
	{"[5~",  PIKbdListener::PageUp,     0, vt_all,   0},
	{"[6~",  PIKbdListener::PageDown,   0, vt_all,   0},
	{"[Z",   PIKbdListener::Tab,        1, vt_xterm, 0},
	{"OP",   PIKbdListener::F1,         0, vt_xterm, 0},
	{"[[A",  PIKbdListener::F1,         0, vt_linux, 0},
	{"[11~", PIKbdListener::F1,         0, 0,        0},
	{"[25~", PIKbdListener::F1,         1, vt_linux, 0},
	{"OQ",   PIKbdListener::F2,         0, vt_xterm, 0},
	{"[[B",  PIKbdListener::F2,         0, vt_linux, 0},
	{"[12~", PIKbdListener::F2,         0, 0,        0},
	{"[26~", PIKbdListener::F2,         1, vt_linux, 0},
	{"OR",   PIKbdListener::F3,         0, vt_xterm, 0},
	{"[[C",  PIKbdListener::F3,         0, vt_linux, 0},
	{"[13~", PIKbdListener::F3,         0, 0,        0},
	{"[28~", PIKbdListener::F3,         1, vt_linux, 0},
	{"OS",   PIKbdListener::F4,         0, vt_xterm, 0},
	{"[[D",  PIKbdListener::F4,         0, vt_linux, 0},
	{"[14~", PIKbdListener::F4,         0, 0,        0},
	{"[29~", PIKbdListener::F4,         1, vt_linux, 0},
	{"[[E",  PIKbdListener::F5,         0, vt_linux, 0},
	{"OT",   PIKbdListener::F5,         0, 0,        0},
	{"[15~", PIKbdListener::F5,         0, vt_xterm, 0},
	{"[31~", PIKbdListener::F5,         1, vt_linux, 0},
	{"OU",   PIKbdListener::F6,         0, 0,        0},
	{"[17~", PIKbdListener::F6,         0, vt_all,   0},
	{"[32~", PIKbdListener::F6,         1, vt_linux, 0},
	{"OV",   PIKbdListener::F7,         0, 0,        0},
	{"[18~", PIKbdListener::F7,         0, vt_all,   0},
	{"[33~", PIKbdListener::F7,         1, vt_linux, 0},
	{"OW",   PIKbdListener::F8,         0, 0,        0},
	{"[19~", PIKbdListener::F8,         0, vt_all,   0},
	{"[34~", PIKbdListener::F8,         1, vt_linux, 0},
	{"OX",   PIKbdListener::F9,         0, 0,        0},
	{"[20~", PIKbdListener::F9,         0, vt_all,   0},
	{"[35~", PIKbdListener::F9,         1, vt_linux, 0},
	{"OY",   PIKbdListener::F10,        0, 0,        0},
	{"[21~", PIKbdListener::F10,        0, vt_all,   0},
	{"[36~", PIKbdListener::F10,        1, vt_linux, 0},
	{"OZ",   PIKbdListener::F11,        0, 0,        0},
	{"[23~", PIKbdListener::F11,        0, vt_all,   0},
	{"O[",   PIKbdListener::F12,        0, 0,        0},
	{"[24~", PIKbdListener::F12,        0, vt_all,   0},
 // End
	{0,      0,						 0, 0,        0},
};
void setupTerminal(bool on) {
	printf("\e[?1000");
	printf(on ? "h" : "l");
	printf("\e[?1002");
	printf(on ? "h" : "l");
	fflush(0);
}
#endif


PRIVATE_DEFINITION_START(PIKbdListener)
#ifdef WINDOWS
	void *hIn, *hOut;
	DWORD smode, tmode;
	CONSOLE_SCREEN_BUFFER_INFO sbi;
#else
	struct termios sterm, tterm;
#endif
#ifdef WINDOWS
	DWORD
#else
	int
#endif
	ret;
	PIWaitEvent event;
PRIVATE_DEFINITION_END(PIKbdListener)


PIKbdListener::PIKbdListener(KBFunc slot, void * _d, bool startNow): PIThread() {
	setName("keyboard_listener"_a);
	_object = this;
#ifdef WINDOWS
	PRIVATE->hIn  = GetStdHandle(STD_INPUT_HANDLE);
	PRIVATE->hOut = GetStdHandle(STD_OUTPUT_HANDLE);
	GetConsoleMode(PRIVATE->hIn, &PRIVATE->smode);
#else
	tcgetattr(0, &PRIVATE->sterm);
#endif
	ret_func     = slot;
	kbddata_     = _d;
	dbl_interval = 400;
	PRIVATE->event.create();
	PIKbdListener::exiting = false;
	if (startNow) start();
}


PIKbdListener::~PIKbdListener() {
	stop();
	if (!waitForFinish(100_ms)) terminate();
	PRIVATE->event.destroy();
}


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


#ifdef WINDOWS
PIKbdListener::KeyModifiers getModifiers(DWORD v, bool * shift = 0) {
	PIKbdListener::KeyModifiers ret;
	bool ctrl = v & (LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED);
	bool shi  = v & SHIFT_PRESSED;
	bool alt  = v & (LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED);
	if (ctrl) ret |= PIKbdListener::Ctrl;
	if (shi) ret |= PIKbdListener::Shift;
	if (alt) ret |= PIKbdListener::Alt;
	// if (meta) ret |= PIKbdListener::Meta;
	if (v & CAPSLOCK_ON) shi = !shi;
	if (shift) *shift = shi;
	return ret;
}
PIKbdListener::MouseButtons getButtons(DWORD v) {
	PIKbdListener::MouseButtons ret;
	if (v & FROM_LEFT_1ST_BUTTON_PRESSED) ret |= PIKbdListener::MouseLeft;
	if (v & RIGHTMOST_BUTTON_PRESSED) ret |= PIKbdListener::MouseRight;
	if (v & FROM_LEFT_2ND_BUTTON_PRESSED) ret |= PIKbdListener::MouseMiddle;
	return ret;
}
#endif


void PIKbdListener::readKeyboard() {
	ke.key       = 0;
	ke.modifiers = 0;
	char rc[8];
	piZeroMemory(rc, 8);
#ifdef WINDOWS
	INPUT_RECORD ir;
	ReadConsoleInput(PRIVATE->hIn, &ir, 1, &(PRIVATE->ret));
	switch (ir.EventType) {
	case KEY_EVENT: {
		KEY_EVENT_RECORD ker = ir.Event.KeyEvent;
		if (ker.bKeyDown) {
			bool shift   = false;
			ke.modifiers = getModifiers(ker.dwControlKeyState, &shift);
			// piCout << "key" << int(ker.wVirtualKeyCode) << int(ker.uChar.AsciiChar);
			switch (ker.wVirtualKeyCode) {
			case 8:
				PRIVATE->ret = 1;
				ke.key       = Backspace;
				break;
			case 33:
				PRIVATE->ret = 1;
				ke.key       = PageUp;
				break;
			case 34:
				PRIVATE->ret = 1;
				ke.key       = PageDown;
				break;
			case 35:
				PRIVATE->ret = 1;
				ke.key       = End;
				break;
			case 36:
				PRIVATE->ret = 1;
				ke.key       = Home;
				break;
			case 37:
				PRIVATE->ret = 1;
				ke.key       = LeftArrow;
				break;
			case 38:
				PRIVATE->ret = 1;
				ke.key       = UpArrow;
				break;
			case 39:
				PRIVATE->ret = 1;
				ke.key       = RightArrow;
				break;
			case 40:
				PRIVATE->ret = 1;
				ke.key       = DownArrow;
				break;
			case 45:
				PRIVATE->ret = 1;
				ke.key       = Insert;
				break;
			case 46:
				PRIVATE->ret = 1;
				ke.key       = Delete;
				break;
			case '\r':
			case '\n':
				PRIVATE->ret = 1;
				ke.key       = Return;
				break;
			case ' ':
				PRIVATE->ret = 1;
				ke.key       = Space;
				break;
			case '\t':
				PRIVATE->ret = 1;
				ke.key       = Tab;
				break;
			case 112:
				PRIVATE->ret = 1;
				ke.key       = F1;
				break;
			case 113:
				PRIVATE->ret = 1;
				ke.key       = F2;
				break;
			case 114:
				PRIVATE->ret = 1;
				ke.key       = F3;
				break;
			case 115:
				PRIVATE->ret = 1;
				ke.key       = F4;
				break;
			case 116:
				PRIVATE->ret = 1;
				ke.key       = F5;
				break;
			case 117:
				PRIVATE->ret = 1;
				ke.key       = F6;
				break;
			case 118:
				PRIVATE->ret = 1;
				ke.key       = F7;
				break;
			case 119:
				PRIVATE->ret = 1;
				ke.key       = F8;
				break;
			case 120:
				PRIVATE->ret = 1;
				ke.key       = F9;
				break;
			case 121:
				PRIVATE->ret = 1;
				ke.key       = F10;
				break;
			case 122:
				PRIVATE->ret = 1;
				ke.key       = F11;
				break;
			case 123:
				PRIVATE->ret = 1;
				ke.key       = F12;
				break;
			default:
				PRIVATE->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;
		}
	} break;
	case MOUSE_EVENT: {
		MOUSE_EVENT_RECORD mer = ir.Event.MouseEvent;
		GetConsoleScreenBufferInfo(PRIVATE->hOut, &PRIVATE->sbi);
		me.modifiers    = getModifiers(mer.dwControlKeyState);
		MouseButtons mb = getButtons(mer.dwButtonState);
		if (mer.dwEventFlags & MOUSE_WHEELED) {
			memcpy((void *)(&we), (const void *)(&me), sizeof(me));
			we.action    = MouseWheel;
			we.direction = short((mer.dwButtonState >> 16) & 0xFFFF) > 0;
			wheelEvent(we, kbddata_);
			break;
		} else {
			me.x      = mer.dwMousePosition.X;
			me.y      = mer.dwMousePosition.Y - PRIVATE->sbi.srWindow.Top;
			bool move = mer.dwEventFlags & MOUSE_MOVED;
			if (move) {
				if (me.action == MouseButtonRelease) {
					me.action = MouseMove;
					return;
				}
				if (me.buttons == 0) return;
				me.action = MouseMove;
			} else {
				if (mb > me.buttons) {
					if (tm_dbl.elapsed_m() <= dbl_interval && prev_p_me.x == me.x && prev_p_me.y == me.y)
						me.action = MouseButtonDblClick;
					else {
						me.action = MouseButtonPress;
						prev_p_me = me;
					}
					tm_dbl.reset();
				} else if (mb < me.buttons)
					me.action = MouseButtonRelease;
				else {
					if (mb != 0) piCoutObj << "WTF";
					break;
				}
			}
			me.buttons = mb;
			if (piCompareBinary(&prev_me, &me, sizeof(me))) break;
			memcpy((void *)(&prev_me), (const void *)(&me), sizeof(me));
			// PIString _s[] = {"press", "release", "dbl click", "move"};
			// piCoutObj << _s[me.action] << me.buttons << ":" << me.x << me.y;
			mouseEvent(me, kbddata_);
			break;
		}
	} break;
	default: piMSleep(10); return;
	}
#else
	tcsetattr(0, TCSANOW, &PRIVATE->tterm);
	if (!PRIVATE->event.wait(0)) return;
	PRIVATE->ret = read(0, rc, 8);
	/*piCout << "key" << PIString(rc).replaceAll("\e", "\\e");
	for (int i = 0; i < PRIVATE->ret; ++i)
	    PICout(0) << PICoutManipulators::Hex << int(((uchar * )&rc)[i]) << ' ';
	PICout(0) << "\n";
	std::cout << PRIVATE->ret << " chars ";
	for (int i = 0; i < PRIVATE->ret; ++i)
	    std::cout << "'" << (char)(rc[i]) << "' " << (int)(uchar)(rc[i]);
	std::cout << std::endl;*/
	if (rc[0] == 0) {
		piMSleep(10);
		return;
	}
	if (PRIVATE->ret < 0 || PRIVATE->ret > 7) {
		piMSleep(10);
		return;
	}
	if (PRIVATE->ret == 1) {
		if (rc[0] == 8)
			ke.key = Backspace;
		else
			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 (PRIVATE->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;
				PRIVATE->ret--;
			}
			if (rc[1] == '[') {
				if (rc[2] == 'M') { // mouse
					if (PRIVATE->ret >= 5) {
						me.x  = uchar(rc[4] - '!');
						me.y  = uchar(rc[5] - '!');
						int b = rc[3] & 0x7, a = rc[3] & 0x60;
						if (a == 0x60) {
							memcpy((void *)(&we), (const void *)(&me), sizeof(me));
							we.action    = MouseWheel;
							we.direction = (b == 0);
							// piCout << "wheel" << we.direction;
							wheelEvent(we, kbddata_);
						} else {
							switch (b) {
							case 0: me.buttons = MouseLeft; break;
							case 1: me.buttons = MouseMiddle; break;
							case 2: me.buttons = MouseRight; break;
							default: break;
							}
							if (a == 0x20) {
								if (b == 3) {
									me.action  = MouseButtonRelease;
									me.buttons = 0;
								} else {
									if (tm_dbl.elapsed_m() <= dbl_interval && prev_p_me.x == me.x && prev_p_me.y == me.y)
										me.action = MouseButtonDblClick;
									else {
										me.action = MouseButtonPress;
										prev_p_me = me;
									}
									tm_dbl.reset();
								}
							}
							if (a == 0x40) me.action = MouseMove;
							// PIString _s[] = {"press", "release", "dbl click", "move"};
							// piCoutObj << _s[me.action] << me.buttons << ":" << me.x << me.y;
							mouseEvent(me, kbddata_);
						}
						// piCout << me.x << me.y << PICoutManipulators::Hex << b << a;
					}
					return;
				} else {
					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;
							PRIVATE->ret -= 2;
							break;
						}
				}
			}
			if (PRIVATE->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;
					PRIVATE->ret--;
				}
			}
			for (int i = 0;; ++i) {
				if (!esc_seq[i].seq) break;
				// piCout << "search" << rc[1] << esc_seq[i].seq;
				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 < PRIVATE->ret; ++i)
		    cout << "'" << (char)(rc[i]) << "' ";
		cout << endl;*/
	}
	if (ke.key == 0 && PRIVATE->ret > 1) ke.key = PIChar::fromSystem(rc).unicode16Code();
#endif
	if ((rc[0] == '\n' || rc[0] == '\r') && PRIVATE->ret == 1) ke.key = Return;
	if (exit_enabled && ke.key == exit_key) {
		PIKbdListener::exiting = true;
		return;
	}
	if (PRIVATE->ret > 0) {
		keyPressed(ke, kbddata_);
		if (ret_func != 0) ret_func(ke, kbddata_);
	}
}


void PIKbdListener::stop() {
	PIThread::stop();
	PRIVATE->event.interrupt();
}


bool PIKbdListener::stopAndWait(PISystemTime timeout) {
	stop();
	return waitForFinish(timeout);
}


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