#include "terminal.hpp"
#include "history.hpp"
#include "linenoise.hpp"
#include <termios.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <ctype.h>
#include <sys/ioctl.h>
#include <sys/select.h>
#include <sys/time.h>

namespace duckdb {

static int mlmode = 1;              /* Multi line mode. Default is multi line. */
static struct termios orig_termios; /* In order to restore at exit.*/
static int atexit_registered = 0;   /* Register atexit just 1 time. */
static int rawmode = 0;             /* For atexit() function to check if restore is needed*/
static const char *unsupported_term[] = {"dumb", "cons25", "emacs", NULL};

/* At exit we'll try to fix the terminal to the initial conditions. */
static void linenoiseAtExit(void) {
	Terminal::DisableRawMode();
	History::Free();
}

/* Return true if the terminal name is in the list of terminals we know are
 * not able to understand basic escape sequences. */
int Terminal::IsUnsupportedTerm() {
	char *term = getenv("TERM");
	int j;

	if (!term) {
		return 0;
	}
	for (j = 0; unsupported_term[j]; j++) {
		if (!strcasecmp(term, unsupported_term[j])) {
			return 1;
		}
	}
	return 0;
}

/* Raw mode: 1960 magic shit. */
int Terminal::EnableRawMode() {
	int fd = STDIN_FILENO;

	if (!isatty(STDIN_FILENO)) {
		errno = ENOTTY;
		return -1;
	}
	if (!atexit_registered) {
		atexit(linenoiseAtExit);
		atexit_registered = 1;
	}
	if (tcgetattr(fd, &orig_termios) == -1) {
		errno = ENOTTY;
		return -1;
	}

	auto raw = orig_termios; /* modify the original mode */
	/* input modes: no break, no CR to NL, no parity check, no strip char,
	 * no start/stop output control. */
	raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
	/* output modes - disable post processing */
	raw.c_oflag &= ~(OPOST);
#ifdef IUTF8
	/* control modes - set 8 bit chars */
	raw.c_iflag |= IUTF8;
#endif
	raw.c_cflag |= CS8;
	/* local modes - choing off, canonical off, no extended functions,
	 * no signal chars (^Z,^C) */
	raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG);
	/* control chars - set return condition: min number of bytes and timer.
	 * We want read to return every single byte, without timeout. */
	raw.c_cc[VMIN] = 1;
	raw.c_cc[VTIME] = 0; /* 1 byte, no timer */

	/* put terminal in raw mode after flushing */
	if (tcsetattr(fd, TCSADRAIN, &raw) < 0) {
		errno = ENOTTY;
		return -1;
	}
	rawmode = 1;
	return 0;
}

void Terminal::DisableRawMode() {
	int fd = STDIN_FILENO;
	/* Don't even check the return value as it's too late. */
	if (rawmode && tcsetattr(fd, TCSADRAIN, &orig_termios) != -1) {
		rawmode = 0;
	}
}

bool Terminal::IsMultiline() {
	return mlmode;
}

bool Terminal::IsAtty() {
	return isatty(STDIN_FILENO);
}

/* This function is called when linenoise() is called with the standard
 * input file descriptor not attached to a TTY. So for example when the
 * program using linenoise is called in pipe or with a file redirected
 * to its standard input. In this case, we want to be able to return the
 * line regardless of its length (by default we are limited to 4k). */
char *Terminal::EditNoTTY() {
	char *line = NULL;
	size_t len = 0, maxlen = 0;

	while (1) {
		if (len == maxlen) {
			if (maxlen == 0)
				maxlen = 16;
			maxlen *= 2;
			char *oldval = line;
			line = (char *)realloc(line, maxlen);
			if (line == NULL) {
				if (oldval)
					free(oldval);
				return NULL;
			}
		}
		int c = fgetc(stdin);
		if (c == EOF || c == '\n') {
			if (c == EOF && len == 0) {
				free(line);
				return NULL;
			} else {
				line[len] = '\0';
				return line;
			}
		} else {
			line[len] = c;
			len++;
		}
	}
}

/* This function calls the line editing function linenoiseEdit() using
 * the STDIN file descriptor set in raw mode. */
int Terminal::EditRaw(char *buf, size_t buflen, const char *prompt) {
	int count;

	if (buflen == 0) {
		errno = EINVAL;
		return -1;
	}

	if (Terminal::EnableRawMode() == -1) {
		return -1;
	}
	Linenoise l(STDIN_FILENO, STDOUT_FILENO, buf, buflen, prompt);
	count = l.Edit();
	Terminal::DisableRawMode();
	printf("\n");
	return count;
}

// returns true if there is more data available to read in a particular stream
int Terminal::HasMoreData(int fd) {
	fd_set rfds;
	FD_ZERO(&rfds);
	FD_SET(fd, &rfds);

	// no timeout: return immediately
	struct timeval tv;
	tv.tv_sec = 0;
	tv.tv_usec = 0;
	return select(1, &rfds, NULL, NULL, &tv);
}

