/* Copyright (C) 2013 G.P. Halkes
   This program is free software: you can redistribute it and/or modify
   it under the terms of the GNU General Public License version 3, as
   published by the Free Software Foundation.

   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 <string.h>
#include <iconv.h>
#include <errno.h>

#include "write_stringlist.h"
#include "curses_handling.h"

/* FIXME:
	- allow specification of the Unicode full-width support level
*/

typedef enum {
	ACTION_NONE,
	ACTION_ATTR_OFF,
	ACTION_ATTR_ON,
	ACTION_CLEAR,
	ACTION_CR,
	ACTION_CUP_OFF,
	ACTION_CUP_ON,
	ACTION_CURSOR_INVIS,
	ACTION_CURSOR_NORMAL,
	ACTION_CURSOR_VVIS,
	ACTION_HOME,
	ACTION_RESTORE_CURSOR,
	ACTION_SAVE_CURSOR,
	ACTION_SCROLL_BCK,
	ACTION_SCROLL_FWD,
	ACTION_TAB
} action_t;

typedef struct {
	const char *name;
	char *str;
	action_t action;
	attr_t attr;
} terminfo_mapping_t;

static iconv_t iconv_handle;

static terminfo_mapping_t terminfo_mappings[] = {
	/* WARNING: do not change the order of the attributes below, or insert any
	   new items except for at the end. Otherwise, the REV_IDX, RMSO_IDX and
	   SMSO_IDX values below have to be adjusted accordingly. */
	{ "blink", NULL, ACTION_ATTR_ON, ATTR_BLINK },
	{ "bold", NULL, ACTION_ATTR_ON, ATTR_BOLD },
	{ "civis", NULL, ACTION_CURSOR_INVIS, 0 },
	{ "clear", NULL, ACTION_CLEAR, 0 },
	{ "cnorm", NULL, ACTION_CURSOR_NORMAL, 0 },
	{ "cr", NULL, ACTION_CR, 0 },
	{ "cvvis", NULL, ACTION_CURSOR_VVIS, 0 },
	{ "home", NULL, ACTION_HOME, 0 },
	{ "ht", NULL, ACTION_TAB, 0 },
	{ "hts", NULL, ACTION_NONE, 0 },
	{ "ind", NULL, ACTION_SCROLL_FWD, 0 },
	{ "invis", NULL, ACTION_ATTR_ON, ATTR_INVISIBLE },
	{ "op", NULL, ACTION_ATTR_OFF, ATTR_FG_MASK | ATTR_BG_MASK },
	{ "rc", NULL, ACTION_RESTORE_CURSOR, 0 },
	{ "rev", NULL, ACTION_ATTR_ON, ATTR_REVERSE },
	{ "ri", NULL, ACTION_SCROLL_BCK, 0 },
	{ "rmacs", NULL, ACTION_ATTR_OFF, ATTR_ACS },
	{ "rmam", NULL, ACTION_NONE, 0 },
	{ "rmcup", NULL, ACTION_CUP_OFF, 0 },
	{ "rmir", NULL, ACTION_NONE, 0 },
	{ "rmkx", NULL, ACTION_NONE, 0 },
	{ "rmso", NULL, ACTION_ATTR_OFF, ATTR_STAND_OUT },
	{ "rmul", NULL, ACTION_ATTR_OFF, ATTR_UNDERLINE },
	{ "sc", NULL, ACTION_SAVE_CURSOR, 0 },
	{ "smacs", NULL, ACTION_ATTR_ON, ATTR_ACS },
	{ "smam", NULL, ACTION_NONE, 0 },
	{ "smcup", NULL, ACTION_CUP_ON, 0 },
	{ "smir", NULL, ACTION_NONE, 0 },
	{ "smkx", NULL, ACTION_NONE, 0 },
	{ "smso", NULL, ACTION_ATTR_ON, ATTR_STAND_OUT },
	{ "smul", NULL, ACTION_ATTR_ON, ATTR_UNDERLINE }
};

#define REV_IDX 14
#define RMSO_IDX 21
#define SMSO_IDX 29

