/*
 * Kangaroo Punch MultiPlayer Game Server Mark II
 * Copyright (C) 2020-2021  Scott Duensing
 *
 * 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 <https://www.gnu.org/licenses/>.
 *
 */


// http://www.ansi-bbs.org/BANSI/bansi.txt
// http://ansi-bbs.org/ansi-bbs-core-server.html


#include "terminal.h"


//#define termWrite(...) logWrite(__VA_ARGS__)
#define termWrite(...) while(0){}


#define TERMINAL_CELL_GET_FLAG(t,x,y,f)   (((t)->cells[(x)][(y)].flags & (1 << (f))) != 0)
#define TERMINAL_CELL_SET_FLAG(t,x,y,f)   ((t)->cells[(x)][(y)].flags |= (1 << (f)))
#define TERMINAL_CELL_CLEAR_FLAG(t,x,y,f) ((t)->cells[(x)][(y)].flags &= (~(1 << (f))))


static void    terminalDel(WidgetT **widget);
static void    terminalFocusEvent(WidgetT *widget, uint8_t focused);
static int16_t terminalIntConvert(char *number, int16_t defaultValue);
static void    terminalMouseEvent(WidgetT *widget, MouseT *mouse, uint16_t x, uint16_t y, uint8_t event);
static void    terminalKeyboardEvent(WidgetT *widget, uint8_t ascii, uint8_t extended, uint8_t scancode, uint8_t shift, uint8_t control, uint8_t alt);
static void    terminalPaint(WidgetT *widget, uint8_t enabled, RectT pos);
static void    terminalSequenceReset(TerminalT *terminal);


void terminalBackgroundSet(TerminalT *terminal, uint8_t color) {
	terminal->background = color;
}


