/*
    PIP - Platform Independent Primitives
    Console output/input
    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 "piscreen.h"

#include "piincludes_p.h"
#include "piliterals_time.h"
// clang-format off
#ifndef WINDOWS
#  include <fcntl.h>
#  include <sys/ioctl.h>
#  include <termios.h>
#else
#  include <wingdi.h>
#  include <wincon.h>
#  ifndef COMMON_LVB_UNDERSCORE
#	define COMMON_LVB_UNDERSCORE 0x8000
#  endif
#endif
// clang-format on


using namespace PIScreenTypes;


PRIVATE_DEFINITION_START(PIScreen::SystemConsole)
#ifdef WINDOWS
	void * hOut;
	CONSOLE_SCREEN_BUFFER_INFO sbi, csbi;
	CONSOLE_CURSOR_INFO curinfo;
	COORD ccoord, ulcoord, bs, bc;
	SMALL_RECT srect;
	WORD dattr;
	DWORD smode, written;
	PIVector<CHAR_INFO> chars;
#endif
PRIVATE_DEFINITION_END(PIScreen::SystemConsole)


PIScreen::SystemConsole::SystemConsole() {
	width = height = pwidth = pheight = 0;
	mouse_x = mouse_y = -1;
}


PIScreen::SystemConsole::~SystemConsole() {
#ifdef WINDOWS
	SetConsoleMode(PRIVATE->hOut, PRIVATE->smode);
	SetConsoleTextAttribute(PRIVATE->hOut, PRIVATE->dattr);
#endif
}


void PIScreen::SystemConsole::begin() {
	int w, h;
#ifdef WINDOWS
	PRIVATE->ulcoord.X = 0;
	PRIVATE->hOut      = GetStdHandle(STD_OUTPUT_HANDLE);
	GetConsoleScreenBufferInfo(PRIVATE->hOut, &PRIVATE->sbi);
	PRIVATE->dattr     = PRIVATE->sbi.wAttributes;
	w                  = PRIVATE->sbi.srWindow.Right - PRIVATE->sbi.srWindow.Left;
	h                  = PRIVATE->sbi.srWindow.Bottom - PRIVATE->sbi.srWindow.Top;
	PRIVATE->ulcoord.Y = PRIVATE->sbi.srWindow.Top;
	GetConsoleMode(PRIVATE->hOut, &PRIVATE->smode);
	GetConsoleCursorInfo(PRIVATE->hOut, &PRIVATE->curinfo);
#else
#  ifdef MICRO_PIP
	w = 80;
	h = 24;
#  else
	winsize ws;
	ioctl(0, TIOCGWINSZ, &ws);
	w = ws.ws_col;
	h = ws.ws_row;
#  endif
#endif
	resize(w, h);
#ifdef WINDOWS
	SetConsoleMode(PRIVATE->hOut, ENABLE_WRAP_AT_EOL_OUTPUT);
	GetConsoleScreenBufferInfo(PRIVATE->hOut, &PRIVATE->sbi);
	PRIVATE->bc.X = 0;
	PRIVATE->bc.Y = 0;
#endif
	clear();
	clearScreen();
	hideCursor();
}


void PIScreen::SystemConsole::end() {
#ifdef WINDOWS
	SetConsoleTextAttribute(PRIVATE->hOut, PRIVATE->dattr);
#else
	printf("\e[0m");
#endif
	moveTo(0, height);
	showCursor();
}


void PIScreen::SystemConsole::prepare() {
	int w = 80, h = 24;
#ifdef WINDOWS
	GetConsoleScreenBufferInfo(PRIVATE->hOut, &PRIVATE->csbi);
	w = PRIVATE->csbi.srWindow.Right - PRIVATE->csbi.srWindow.Left + 1;
	h = PRIVATE->csbi.srWindow.Bottom - PRIVATE->csbi.srWindow.Top + 1;
#else
#  ifndef MICRO_PIP
	winsize ws;
	ioctl(0, TIOCGWINSZ, &ws);
	w = ws.ws_col;
	h = ws.ws_row;
#  endif
#endif
	resize(w, h);
}


void PIScreen::SystemConsole::clear() {
	for (int i = 0; i < cells.size_s(); ++i)
		cells[i].fill(Cell());
}


void PIScreen::SystemConsole::resize(int w, int h) {
	if (w == pwidth && h == pheight) return;
	width   = piMaxi(w, 0);
	height  = piMaxi(h, 0);
	pwidth  = width;
	pheight = height;
	cells.resize(height);
	pcells.resize(height);
	for (int i = 0; i < height; ++i) {
		cells[i].resize(width);
		pcells[i].resize(width, Cell(PIChar()));
	}
#ifdef WINDOWS
	PRIVATE->sbi.srWindow = PRIVATE->csbi.srWindow;
	PRIVATE->chars.resize(width * height);
#endif
	for (int i = 0; i < pcells.size_s(); ++i)
		pcells[i].fill(Cell());
	clear();
	clearScreen();
}


void PIScreen::SystemConsole::print() {
	if (mouse_x >= 0 && mouse_x < width && mouse_y >= 0 && mouse_y < height) {
		/// cells[mouse_y][mouse_x].format.flags ^= Inverse;
	}
#ifdef WINDOWS
	PRIVATE->srect = PRIVATE->sbi.srWindow;
	int dx0 = -1, dx1 = -1, dy0 = -1, dy1 = -1;
	for (int j = 0; j < height; ++j) {
		PIVector<Cell> & ccv(cells[j]);
		PIVector<Cell> & pcv(pcells[j]);
		for (int i = 0; i < width; ++i)
			if (ccv[i] != pcv[i]) {
				if (dx0 < 0) {
					dx0 = dx1 = i;
					dy0 = dy1 = j;
				} else {
					dx0 = piMini(dx0, i);
					dx1 = piMaxi(dx1, i);
					dy0 = piMini(dy0, j);
					dy1 = piMaxi(dy1, j);
				}
			}
	}
	if (dx0 < 0) return;
	int dw = dx1 - dx0 + 1, dh = dy1 - dy0 + 1;
	for (int i = 0; i < dw; ++i)
		for (int j = 0; j < dh; ++j) {
			int k = j * dw + i;
			Cell & c(cells[j + dy0][i + dx0]);
			PRIVATE->chars[k].Char.UnicodeChar = c.symbol.unicode16Code();
			// PRIVATE->chars[k].Char.AsciiChar = c.symbol.toConsole1Byte();
			PRIVATE->chars[k].Attributes       = attributes(c);
		}
	// piCout << "draw" << dw << dh;
	PRIVATE->bs.X = dw;
	PRIVATE->bs.Y = dh;
	PRIVATE->srect.Left += dx0;
	PRIVATE->srect.Top += dy0;
	PRIVATE->srect.Right -= width - dx1 - 1;
	PRIVATE->srect.Bottom -= height - dy1 - 1;
	WriteConsoleOutputW(PRIVATE->hOut, PRIVATE->chars.data(), PRIVATE->bs, PRIVATE->bc, &PRIVATE->srect);
#else
	PIString s;
	int si = 0, sj = 0;
	CellFormat prf(0xFFFF);
	for (int j = 0; j < height; ++j) {
		PIVector<Cell> & ccv(cells[j]);
		PIVector<Cell> & pcv(pcells[j]);
		for (int i = 0; i < width; ++i) {
			Cell & cc(ccv[i]);
			Cell & pc(pcv[i]);
			if (cc != pc) {
				if (s.isEmpty()) {
					si = i;
					sj = j;
				}
				if (prf != cc.format) {
					prf = cc.format;
					s += formatString(cc);
				}
				s += cc.symbol;
			} else {
				if (!s.isEmpty()) {
					moveTo(si, sj);
					PICout::stdoutPIString(s);
					s.clear();
				}
			}
		}
		if (!s.isEmpty()) {
			moveTo(si, sj);
			PICout::stdoutPIString(s);
			// printf("%s", s.data());
			s.clear();
		}
	}
	printf("\e[0m");
	fflush(0);
#endif
	pcells = cells;
}


#ifdef WINDOWS
#  define FOREGROUND_MASK (FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE)
#  define BACKGROUND_MASK (BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE)
ushort PIScreen::SystemConsole::attributes(const PIScreenTypes::Cell & c) {
	WORD attr = PRIVATE->dattr;
	if (c.format.flags & Bold)
		attr |= FOREGROUND_INTENSITY;
	else
		attr &= ~FOREGROUND_INTENSITY;
	if (c.format.flags & Underline)
		attr |= COMMON_LVB_UNDERSCORE;
	else
		attr &= ~COMMON_LVB_UNDERSCORE;
	switch (c.format.color_char) {
	case Black: attr = (attr & ~FOREGROUND_MASK); break;
	case Red: attr = (attr & ~FOREGROUND_MASK) | FOREGROUND_RED; break;
	case Green: attr = (attr & ~FOREGROUND_MASK) | FOREGROUND_GREEN; break;
	case Blue: attr = (attr & ~FOREGROUND_MASK) | FOREGROUND_BLUE; break;
	case Cyan: attr = (attr & ~FOREGROUND_MASK) | FOREGROUND_GREEN | FOREGROUND_BLUE; break;
	case Magenta: attr = (attr & ~FOREGROUND_MASK) | FOREGROUND_RED | FOREGROUND_BLUE; break;
	case Yellow: attr = (attr & ~FOREGROUND_MASK) | FOREGROUND_RED | FOREGROUND_GREEN; break;
	case White: attr = attr | FOREGROUND_MASK; break;
	}
	switch (c.format.color_back) {
	case Black: attr = (attr & ~BACKGROUND_MASK); break;
	case Red: attr = (attr & ~BACKGROUND_MASK) | BACKGROUND_RED; break;
	case Green: attr = (attr & ~BACKGROUND_MASK) | BACKGROUND_GREEN; break;
	case Blue: attr = (attr & ~BACKGROUND_MASK) | BACKGROUND_BLUE; break;
	case Cyan: attr = (attr & ~BACKGROUND_MASK) | BACKGROUND_GREEN | BACKGROUND_BLUE; break;
	case Magenta: attr = (attr & ~BACKGROUND_MASK) | BACKGROUND_RED | BACKGROUND_BLUE; break;
	case Yellow: attr = (attr & ~BACKGROUND_MASK) | BACKGROUND_RED | BACKGROUND_GREEN; break;
	case White: attr = attr | BACKGROUND_MASK; break;
	}
	if ((c.format.flags & Inverse) == Inverse) {
		uchar f = attr & 0xFF;
		attr &= 0xFFFFFF00;
		f = (f << 4) | (f >> 4);
		attr |= f;
	}
	return attr;
}
#  undef FOREGROUND_MASK
#  undef BACKGROUND_MASK

void PIScreen::SystemConsole::getWinCurCoord() {
	GetConsoleScreenBufferInfo(PRIVATE->hOut, &PRIVATE->csbi);
	PRIVATE->ccoord = PRIVATE->csbi.dwCursorPosition;
}

void PIScreen::SystemConsole::clearLine() {
	getWinCurCoord();
	FillConsoleOutputAttribute(PRIVATE->hOut, PRIVATE->dattr, width - PRIVATE->ccoord.X, PRIVATE->ccoord, &PRIVATE->written);
	FillConsoleOutputCharacter(PRIVATE->hOut, ' ', width - PRIVATE->ccoord.X, PRIVATE->ccoord, &PRIVATE->written);
}

void PIScreen::SystemConsole::newLine() {
	getWinCurCoord();
	PRIVATE->ccoord.X = 0;
	PRIVATE->ccoord.Y++;
	SetConsoleCursorPosition(PRIVATE->hOut, PRIVATE->ccoord);
}
#else  // WINDOWS
PIString PIScreen::SystemConsole::formatString(const PIScreenTypes::Cell & c) {
	PIString ts = PIStringAscii("\e[0");
	switch (c.format.color_char) {
	case Black: ts += PIStringAscii(";30"); break;
	case Red: ts += PIStringAscii(";31"); break;
	case Green: ts += PIStringAscii(";32"); break;
	case Blue: ts += PIStringAscii(";34"); break;
	case Cyan: ts += PIStringAscii(";36"); break;
	case Magenta: ts += PIStringAscii(";35"); break;
	case Yellow: ts += PIStringAscii(";33"); break;
	case White: ts += PIStringAscii(";37"); break;
	}
	switch (c.format.color_back) {
	case Black: ts += PIStringAscii(";40"); break;
	case Red: ts += PIStringAscii(";41"); break;
	case Green: ts += PIStringAscii(";42"); break;
	case Blue: ts += PIStringAscii(";44"); break;
	case Cyan: ts += PIStringAscii(";46"); break;
	case Magenta: ts += PIStringAscii(";45"); break;
	case Yellow: ts += PIStringAscii(";43"); break;
	case White: ts += PIStringAscii(";47"); break;
	}
	if ((c.format.flags & Bold) == Bold) ts += PIStringAscii(";1");
	if ((c.format.flags & Underline) == Underline) ts += PIStringAscii(";4");
	if ((c.format.flags & Blink) == Blink) ts += PIStringAscii(";5");
	if ((c.format.flags & Inverse) == Inverse) ts += PIStringAscii(";7");
	return ts + 'm';
}
#endif // WINDOWS


void PIScreen::SystemConsole::toUpperLeft() {
#ifdef WINDOWS
	SetConsoleCursorPosition(PRIVATE->hOut, PRIVATE->ulcoord);
#else
	printf("\e[H");
#endif
}

void PIScreen::SystemConsole::moveTo(int x, int y) {
#ifdef WINDOWS
	PRIVATE->ccoord.X = x;
	PRIVATE->ccoord.Y = PRIVATE->ulcoord.Y + y;
	SetConsoleCursorPosition(PRIVATE->hOut, PRIVATE->ccoord);
#else
	printf("\e[%d;%dH", y + 1, x + 1);
#endif
}

void PIScreen::SystemConsole::clearScreen() {
#ifdef WINDOWS
	toUpperLeft();
	FillConsoleOutputAttribute(PRIVATE->hOut, PRIVATE->dattr, width * (height + 1), PRIVATE->ulcoord, &PRIVATE->written);
	FillConsoleOutputCharacter(PRIVATE->hOut, ' ', width * (height + 1), PRIVATE->ulcoord, &PRIVATE->written);
#else
	printf("\e[0m\e[H\e[J");
#endif
}

void PIScreen::SystemConsole::clearScreenLower() {
#ifdef WINDOWS
	getWinCurCoord();
	FillConsoleOutputAttribute(PRIVATE->hOut,
	                           PRIVATE->dattr,
	                           width * height - width * PRIVATE->ccoord.Y + PRIVATE->ccoord.X,
	                           PRIVATE->ccoord,
	                           &PRIVATE->written);
	FillConsoleOutputCharacter(PRIVATE->hOut,
	                           ' ',
	                           width * height - width * PRIVATE->ccoord.Y + PRIVATE->ccoord.X,
	                           PRIVATE->ccoord,
	                           &PRIVATE->written);
#else
	printf("\e[0m\e[J");
#endif
}

void PIScreen::SystemConsole::hideCursor() {
#ifdef WINDOWS
	PRIVATE->curinfo.bVisible = false;
	SetConsoleCursorInfo(PRIVATE->hOut, &PRIVATE->curinfo);
#else
	printf("\e[?25l");
#endif
}

void PIScreen::SystemConsole::showCursor() {
#ifdef WINDOWS
	PRIVATE->curinfo.bVisible = true;
	SetConsoleCursorInfo(PRIVATE->hOut, &PRIVATE->curinfo);
#else
	printf("\e[?25h");
#endif
}


// PIScreen

PIScreen::PIScreen(bool startNow, PIKbdListener::KBFunc slot): PIThread(), drawer_(console.cells), root("rootTile") {
	setName("screen");
	setPriority(piLow);
	needLockRun(true);
	mouse_     = false;
	ret_func   = slot;
	tile_focus = tile_dialog = nullptr;
	root.screen              = this;
	listener                 = new PIKbdListener(key_eventS, this, startNow);
	CONNECT1(void, PIKbdListener::MouseEvent, listener, mouseEvent, this, mouse_event);
	CONNECT1(void, PIKbdListener::WheelEvent, listener, wheelEvent, this, wheel_event);
	if (startNow) start();
}


PIScreen::~PIScreen() {
	if (isRunning()) stop();
	PIThread::waitForFinish(100_ms);
	listener->waitForFinish(100_ms);
	delete listener;
}


void PIScreen::setMouseEnabled(bool on) {
	mouse_          = on;
	console.mouse_x = console.mouse_y = -1;
}


void PIScreen::key_event(PIKbdListener::KeyEvent key) {
	/** DEBUG
	if (ret_func != 0) ret_func(key, t);
	keyPressed(key, t);
	return;
	*/
	PIScreenTile * rtile = rootTile();
	if (tile_dialog) rtile = tile_dialog;
	bool used = nextFocus(rtile, key);
	if (used) return;
	if (!used && tile_focus) {
		if (tile_focus->visible) {
			if (tile_focus->keyEvent(key)) return;
		}
	}
	if (ret_func != 0) ret_func(key, data_);
	keyPressed(key, data_);
}