static const char *get_param(const char *str, int *param, td_bool *more) {
	char *endptr;
	long result = strtol(str, &endptr, 10);

	if (*endptr == ';') {
		endptr++;
		*more = td_true;
	} else if (*endptr >= 64 && *endptr <= 126) {
		endptr++;
		*more = td_false;
	} else {
		return NULL;
	}
	if (result > INT_MAX || result < 0)
		return NULL;
	*param = (int) result;
	return endptr;
}

static void do_action(window_t *win, action_t action, attr_t attr) {
	switch (action) {
		case ACTION_ATTR_OFF:
			win_set_default_attrs(win, win_get_default_attrs(win) & ~attr);
			break;
		case ACTION_ATTR_ON:
			win_set_default_attrs(win, win_get_default_attrs(win) | attr);
			break;
		case ACTION_CLEAR:
			win_set_paint(win, 0, 0);
			win_clrtobot(win);
			break;
		case ACTION_CR:
			win_set_paint(win, win_get_paint_y(win), 0);
			break;
		case ACTION_CUP_OFF:
			win_set_cup(win, td_false);
			break;
		case ACTION_CUP_ON:
			win_set_cup(win, td_true);
			break;
		case ACTION_CURSOR_INVIS:
			win_set_cursor(win, CURSOR_INVIS);
			break;
		case ACTION_CURSOR_NORMAL:
			win_set_cursor(win, CURSOR_NORM);
			break;
		case ACTION_CURSOR_VVIS:
			win_set_cursor(win, CURSOR_VVIS);
			break;
		case ACTION_HOME:
			win_set_paint(win, 0, 0);
			break;
		case ACTION_NONE:
			break;
		case ACTION_RESTORE_CURSOR:
			win_restore_cursor(win);
			break;
		case ACTION_SAVE_CURSOR:
			win_save_cursor(win);
			break;
		case ACTION_SCROLL_BCK:
			win_scroll_back(win);
			break;
		case ACTION_SCROLL_FWD:
			win_scroll(win);
			break;
		case ACTION_TAB: {
			int paint_x = win_get_paint_x(win);
			paint_x = (paint_x & ~0x7) + 8 - (paint_x & 0x7);
			if (paint_x >= win_get_width(win)) {
				int paint_y = win_get_paint_y(win);
				if (paint_y + 1 < win_get_height(win))
					win_set_paint(win, paint_y + 1, 0);
			} else {
				win_set_paint(win, win_get_paint_y(win), paint_x);
			}
			break;
		}
	}
}