void terminalCharPrint(TerminalT *terminal, uint8_t c) {
	int16_t   x;
	int16_t   y;
	uint16_t  p;

	// Find leading ESC.
	if (c == 27 && !terminal->escFound && !terminal->inEscape) {
		terminal->escFound = 1;
		return;
	}

	// Find [.
	if (c == '[' && terminal->escFound && !terminal->inEscape) {
		terminal->inEscape = 1;
		return;
	}

	// Is this a sequence that is ESC and then not [ ?
	if (terminal->escFound && !terminal->inEscape) {
		termWrite("Unexpected character:  '%c' %d\n", c, c);
		terminalSequenceReset(terminal);
		return;
	}

	if (terminal->inEscape) {
		switch (c) {

			case '[':
			case 27:
				termWrite("Escape found inside sequence\n");
				// ESC sequence inside sequence.  Invalid ANSIBBS.
				//***TODO*** Can stick custom sequences here.
				break;

			// End of a parameter
			case ';':
				arrput(terminal->parameters, strdup(terminal->number));
				terminal->number[0]   = 0;
				terminal->numberIndex = 0;
				break;

			// Cursor up.
			case 'A':
				y = terminalIntConvert(terminal->number, 1);
				termWrite("Moving cursor up %d\n", y);
				terminalCursorMove(terminal, terminal->cursorX, terminal->cursorY - y);
				terminalSequenceReset(terminal);
				break;

			// Cursor down.
			case 'B':
				y = terminalIntConvert(terminal->number, 1);
				termWrite("Moving cursor down %d\n", y);
				terminalCursorMove(terminal, terminal->cursorX, terminal->cursorY + y);
				terminalSequenceReset(terminal);
				break;

			// Cursor forward.
			case 'C':
				x = terminalIntConvert(terminal->number, 1);
				termWrite("Moving cursor forward %d\n", x);
				terminalCursorMove(terminal, terminal->cursorX + x, terminal->cursorY);
				terminalSequenceReset(terminal);
				break;

			// Cursor backward.
			case 'D':
				x = terminalIntConvert(terminal->number, 1);
				termWrite("Moving cursor backward %d\n", x);
				terminalCursorMove(terminal, terminal->cursorX - x, terminal->cursorY);
				terminalSequenceReset(terminal);
				break;

			// Cursor line down.
			case 'E':
				y = terminalIntConvert(terminal->number, 1);
				termWrite("Moving cursor down %d lines\n", y);
				terminalCursorMove(terminal, 1, terminal->cursorY + y);
				//***TODO*** This should allow scrolling.
				terminalSequenceReset(terminal);
				break;

			// Cursor line up.
			case 'F':
				y = terminalIntConvert(terminal->number, 1);
				termWrite("Moving cursor up %d lines\n", y);
				terminalCursorMove(terminal, 1, terminal->cursorY - y);
				//***TODO*** This should allow scrolling.
				terminalSequenceReset(terminal);
				break;

			// Cursor horizontal absolute.
			case 'G':
				x = terminalIntConvert(terminal->number, 1);
				termWrite("Moving cursor horizontal absolute %d\n", x);
				terminalCursorMove(terminal, x, terminal->cursorY);
				terminalSequenceReset(terminal);
				break;

			// Cursor move.
			case 'H':
			case 'f':
				switch (arrlen(terminal->parameters)) {
					// Cursor vertical absolute.  Kinda.  Moves X to 1.
					case 0:
						y = terminalIntConvert(terminal->number, 1);
						termWrite("Moving cursor kinda vertictal absolute %d\n", y);
						terminalCursorMove(terminal, 1, y);
						break;

					// Absolute position.
					case 1:
						x = terminalIntConvert(terminal->number, 1);
						y = terminalIntConvert(terminal->parameters[0], 1);
						termWrite("Moving cursor absolute %d %d\n", x, y);
						terminalCursorMove(terminal, x, y);
						break;

					// Invalid nonsense.
					default:
						termWrite("Unknown cursor move:  '%c' %d\n", c, c);
						break;
				}
				terminalSequenceReset(terminal);
				break;

			// Clear display.
			case 'J':
				if (arrlen(terminal->parameters) == 0 && terminalIntConvert(terminal->number, 1) == 2) {
					termWrite("Clear display\n");
					terminalScreenClear(terminal);
					terminalCursorMove(terminal, 1, 1);
				} else {
					//***TODO*** Unimplemented screen clear.
					termWrite("Unimplemented clear display\n");
				}
				terminalSequenceReset(terminal);
				break;

			// Clear from cursor to end of line.
			case 'K':
				x = terminalIntConvert(terminal->number, 0);
				if (x == 0) {
					termWrite("Clear to end of line\n");
					for (y=terminal->cursorX-1; y<terminal->cols; y++) {
						terminal->cells[y][terminal->cursorY - 1].character = ' ';
						TERMINAL_CELL_SET_FLAG(terminal, y, terminal->cursorY - 1, TERMINAL_FLAG_DIRTY);
						GUI_SET_FLAG((WidgetT *)terminal, WIDGET_FLAG_DIRTY);
					}
				} else {
					//***TODO*** Unimplemented line clear.
					termWrite("Unimplemented line clear\n");
				}
				terminalSequenceReset(terminal);
				break;

			// Insert lines.
			case 'L':
				//***TODO*** Unimplemented.
				termWrite("Unimplemented insert lines\n");
				terminalSequenceReset(terminal);
				break;

			// Delete lines.
			case 'M':
				//***TODO*** Unimplemented.
				termWrite("Unimplemented delete lines\n");
				terminalSequenceReset(terminal);
				break;

			// Delete characters.
			case 'P':
				//***TODO*** Unimplemented.
				termWrite("Unimplemented delete characters\n");
				terminalSequenceReset(terminal);
				break;

			// Scroll up.
			case 'S':
				//***TODO*** Unimplemented.
				termWrite("Unimplemented scroll up\n");
				terminalSequenceReset(terminal);
				break;

			// Scroll down.
			case 'T':
				//***TODO*** Unimplemented.
				termWrite("Unimplemented scroll down\n");
				terminalSequenceReset(terminal);
				break;

			// Clear screen with normal attribute.
			case 'U':
				termWrite("Clear screen with normal attribute\n");
				x = terminal->background;
				terminal->background = TERMINAL_COLOR_BLACK;
				terminalScreenClear(terminal);
				terminal->background = x;
				terminalCursorMove(terminal, 1, 1);
				terminalSequenceReset(terminal);
				break;

			// Back-TAB.
			case 'Z':
				//***TODO*** Unimplemented.
				termWrite("Unimplemented back-TAB\n");
				terminalSequenceReset(terminal);
				break;

			// Set attributes.
			case 'm':
				arrput(terminal->parameters, strdup(terminal->number));
				for (p=0; p<arrlen(terminal->parameters); p++) {
					x = terminalIntConvert(terminal->parameters[p], 0);
					switch (x) {
						// Reset.
						case 0:
							termWrite("Attribute reset\n");
							terminal->bold       = 0;
							terminal->blink      = 0;
							terminal->reverse    = 0;
							terminal->foreground = TERMINAL_COLOR_LIGHT_GRAY;
							terminal->background = TERMINAL_COLOR_BLACK;
							break;

						// Bold.
						case 1:
							termWrite("Bold\n");
							terminal->bold = 1;
							break;

						// Blink slow.
						case 5:
							termWrite("Blink slow\n");
							terminal->blink = TERMINAL_FLAG_BLINK_SLOW;
							break;

						// Blink fast.
						case 6:
							termWrite("Blink fast\n");
							terminal->blink = TERMINAL_FLAG_BLINK_FAST;
							break;

						// Reverse.
						case 7:
							if (!terminal->reverse) {
								termWrite("Reverse\n");
								x                    = terminal->foreground;
								terminal->foreground = terminal->background;
								terminal->background = x;
								terminal->reverse    = 1;
							} else {
								termWrite("Already reversed\n");
							}
							break;

						// Normal intensity.  (22 is the actual code, unsure exactly what 21 is.)
						case 21:
						case 22:
							termWrite("Normal intensity\n");
							terminal->bold = 0;
							break;

						// Steady.
						case 25:
							termWrite("Steady\n");
							terminal->blink = 0;
							break;

						// Normal
						case 27:
							if (terminal->reverse) {
								termWrite("Normal\n");
								x                    = terminal->foreground;
								terminal->foreground = terminal->background;
								terminal->background = x;
								terminal->reverse    = 0;
							} else {
								termWrite("Already normal\n");
							}
							break;

						// Color change.
						default:
							if (x > 29 && x < 38) {
								terminal->foreground = x - 30;
								termWrite("Foreground %d\n", terminal->foreground);
							}
							if (x > 39 && x < 48) {
								terminal->background = x - 40;
								termWrite("Background %d\n", terminal->background);
							}
							break;

					} // switch (x)
				}
				terminalSequenceReset(terminal);
				break;

			// Define scroll region.
			case 'r':
				//***TODO*** Unimplemented.
				termWrite("Unimplemented define scroll region\n");
				terminalSequenceReset(terminal);
				break;

			// Cursor save.
			case 's':
				terminal->saveX = terminal->cursorX;
				terminal->saveY = terminal->cursorY;
				termWrite("Cursor save %d %d\n", terminal->saveX, terminal->saveX);
				terminalSequenceReset(terminal);
				break;

			// Cursor restore.
			case 'u':
				termWrite("Cursor restore %d %d\n", terminal->saveX, terminal->saveX);
				terminal->cursorX = terminal->saveX;
				terminal->cursorY = terminal->saveY;
				terminalSequenceReset(terminal);
				break;

			// Something else.
			default:
				// Number?
				if (c >= '0' && c <= '9') {
					terminal->number[terminal->numberIndex++] = c;
					terminal->number[terminal->numberIndex]   = 0;
				} else {
					// Unknown sequence.
					termWrite("Unknown sequence:  '%c' %d\n", c, c);
					terminalSequenceReset(terminal);
				}

		}  // switch (c)

	} else {  // inEscape

		switch (c) {

			// Bell.
			case 7:
				//***TODO*** Unimplemented.
				termWrite("Unimplemented bell\n");
				break;

			// Backspace.
			case 8:
			case 128:
				//***TODO*** Unimplemented.
				termWrite("Unimplemented backspace\n");
				break;

			// TAB
			case 9:
				//***TODO*** Unimplemented.
				termWrite("Unimplemented TAB\n");
				break;

			// Line feed.
			case 10:
				terminal->cursorY++;
				termWrite("Line feed\n");
				if (terminal->cursorY > terminal->rows) {
					terminal->cursorY--;
					//***TODO*** Scroll.
					termWrite("Unimplemented line feed scroll\n");
				}
				terminalCursorMove(terminal, terminal->cursorX, terminal->cursorY);
				break;

			// Clear screen (form feed).
			case 12:
				termWrite("Form feed\n");
				x = terminal->background;
				terminal->background = TERMINAL_COLOR_BLACK;
				terminalScreenClear(terminal);
				terminal->background = x;
				terminalCursorMove(terminal, 1, 1);
				break;

			// Carrage return.
			case 13:
				termWrite("Carrage return\n");
				terminalCursorMove(terminal, 1, terminal->cursorY);
				break;

			// Non-special character.
			default:
				// Render character.
				terminal->cells[terminal->cursorX - 1][terminal->cursorY - 1].character  = c;
				terminal->cells[terminal->cursorX - 1][terminal->cursorY - 1].background = terminal->background;
				terminal->cells[terminal->cursorX - 1][terminal->cursorY - 1].foreground = terminal->foreground + (terminal->bold ? 8 : 0);
				terminal->cells[terminal->cursorX - 1][terminal->cursorY - 1].flags      = 0;
				if (terminal->blink == TERMINAL_FLAG_BLINK_SLOW) TERMINAL_CELL_SET_FLAG(terminal, terminal->cursorX - 1, terminal->cursorY - 1, TERMINAL_FLAG_BLINK_SLOW);
				if (terminal->blink == TERMINAL_FLAG_BLINK_FAST) TERMINAL_CELL_SET_FLAG(terminal, terminal->cursorX - 1, terminal->cursorY - 1, TERMINAL_FLAG_BLINK_FAST);
				TERMINAL_CELL_SET_FLAG(terminal, terminal->cursorX - 1, terminal->cursorY - 1, TERMINAL_FLAG_DIRTY);
				GUI_SET_FLAG((WidgetT *)terminal, WIDGET_FLAG_DIRTY);
				// Move cursor.
				terminal->cursorX++;
				if (terminal->cursorX > terminal->cols) {
					terminal->cursorX = 1;
					terminal->cursorY++;
					if (terminal->cursorY > terminal->rows) {
						terminal->cursorY--;
						//***TODO*** Scroll.
						termWrite("Unimplemented render scroll\n");
					}
				}
				terminalCursorMove(terminal, terminal->cursorX, terminal->cursorY);
				break;

		}  // switch (c)

	}  // inEscape
}