PIVector<PIScreenTile *> PIScreen::prepareMouse(PIKbdListener::MouseEvent * e) {
	PIVector<PIScreenTile *> ret;
	if (!mouse_ || !e) return ret;
	console.mouse_x             = e->x;
	console.mouse_y             = e->y;
	PIVector<PIScreenTile *> tl = tilesUnderMouse(e->x, e->y);
	bool ff                     = false;
	piForeachR(PIScreenTile * t, tl) {
		if (!ff) {
			if (t->focus_flags[FocusOnMouse] && (e->action == PIKbdListener::MouseButtonPress)) {
				t->setFocus();
				ff = true;
			}
			if (t->focus_flags[FocusOnWheel] && (e->action == PIKbdListener::MouseWheel)) {
				t->setFocus();
				ff = true;
			}
		}
	}
	return tl;
}


PIVector<PIScreenTile *> PIScreen::tilesUnderMouse(int x, int y) {
	PIVector<PIScreenTile *> ret;
	if (x < 0 || x >= console.width || y < 0 || y >= console.height) return ret;
	PIScreenTile * ct = tile_dialog ? tile_dialog : rootTile();
	bool f            = true;
	while (ct) {
		if (!f) ret << ct;
		f  = false;
		ct = ct->childUnderMouse(x, y);
	}
	return ret;
}