static int handle_escape(char *str, int len, window_t *win) {
	int i;

	if (len == 0)
		return 0;

	for (i = 0; i < (int) ARRAY_SIZE(terminfo_mappings); i++) {
		size_t ti_len;
		if (terminfo_mappings[i].str != NULL && terminfo_mappings[i].str[0] == 27 &&
				(ti_len = strlen(terminfo_mappings[i].str + 1)) <= (size_t) len &&
				memcmp(terminfo_mappings[i].str + 1, str, ti_len) == 0)
		{
			do_action(win, terminfo_mappings[i].action, terminfo_mappings[i].attr);
			return ti_len;
		}
	}

	if (str[0] == '[') {
		int param = 1;
		td_bool more;
		for (i = 1; i < len && (str[i] < 64 || str[i] > 126) && str[i] != 27; i++) {}

		if (i == len)
			return 0;

		switch (str[i]) {
			case 'A': /* Cursor up (n or 1) */
				if (get_param(str + 1, &param, &more) == NULL || more)
					break;
				win_set_paint(win, win_get_paint_y(win) - param, win_get_paint_x(win));
				break;
			case 'B': /* Cursor down (n or 1) */
				if (get_param(str + 1, &param, &more) == NULL || more)
					break;
				win_set_paint(win, win_get_paint_y(win) + param, win_get_paint_x(win));
				break;
			case 'C': /* Cursor forward (n or 1) */
				if (get_param(str + 1, &param, &more) == NULL || more)
					break;
				win_set_paint(win, win_get_paint_y(win), win_get_paint_x(win) + param);
				break;
			case 'D': /* Cursor back (n or 1) */
				if (get_param(str + 1, &param, &more) == NULL || more)
					break;
				win_set_paint(win, win_get_paint_y(win), win_get_paint_x(win) - param);
				break;
			case 'E': /* Cursor next line (n or 1) (like 'B' but set x to 0) */
				if (get_param(str + 1, &param, &more) == NULL || more)
					break;
				win_set_paint(win, win_get_paint_y(win) + param, 0);
				break;
			case 'F': /* Cursor previous line (n or 1) (like 'B' but set x to 0) */
				if (get_param(str + 1, &param, &more) == NULL || more)
					break;
				win_set_paint(win, win_get_paint_y(win) - param, 0);
				break;
			case 'G': /* Cursor horizontal absolute */
				if (i == 2 || get_param(str + 1, &param, &more) == NULL || more)
					break;
				win_set_paint(win, win_get_paint_y(win), param - 1);
				break;
			case 'f': /* Same as cup */
			case 'H': { /* Cursor position */
				const char *new_str;
				int param2;

				if (!win_get_cup(win))
					break;

				if ((new_str = get_param(str + 1, &param, &more)) == NULL || !more ||
						get_param(new_str, &param2, &more) == NULL || more)
					break;
				win_set_paint(win, param - 1, param2 - 1);
				break;
			}
			case 'J': /* Erase data (0 or missing = to end, 1 = to beginning, 2 = entire screen) */
				if (param == 0) {
					win_clrtobot(win);
				} else if (param == 1) {
					win_clrtotop(win);
				} else if (param == 2) {
					win_clrtobot(win);
					win_clrtotop(win);
				}
				break;
			case 'K': /* Erase line (0 or missing = to end, 1 = to beginning, 2 = entire line) */
				param = 0;
				if (get_param(str + 1, &param, &more) == NULL || more)
					break;
				if (param == 0) {
					win_clrtoeol(win);
				} else if (param == 1) {
					win_clrtobol(win);
				} else if (param == 2) {
					win_clrtoeol(win);
					win_clrtobol(win);
				}
				break;
			case 'S': /* Scroll up (n or 1) */
				if (get_param(str + 1, &param, &more) == NULL || more)
					break;
				for (; param > 0; param--)
					win_scroll(win);
				break;
			case 'T': /* Scroll down (n or 1) */
				if (get_param(str + 1, &param, &more) == NULL || more)
					break;
				for (; param > 0; param--)
					win_scroll_back(win);
				break;
			case 'd': /* Vertical position absolute */
				if (i == 2 || get_param(str + 1, &param, &more) == NULL || more)
					break;
				win_set_paint(win, param - 1, win_get_paint_x(win));
				break;
			case 'm': /* Set graphics rendition (SGR) */
				param = 0;
				str++;
				while ((str = (char *) get_param(str, &param, &more)) != NULL) {
					if (param == 0) {
						/* Turn all attributes off, except ACS. At least xterm does not consider
						   it an "attribute" in the normal sense. */
						win_set_default_attrs(win, win_get_default_attrs(win) & ATTR_ACS);
					} else if (param == 1) {
						win_set_default_attrs(win, win_get_default_attrs(win) | ATTR_BOLD);
					} else if (param == 2) {
						win_set_default_attrs(win, win_get_default_attrs(win) | ATTR_DIM);
					} else if (param == 4) {
						win_set_default_attrs(win, win_get_default_attrs(win) | ATTR_UNDERLINE);
					} else if (param == 5) {
						win_set_default_attrs(win, win_get_default_attrs(win) | ATTR_BLINK);
					} else if (param == 7) {
						win_set_default_attrs(win, win_get_default_attrs(win) | ATTR_REVERSE);
					} else if (param == 8) {
						win_set_default_attrs(win, win_get_default_attrs(win) | ATTR_INVISIBLE);
					} else if (param == 21) {
						win_set_default_attrs(win, win_get_default_attrs(win) & ~ATTR_BOLD);
					} else if (param == 22) {
						win_set_default_attrs(win, win_get_default_attrs(win) & ~(ATTR_BOLD | ATTR_DIM));
					} else if (param == 24) {
						win_set_default_attrs(win, win_get_default_attrs(win) & ~ATTR_UNDERLINE);
					} else if (param == 25) {
						win_set_default_attrs(win, win_get_default_attrs(win) & ~ATTR_BLINK);
					} else if (param == 27) {
						win_set_default_attrs(win, win_get_default_attrs(win) & ~ATTR_REVERSE);
					} else if (param == 28) {
						win_set_default_attrs(win, win_get_default_attrs(win) & ~ATTR_INVISIBLE);
					} else if (param >= 30 && param < 38) {
						win_set_default_attrs(win, (win_get_default_attrs(win) & ~ATTR_FG_MASK) | ATTR_FG(param - 30));
					} else if (param == 38) {
						if ((str = (char *) get_param(str, &param, &more)) == NULL)
							break;
						if (param == 5) {
							if (!more || (str = (char *) get_param(str, &param, &more)) == NULL)
								break;
							if (param < 256)
								win_set_default_attrs(win, (win_get_default_attrs(win) & ~ATTR_FG_MASK) | ATTR_FG(param));
						}
					} else if (param == 39) {
						win_set_default_attrs(win, win_get_default_attrs(win) & ~ATTR_FG_MASK);
					} else if (param >= 40 && param < 48) {
						win_set_default_attrs(win, (win_get_default_attrs(win) & ~ATTR_BG_MASK) | ATTR_BG(param - 40));
					} else if (param == 48) {
						if ((str = (char *) get_param(str, &param, &more)) == NULL)
							break;
						if (param == 5) {
							if (!more || (str = (char *) get_param(str, &param, &more)) == NULL)
								break;
							if (param < 256)
								win_set_default_attrs(win, (win_get_default_attrs(win) & ~ATTR_BG_MASK) | ATTR_BG(param));
						}
					} else if (param == 49) {
						win_set_default_attrs(win, win_get_default_attrs(win) & ~ATTR_BG_MASK);
					}
					param = 0;
					/* Retrieving more parameters may have exhausted the string. */
					if (!more)
						break;
				}

				break;
			case 'n': /* With parameter 6: device status report */
				break;
			default:
				break;
		}
		return i + 1;
	} else if (str[0] == ']') {
		/* OSC (operating system control). These may be used to set the title of the terminal etc. */
		for (i = 1; i < len; i++) {
			if (str[i] == 7)
				return i + 1;
			if (str[i] == 27 && i + 1 < len && str[i] == '\\')
				return i + 2;
		}
		return 0;
	}


	return 0;
}