void terminalCursorMove(TerminalT *terminal, uint16_t col, uint16_t row) {

	// Clamp X.
	if (col < 1) {
		terminal->cursorX = 1;
	} else {
		if (col > terminal->cols) {
			terminal->cursorX = terminal->cols;
		} else {
			terminal->cursorX = col;
		}
	}

	// Clamp Y.
	if (row < 1) {
		terminal->cursorY = 1;
	} else {
		if (row > terminal->rows) {
			terminal->cursorY = terminal->rows;
		} else {
			terminal->cursorY = row;
		}
	}
}


static void terminalDel(WidgetT **widget) {
	TerminalT *t = (TerminalT *)*widget;
	uint16_t   x;

	terminalSequenceReset(t);

	for (x=0; x<t->cols; x++) {
		free(t->cells[x]);
	}
	free(t->cells);
}


static void terminalFocusEvent(WidgetT *widget, uint8_t focused) {
	TerminalT *t = (TerminalT *)widget;
	uint16_t   x;
	uint16_t   y;

	(void)focused;

	// When focus changes, we need to mark ALL the cells dirty so they get redrawn when the window is redrawn.
	for (y=0; y<t->rows; y++) {
		for (x=0; x<t->cols; x++) {
			TERMINAL_CELL_SET_FLAG(t, x, y, TERMINAL_FLAG_DIRTY);
		}
	}

	GUI_SET_FLAG(widget, WIDGET_FLAG_DIRTY);
}