void PIScreen::mouse_event(PIKbdListener::MouseEvent me) {
	PIVector<PIScreenTile *> tl = prepareMouse(&me);
	if (tl.isEmpty()) return;
	piForeachR(PIScreenTile * t, tl)
		if (t->mouseEvent(me)) break;
}


void PIScreen::wheel_event(PIKbdListener::WheelEvent we) {
	PIVector<PIScreenTile *> tl = prepareMouse(&we);
	if (tl.isEmpty()) return;
	piForeachR(PIScreenTile * t, tl)
		if (t->wheelEvent(we)) break;
}


bool PIScreen::nextFocus(PIScreenTile * rt, PIKbdListener::KeyEvent key) {
	PIVector<PIScreenTile *> vtl = rt->children(true), ftl;
	for (PIScreenTile * t: vtl) {
		if (t->focus_flags[CanHasFocus]) ftl << t;
	}
	int ind = -1;
	for (int i = 0; i < ftl.size_s(); ++i)
		if (ftl[i] == tile_focus) {
			ind = i;
			break;
		}
	if (ind < 0) tile_focus = 0;
	if (ftl.isEmpty())
		tile_focus = 0;
	else {
		if (tile_focus)
			if (!tile_focus->visible) tile_focus = 0;
		int next = tile_focus ? 0 : 1;
		if (tile_focus) {
			if (tile_focus->focus_flags[NextByTab] && key.key == PIKbdListener::Tab) next = 1;
			if (tile_focus->focus_flags[NextByArrowsHorizontal]) {
				if (key.key == PIKbdListener::LeftArrow) next = -1;
				if (key.key == PIKbdListener::RightArrow) next = 1;
			}
			if (tile_focus->focus_flags[NextByArrowsVertical]) {
				if (key.key == PIKbdListener::UpArrow) next = -1;
				if (key.key == PIKbdListener::DownArrow) next = 1;
			}
		}
		// piCout << ftl.size() << ind << next;
		if (next != 0) {
			PIVector<PIScreenTile *> tl = rt->children();
			for (PIScreenTile * t: tl)
				t->has_focus = false;
			if (!ftl.isEmpty()) {
				ind += next;
				if (ind >= ftl.size_s()) ind = 0;
				if (ind < 0) ind = ftl.size_s() - 1;
				tile_focus            = ftl[ind];
				tile_focus->has_focus = true;
			}
			return true;
		}
	}
	return false;
}