static int write_text(window_t *win, int start, int end, td_bool force) {
	char output[1024], *output_ptr;
	char *input_ptr = win->output_buffer.data + start;
	size_t inbytes_left = end - start, outbytes_left;
	size_t result;
	int saved_errno;

	if (iconv_handle == (iconv_t) -1) {
		win_addnstr(win, win->output_buffer.data + start, inbytes_left);
		return 0;
	}

	while (td_true) {
		output_ptr = output;
		outbytes_left = sizeof(output);
		result = iconv(iconv_handle, &input_ptr, &inbytes_left, &output_ptr, &outbytes_left);
		saved_errno = errno;
		if (outbytes_left != sizeof(output))
			win_addnstr(win, output, sizeof(output) - outbytes_left);

		if (result == (size_t) -1) {
			if (saved_errno == EILSEQ) {
				input_ptr++;
				inbytes_left--;
				win_addnstr(win, "\357\277\275", 3);
			} else if (saved_errno == EINVAL) {
				if (force) {
					inbytes_left = 0;
					outbytes_left = 0;
					iconv(iconv_handle, NULL, &inbytes_left, NULL, &outbytes_left);
					win_addnstr(win, "\357\277\275", 3);
					return 0;
				} else {
					return (int) inbytes_left;
				}
			} else if (saved_errno != E2BIG) {
				fatal("Error in character-set conversion\n");
			}
		} else {
			return 0;
		}
	}
}