/* ======================= Low level terminal handling ====================== */

/* Set if to use or not the multi line mode. */
void Terminal::SetMultiLine(int ml) {
	mlmode = ml;
}

static int parseInt(const char *s, int *offset = nullptr) {
	int result = 0;
	int idx;
	for (idx = 0; s[idx]; idx++) {
		char c = s[idx];
		if (c < '0' || c > '9') {
			break;
		}
		result = result * 10 + c - '0';
		if (result > 1000000) {
			result = 1000000;
		}
	}
	if (offset) {
		*offset = idx;
	}
	return result;
}

static int tryParseEnv(const char *env_var) {
	char *s;
	s = getenv(env_var);
	if (!s) {
		return 0;
	}
	return parseInt(s);
}

/* Use the ESC [6n escape sequence to query the cursor position
 * and return it. On error -1 is returned, on success the position of the
 * cursor. */
TerminalSize Terminal::GetCursorPosition() {
	int ifd = STDIN_FILENO;
	int ofd = STDOUT_FILENO;
	TerminalSize ws;

	char buf[32];
	unsigned int i = 0;

	/* Report cursor location */
	if (write(ofd, "\x1b[6n", 4) != 4) {
		return ws;
	}

	/* Read the response: ESC [ rows ; cols R */
	while (i < sizeof(buf) - 1) {
		if (read(ifd, buf + i, 1) != 1) {
			break;
		}
		if (buf[i] == 'R') {
			break;
		}
		i++;
	}
	buf[i] = '\0';

	/* Parse it. */
	if (buf[0] != ESC || buf[1] != '[') {
		return ws;
	}
	int offset = 2;
	int new_offset;
	ws.ws_row = parseInt(buf + offset, &new_offset);
	offset += new_offset;
	if (buf[offset] != ';') {
		return ws;
	}
	offset++;
	ws.ws_col = parseInt(buf + offset);
	return ws;
}

TerminalSize Terminal::TryMeasureTerminalSize() {
	int ofd = STDOUT_FILENO;
	/* ioctl() failed. Try to query the terminal itself. */
	TerminalSize start, result;

	/* Get the initial position so we can restore it later. */
	start = GetCursorPosition();
	if (!start.ws_col) {
		return result;
	}

	/* Go to bottom-right margin */
	if (write(ofd, "\x1b[999;999f", 10) != 10) {
		return result;
	}
	result = GetCursorPosition();
	if (!result.ws_col) {
		return result;
	}

	/* Restore position. */
	char seq[32];
	snprintf(seq, 32, "\x1b[%d;%df", start.ws_row, start.ws_col);
	if (write(ofd, seq, strlen(seq)) == -1) {
		/* Can't recover... */
	}
	return result;
}

/* Try to get the number of columns in the current terminal, or assume 80
 * if it fails. */
TerminalSize Terminal::GetTerminalSize() {
	TerminalSize result;

	// try ioctl first
	{
		struct winsize ws;
		ioctl(1, TIOCGWINSZ, &ws);
		result.ws_col = ws.ws_col;
		result.ws_row = ws.ws_row;
	}
	// try ROWS and COLUMNS env variables
	if (!result.ws_col) {
		result.ws_col = tryParseEnv("COLUMNS");
	}
	if (!result.ws_row) {
		result.ws_row = tryParseEnv("ROWS");
	}
	// if those fail measure the size by moving the cursor to the corner and fetching the position
	if (!result.ws_col || !result.ws_row) {
		TerminalSize measured_size = TryMeasureTerminalSize();
		Linenoise::Log("measured size col %d,row %d -- ", measured_size.ws_row, measured_size.ws_col);
		if (measured_size.ws_row) {
			result.ws_row = measured_size.ws_row;
		}
		if (measured_size.ws_col) {
			result.ws_col = measured_size.ws_col;
		}
	}
	// if all else fails use defaults (80,24)
	if (!result.ws_col) {
		result.ws_col = 80;
	}
	if (!result.ws_row) {
		result.ws_row = 24;
	}
	return result;
}

/* Clear the screen. Used to handle ctrl+l */
void Terminal::ClearScreen() {
	if (write(STDOUT_FILENO, "\x1b[H\x1b[2J", 7) <= 0) {
		/* nothing to do, just to avoid warning. */
	}
}

/* Beep, used for completion when there is nothing to complete or when all
 * the choices were already shown. */
void Terminal::Beep() {
	fprintf(stderr, "\x7");
	fflush(stderr);
}