void PIScreen::tileEventInternal(PIScreenTile * t, TileEvent e) {
	tileEvent(t, e);
}


void PIScreen::tileRemovedInternal(PIScreenTile * t) {
	if (tile_dialog == t) tile_dialog = 0;
}


void PIScreen::tileSetFocusInternal(PIScreenTile * t) {
	PIScreenTile * rt = rootTile();
	if (tile_dialog) rt = tile_dialog;
	PIVector<PIScreenTile *> tl = rt->children(), ftl;
	for (PIScreenTile * i: tl)
		i->has_focus = false;
	tile_focus = t;
	if (!tile_focus) return;
	if (tile_focus->focus_flags[CanHasFocus]) tile_focus->has_focus = true;
}


void PIScreen::setDialogTile(PIScreenTile * t) {
	tile_dialog = t;
	if (!tile_dialog) {
		nextFocus(&root);
		return;
	}
	tile_dialog->setScreen(this);
	tile_dialog->parent = 0;
	nextFocus(tile_dialog);
}


void PIScreen::waitForFinish() {
	WAIT_FOR_EXIT
	stop();
}


void PIScreen::start(bool wait) {
	PIThread::start(25_Hz);
	if (wait) waitForFinish();
}


void PIScreen::stop(bool clear) {
	PIThread::stopAndWait();
	if (clear) console.clearScreen();
#ifndef WINDOWS
	fflush(0);
#endif
}