static void parse_output_buffer(window_t *win) {
	int i, j;
	int start_normal;
	int handled;

	start_normal = 0;
	for (i = 0; i < win->output_buffer.length; i++) {
		if (win->output_buffer.data[i] < 32 && win->output_buffer.data[i] >= 0) {
			if (i != start_normal)
				write_text(win, start_normal, i, td_true);

			if (win->output_buffer.data[i] == 27) {
				if ((handled = handle_escape(win->output_buffer.data + i + 1, win->output_buffer.length - i, win)) == 0) {
					win->output_buffer.length -= i;
					memmove(win->output_buffer.data, win->output_buffer.data + i, win->output_buffer.length);
					return;
				}
				i += handled;
			} else {
				/* Check single character, non-escape terminfo mappings */
				for (j = 0; j < (int) ARRAY_SIZE(terminfo_mappings); j++) {
					if (terminfo_mappings[j].str != NULL && terminfo_mappings[j].str[1] == 0 &&
							terminfo_mappings[j].str[0] == win->output_buffer.data[i])
					{
						do_action(win, terminfo_mappings[j].action, terminfo_mappings[j].attr);
						break;
					}
				}
				/* If the terminfo mappings didn't yield a result, check for
				   other standard control characters. */
				if (j >= (int) ARRAY_SIZE(terminfo_mappings)) {
					switch (win->output_buffer.data[i]) {
						case 8: /* Backspace */
							win_set_paint(win, win_get_paint_y(win), win_get_paint_x(win) - 1);
							break;
						case 9: /* Tab */
							do_action(win, ACTION_TAB, 0);
							break;
						case 10: /* NL */
							if (win_get_paint_y(win) == win_get_height(win) - 1)
								win_scroll(win);
							else
								win_set_paint(win, win_get_paint_y(win) + 1, win_get_paint_x(win));
							break;
						case 13: /* CR */
							do_action(win, ACTION_CR, 0);
							break;
						default:
							break;
					}
				}
			}
			start_normal = i + 1;
		}
	}
	if (start_normal != i) {
		int to_save = write_text(win, start_normal, win->output_buffer.length, td_false);
		memmove(win->output_buffer.data, win->output_buffer.data + win->output_buffer.length - to_save, to_save);
		win->output_buffer.length = to_save;
	} else {
		win->output_buffer.length = 0;
	}
}

void win_write_data(window_t *win, char *data, size_t length) {
	if (!ensure_space(&win->output_buffer, length))
		fatal("Out of memory\n");
	memcpy(win->output_buffer.data + win->output_buffer.length, data, length);
	win->output_buffer.length += length;
	parse_output_buffer(win);
}

void win_write_stringlist(window_t *win, StringListNode *string) {
	for (; string != NULL; string = string->next)
		win_write_data(win, string->string, string->length);
}

void setup_ti_strings(const char *term, const char *lang) {
	size_t i;
	const char *codeset;

	call_setupterm(term);

	for (i = 0; i < ARRAY_SIZE(terminfo_mappings); i++)
		terminfo_mappings[i].str = get_ti_string(terminfo_mappings[i].name);

	if (strcmp(terminfo_mappings[REV_IDX].str, terminfo_mappings[SMSO_IDX].str) == 0) {
		free(terminfo_mappings[SMSO_IDX].str);
		free(terminfo_mappings[RMSO_IDX].str);
		terminfo_mappings[SMSO_IDX].str = NULL;
		terminfo_mappings[RMSO_IDX].str = NULL;
	}

	win_bce = get_ti_bool("bce") != 0;

	/* Initialize the character-set convertion, based on the current LANG setting. */
	if (lang == NULL)
		iconv_handle = (iconv_t) -1;
	else if ((codeset = strchr(lang, '.')) != NULL)
		iconv_handle = iconv_open("UTF-8", codeset + 1);
	else if (strcmp(lang, "C") == 0 || strcmp(lang, "POSIX") == 0)
		iconv_handle = iconv_open("UTF-8", "ANSI_X3.4-1968");
	else
		iconv_handle = (iconv_t) -1;
}