EscapeSequence Terminal::ReadEscapeSequence(int ifd) {
	char seq[5];
	idx_t length = ReadEscapeSequence(ifd, seq);
	if (length == 0) {
		return EscapeSequence::INVALID;
	}
	Linenoise::Log("escape of length %d\n", length);
	switch (length) {
	case 1:
		if (seq[0] >= 'a' && seq[0] <= 'z') {
			return EscapeSequence(idx_t(EscapeSequence::ALT_A) + (seq[0] - 'a'));
		}
		if (seq[0] >= 'A' && seq[0] <= 'Z') {
			return EscapeSequence(idx_t(EscapeSequence::ALT_A) + (seq[0] - 'A'));
		}
		switch (seq[0]) {
		case BACKSPACE:
			return EscapeSequence::ALT_BACKSPACE;
		case ESC:
			return EscapeSequence::ESCAPE;
		case '<':
			return EscapeSequence::ALT_LEFT_ARROW;
		case '>':
			return EscapeSequence::ALT_RIGHT_ARROW;
		case '\\':
			return EscapeSequence::ALT_BACKSLASH;
		default:
			Linenoise::Log("unrecognized escape sequence of length 1 - %d\n", seq[0]);
			break;
		}
		break;
	case 2:
		if (seq[0] == 'O') {
			switch (seq[1]) {
			case 'A': /* Up */
				return EscapeSequence::UP;
			case 'B': /* Down */
				return EscapeSequence::DOWN;
			case 'C': /* Right */
				return EscapeSequence::RIGHT;
			case 'D': /* Left */
				return EscapeSequence::LEFT;
			case 'H': /* Home */
				return EscapeSequence::HOME;
			case 'F': /* End*/
				return EscapeSequence::END;
			case 'c':
				return EscapeSequence::ALT_F;
			case 'd':
				return EscapeSequence::ALT_B;
			default:
				Linenoise::Log("unrecognized escape sequence (O) %d\n", seq[1]);
				break;
			}
		} else if (seq[0] == '[') {
			switch (seq[1]) {
			case 'A': /* Up */
				return EscapeSequence::UP;
			case 'B': /* Down */
				return EscapeSequence::DOWN;
			case 'C': /* Right */
				return EscapeSequence::RIGHT;
			case 'D': /* Left */
				return EscapeSequence::LEFT;
			case 'H': /* Home */
				return EscapeSequence::HOME;
			case 'F': /* End*/
				return EscapeSequence::END;
			case 'Z': /* Shift Tab */
				return EscapeSequence::SHIFT_TAB;
			default:
				Linenoise::Log("unrecognized escape sequence (seq[1]) %d\n", seq[1]);
				break;
			}
		} else {
			Linenoise::Log("unrecognized escape sequence of length %d (%d %d)\n", length, seq[0], seq[1]);
		}
		break;
	case 3:
		if (seq[2] == '~') {
			switch (seq[1]) {
			case '1':
				return EscapeSequence::HOME;
			case '3': /* Delete key. */
				return EscapeSequence::DELETE;
			case '4':
			case '8':
				return EscapeSequence::END;
			default:
				Linenoise::Log("unrecognized escape sequence (~) %d\n", seq[1]);
				break;
			}
		} else if (seq[1] == '5' && seq[2] == 'C') {
			return EscapeSequence::ALT_F;
		} else if (seq[1] == '5' && seq[2] == 'D') {
			return EscapeSequence::ALT_B;
		} else {
			Linenoise::Log("unrecognized escape sequence of length %d\n", length);
		}
		break;
	case 5:
		if (memcmp(seq, "[1;5C", 5) == 0 || memcmp(seq, "[1;3C", 5) == 0) {
			// [1;5C: move word right
			return EscapeSequence::CTRL_MOVE_FORWARDS;
		} else if (memcmp(seq, "[1;5D", 5) == 0 || memcmp(seq, "[1;3D", 5) == 0) {
			// [1;5D: move word left
			return EscapeSequence::CTRL_MOVE_BACKWARDS;
		} else {
			Linenoise::Log("unrecognized escape sequence (;) %d\n", seq[1]);
		}
		break;
	default:
		Linenoise::Log("unrecognized escape sequence of length %d\n", length);
		break;
	}
	return EscapeSequence::UNKNOWN;
}

idx_t Terminal::ReadEscapeSequence(int ifd, char seq[]) {
	if (read(ifd, seq, 1) == -1) {
		return 0;
	}
	switch (seq[0]) {
	case 'O':
	case '[':
		// these characters have multiple bytes following them
		break;
	default:
		return 1;
	}
	if (read(ifd, seq + 1, 1) == -1) {
		return 0;
	}

	if (seq[0] != '[') {
		return 2;
	}
	if (seq[1] < '0' || seq[1] > '9') {
		return 2;
	}
	/* Extended escape, read additional byte. */
	if (read(ifd, seq + 2, 1) == -1) {
		return 0;
	}
	if (seq[2] == ';') {
		// read 2 extra bytes
		if (read(ifd, seq + 3, 2) == -1) {
			return 0;
		}
		return 5;
	} else {
		return 3;
	}
}

} // namespace duckdb