void PIScreen::begin() {
	listener->start();
	nextFocus(&root);
	console.begin();
}


void PIScreen::run() {
	console.prepare();
	root.width_ = drawer_.width = console.width;
	root.height_ = drawer_.height = console.height;
	root.layout();
	root.drawEventInternal(&drawer_);
	if (tile_dialog) {
		int sw(0), sh(0);
		tile_dialog->sizeHint(sw, sh);
		sw                   = piClampi(sw, tile_dialog->minimumWidth, tile_dialog->maximumWidth);
		sh                   = piClampi(sh, tile_dialog->minimumHeight, tile_dialog->maximumHeight);
		tile_dialog->x_      = (console.width - sw) / 2;
		tile_dialog->y_      = (console.height - sh) / 2;
		tile_dialog->width_  = sw;
		tile_dialog->height_ = sh;
		tile_dialog->layout();
		int dx = tile_dialog->x_ - 1, dy = tile_dialog->y_ - 1, dw = tile_dialog->width_, dh = tile_dialog->height_;
		drawer_.drawFrame(dx,
		                  dy,
		                  dx + dw + 1,
		                  dy + dh + 1,
		                  (Color)tile_dialog->back_format.color_char,
		                  (Color)tile_dialog->back_format.color_back,
		                  (CharFlags)tile_dialog->back_format.flags);
		tile_dialog->drawEventInternal(&drawer_);
	}
	console.print();
}


void PIScreen::end() {
	listener->stop();
	console.end();
}


PIScreenTile * PIScreen::tileByName(const PIString & name) {
	PIVector<PIScreenTile *> tl(tiles());
	for (PIScreenTile * t: tl)
		if (t->name() == name) return t;
	return 0;
}