void terminalForegroundSet(TerminalT *terminal, uint8_t color) {
	terminal->foreground = color;
	terminal->bold = (color > 7);
}


WidgetT *terminalInit(WidgetT *widget, uint16_t cols, uint16_t rows) {
	TerminalT *t = (TerminalT *)widget;
	uint16_t   x;
	uint16_t   y;

	t->base.delMethod           = terminalDel;
	t->base.focusMethod         = terminalFocusEvent;
	t->base.paintMethod         = terminalPaint;
	t->base.keyboardEventMethod = terminalKeyboardEvent;
	t->base.mouseEventMethod    = terminalMouseEvent;
	t->font                     = _guiFont;  // Default font.  Also see terminalNew.
	t->cols                     = cols;
	t->rows                     = rows;
	t->cursorX                  = 1;
	t->cursorY                  = 1;
	t->saveX                    = 1;
	t->saveY                    = 1;
	t->bold                     = 0;
	t->blink                    = 0;
	t->reverse                  = 0;
	t->background               = TERMINAL_COLOR_BLACK;
	t->foreground               = TERMINAL_COLOR_LIGHT_GRAY;
	t->escFound                 = 0;
	t->inEscape                 = 0;
	t->numberIndex              = 0;
	t->number[0]                = 0;
	t->parameters               = NULL;

	// Allocate space for column data.
	t->cells = (CellT **)malloc(sizeof(CellT *) * cols);
	if (!t->cells) return NULL;
	// Allocate space for row data.
	for (x=0; x<cols; x++) {
		t->cells[x] = (CellT *)malloc(sizeof(CellT) * rows);
		if (!t->cells[x]) {
			for (y=0; y<x; y++) {
				free(t->cells[y]);
			}
			free(t->cells);
			return NULL;
		}
	}

	// Set up default palette.
	t->palette[TERMINAL_COLOR_BLACK]          = vbeColorMake(  0,   0,   0);
	t->palette[TERMINAL_COLOR_BLUE]           = vbeColorMake(170,   0,   0);
	t->palette[TERMINAL_COLOR_GREEN]          = vbeColorMake(  0, 170,   0);
	t->palette[TERMINAL_COLOR_CYAN]           = vbeColorMake(170,  85,   0);
	t->palette[TERMINAL_COLOR_RED]            = vbeColorMake(  0,   0, 170);
	t->palette[TERMINAL_COLOR_MAGENTA]        = vbeColorMake(170,   0, 170);
	t->palette[TERMINAL_COLOR_BROWN]          = vbeColorMake(  0, 170, 170);
	t->palette[TERMINAL_COLOR_LIGHT_GRAY]     = vbeColorMake(170, 170, 170);
	t->palette[TERMINAL_COLOR_DARK_GRAY]      = vbeColorMake( 85,  85,  85);
	t->palette[TERMINAL_COLOR_BRIGHT_BLUE]    = vbeColorMake(255,  85,  85);
	t->palette[TERMINAL_COLOR_BRIGHT_GREEN]   = vbeColorMake( 85, 255,  85);
	t->palette[TERMINAL_COLOR_BRIGHT_CYAN]    = vbeColorMake(255, 255,  85);
	t->palette[TERMINAL_COLOR_BRIGHT_RED]     = vbeColorMake( 85,  85, 255);
	t->palette[TERMINAL_COLOR_BRIGHT_MAGENTA] = vbeColorMake(255,  85, 255);
	t->palette[TERMINAL_COLOR_YELLOW]         = vbeColorMake( 85, 255, 255);
	t->palette[TERMINAL_COLOR_WHITE]          = vbeColorMake(255, 255, 255);

	// Default attributes is gray on black, no bold, no blink, and dirty.
	for (y=0; y<rows; y++) {
		for (x=0; x<cols; x++) {
			t->cells[x][y].character  = ' ';
			t->cells[x][y].background = TERMINAL_COLOR_BLACK;
			t->cells[x][y].foreground = TERMINAL_COLOR_LIGHT_GRAY;
			t->cells[x][y].flags      = 0;
			TERMINAL_CELL_SET_FLAG(t, x, y, TERMINAL_FLAG_DIRTY);
		}
	}

	return widget;
}


static int16_t terminalIntConvert(char *number, int16_t defaultValue) {
	char    *end    = NULL;
	int16_t  result = (int16_t)strtol(number, &end, 10);

	if (end == NULL || number[0] == 0) result = defaultValue;

	return result;
}


static void terminalKeyboardEvent(WidgetT *widget, uint8_t ascii, uint8_t extended, uint8_t scancode, uint8_t shift, uint8_t control, uint8_t alt) {

}


static void terminalMouseEvent(WidgetT *widget, MouseT *mouse, uint16_t x, uint16_t y, uint8_t event) {
	TerminalT *t = (TerminalT *)widget;

	(void)mouse;

	// Fire callback on mouse up.
	if (event == MOUSE_EVENT_LEFT_UP) {

	}
}


TerminalT *terminalNew(uint16_t x, uint16_t y, uint16_t cols, uint16_t rows) {
	TerminalT *terminal = (TerminalT *)malloc(sizeof(TerminalT));
	WidgetT   *widget  = NULL;
	uint16_t   w;
	uint16_t   h;

	if (!terminal) return NULL;

	// Default font.  Also see terminalInit.
	w = fontWidthGet(_guiFont) * cols;
	h = fontHeightGet(_guiFont) * rows;

	widget = widgetInit(W(terminal), MAGIC_TERMINAL, x, y, w, h, 0, 0, 0, 0);
	if (!widget) {
		free(terminal);
		return NULL;
	}

	terminal = (TerminalT *)terminalInit((WidgetT *)terminal, cols, rows);
	if (!terminal) {
		free(terminal);
		return NULL;
	}

	return terminal;
}


static void terminalPaint(WidgetT *widget, uint8_t enabled, RectT pos) {
	TerminalT *t = (TerminalT *)widget;
	uint16_t   x;
	uint16_t   y;
	uint16_t   xp;
	uint16_t   yp;
	char       c[2];

	c[1] = 0;

	yp = pos.y;
	for (y=0; y<t->rows; y++) {
		xp = pos.x;
		for (x=0; x<t->cols; x++) {
			if (TERMINAL_CELL_GET_FLAG(t, x, y, TERMINAL_FLAG_DIRTY)) {
				c[0] = t->cells[x][y].character;
				fontRender(t->font, c, t->palette[t->cells[x][y].foreground], t->palette[t->cells[x][y].background], xp, yp);
				TERMINAL_CELL_CLEAR_FLAG(t, x, y, TERMINAL_FLAG_DIRTY);
			}
			xp += fontWidthGet(t->font);
		}
		yp += fontHeightGet(t->font);
	}
}


void terminalScreenClear(TerminalT *terminal) {
	uint16_t   x;
	uint16_t   y;

	// Does not move cursor.

	for (y=0; y<terminal->rows; y++) {
		for (x=0; x<terminal->cols; x++) {
			terminal->cells[x][y].character  = ' ';
			TERMINAL_CELL_CLEAR_FLAG(terminal, x, y, TERMINAL_FLAG_BLINK_SLOW);
			TERMINAL_CELL_CLEAR_FLAG(terminal, x, y, TERMINAL_FLAG_BLINK_FAST);
			TERMINAL_CELL_SET_FLAG(terminal, x, y, TERMINAL_FLAG_DIRTY);
		}
	}
	GUI_SET_FLAG((WidgetT *)terminal, WIDGET_FLAG_DIRTY);

}


static void terminalSequenceReset(TerminalT *terminal) {
	terminal->escFound    = 0;
	terminal->inEscape    = 0;
	terminal->number[0]   = 0;
	terminal->numberIndex = 0;

	if (terminal->parameters != NULL) {
		while (arrlen(terminal->parameters) > 0) {
			free(arrpop(terminal->parameters));
		}
		terminal->parameters = NULL;
	}
}


void terminalStringPrint(TerminalT *terminal, char *string) {
	uint16_t  i;

	for (i=0; i<strlen(string); i++) {
		terminalCharPrint(terminal, string[i]);
	}
}


