// SPDX-License-Identifier: Apache-2.0
/*
 * Copyright (C) 2013 Intel Corporation
 *
 */

#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <stdbool.h>
#include <termios.h>
#include <stdlib.h>

#include "terminal.h"
#include "history.h"

/*
 * Character sequences recognized by code in this file
 * Leading ESC 0x1B is not included
 */
#define SEQ_INSERT "[2~"
#define SEQ_DELETE "[3~"
#define SEQ_HOME   "OH"
#define SEQ_END    "OF"
#define SEQ_PGUP   "[5~"
#define SEQ_PGDOWN "[6~"
#define SEQ_LEFT   "[D"
#define SEQ_RIGHT  "[C"
#define SEQ_UP     "[A"
#define SEQ_DOWN   "[B"
#define SEQ_STAB   "[Z"
#define SEQ_M_n    "n"
#define SEQ_M_p    "p"
#define SEQ_CLEFT  "[1;5D"
#define SEQ_CRIGHT "[1;5C"
#define SEQ_CUP    "[1;5A"
#define SEQ_CDOWN  "[1;5B"
#define SEQ_SLEFT  "[1;2D"
#define SEQ_SRIGHT "[1;2C"
#define SEQ_SUP    "[1;2A"
#define SEQ_SDOWN  "[1;2B"
#define SEQ_MLEFT  "[1;3D"
#define SEQ_MRIGHT "[1;3C"
#define SEQ_MUP    "[1;3A"
#define SEQ_MDOWN  "[1;3B"

#define KEY_SEQUENCE(k) { KEY_##k, SEQ_##k }
struct ansii_sequence {
	int code;
	const char *sequence;
};

/* Table connects single int key codes with character sequences */
static const struct ansii_sequence ansii_sequnces[] = {
	KEY_SEQUENCE(INSERT),
	KEY_SEQUENCE(DELETE),
	KEY_SEQUENCE(HOME),
	KEY_SEQUENCE(END),
	KEY_SEQUENCE(PGUP),
	KEY_SEQUENCE(PGDOWN),
	KEY_SEQUENCE(LEFT),
	KEY_SEQUENCE(RIGHT),
	KEY_SEQUENCE(UP),
	KEY_SEQUENCE(DOWN),
	KEY_SEQUENCE(CLEFT),
	KEY_SEQUENCE(CRIGHT),
	KEY_SEQUENCE(CUP),
	KEY_SEQUENCE(CDOWN),
	KEY_SEQUENCE(SLEFT),
	KEY_SEQUENCE(SRIGHT),
	KEY_SEQUENCE(SUP),
	KEY_SEQUENCE(SDOWN),
	KEY_SEQUENCE(MLEFT),
	KEY_SEQUENCE(MRIGHT),
	KEY_SEQUENCE(MUP),
	KEY_SEQUENCE(MDOWN),
	KEY_SEQUENCE(STAB),
	KEY_SEQUENCE(M_p),
	KEY_SEQUENCE(M_n),
	{ 0, NULL }
};

#define KEY_SEQUNCE_NOT_FINISHED -1
#define KEY_C_C 3
#define KEY_C_D 4
#define KEY_C_L 12

#define isseqence(c) ((c) == 0x1B)

/*
 * Number of characters that consist of ANSI sequence
 * Should not be less then longest string in ansi_sequences
 */
#define MAX_ASCII_SEQUENCE 10

static char current_sequence[MAX_ASCII_SEQUENCE];
static int current_sequence_len = -1;

/* single line typed by user goes here */
static char line_buf[LINE_BUF_MAX];
/* index of cursor in input line */
static int line_buf_ix = 0;
/* current length of input line */
static int line_len = 0;

/* line index used for fetching lines from history */
static int line_index = 0;

static char prompt_buf[10] = "> ";
static const char *const noprompt = "";
static const char *current_prompt = prompt_buf;
static const char *prompt = prompt_buf;
/*
 * Moves cursor to right or left
 *
 * n - positive - moves cursor right
 * n - negative - moves cursor left
 */
static void terminal_move_cursor(int n)
{
	if (n < 0) {
		for (; n < 0; n++)
			putchar('\b');
	} else if (n > 0) {
		printf("%*s", n, line_buf + line_buf_ix);
	}
}

/* Draw command line */
void terminal_draw_command_line(void)
{
	/*
	 * this needs to be checked here since line_buf is not cleared
	 * before parsing event though line_len and line_buf_ix are
	 */
	if (line_len > 0)
		printf("%s%s", prompt, line_buf);
	else
		printf("%s", prompt);

	/* move cursor to it's place */
	terminal_move_cursor(line_buf_ix - line_len);
}

/* inserts string into command line at cursor position */
void terminal_insert_into_command_line(const char *p)
{
	int len = strlen(p);

	if (line_len == line_buf_ix) {
		strcat(line_buf, p);
		printf("%s", p);
		line_len = line_len + len;
		line_buf_ix = line_len;
	} else {
		memmove(line_buf + line_buf_ix + len,
			line_buf + line_buf_ix, line_len - line_buf_ix + 1);
		memmove(line_buf + line_buf_ix, p, len);
		printf("%s", line_buf + line_buf_ix);
		line_buf_ix += len;
		line_len += len;
		terminal_move_cursor(line_buf_ix - line_len);
	}
}

/* Prints string and redraws command line */
int terminal_print(const char *format, ...)
{
	va_list args;
	int ret;

	va_start(args, format);

	ret = terminal_vprint(format, args);

	va_end(args);
	return ret;
}

/* Prints string and redraws command line */
int terminal_vprint(const char *format, va_list args)
{
	int ret;

	printf("\r%*s\r", (int) line_len + 1, " ");

	ret = vprintf(format, args);

	terminal_draw_command_line();

	fflush(stdout);

	return ret;
}

/*
 * Call this when text in line_buf was changed
 * and line needs to be redrawn
 */
static void terminal_line_replaced(void)
{
	int len = strlen(line_buf);

	/* line is shorter that previous */
	if (len < line_len) {
		/* if new line is shorter move cursor to end of new end */
		while (line_buf_ix > len) {
			putchar('\b');
			line_buf_ix--;
		}

		/* If cursor was not at the end, move it to the end */
		if (line_buf_ix < line_len)
			printf("%.*s", line_len - line_buf_ix,
					line_buf + line_buf_ix);
		/* over write end of previous line */
		while (line_len >= len++)
			putchar(' ');
	}

	/* draw new line */
	printf("\r%s%s", prompt, line_buf);
	/* set up indexes to new line */
	line_len = strlen(line_buf);
	line_buf_ix = line_len;
	fflush(stdout);
}

static void terminal_clear_line(void)
{
	line_buf[0] = '\0';
	terminal_line_replaced();
}

static void terminal_clear_screen(void)
{
	line_buf[0] = '\0';
	line_buf_ix = 0;
	line_len = 0;

	printf("\x1b[2J\x1b[1;1H%s", prompt);
}

static void terminal_delete_char(void)
{
	/* delete character under cursor if not at the very end */
	if (line_buf_ix >= line_len)
		return;
	/*
	 * Prepare buffer with one character missing
	 * trailing 0 is moved
	 */
	line_len--;
	memmove(line_buf + line_buf_ix, line_buf + line_buf_ix + 1,
						line_len - line_buf_ix + 1);
	/* print rest of line from current cursor position */
	printf("%s \b", line_buf + line_buf_ix);
	/* move back cursor */
	terminal_move_cursor(line_buf_ix - line_len);
}

/*
 * Function tries to replace current line with specified line in history
 * new_line_index - new line to show, -1 to show oldest
 */
static void terminal_get_line_from_history(int new_line_index)
{
	new_line_index = history_get_line(new_line_index,
						line_buf, LINE_BUF_MAX);

	if (new_line_index >= 0) {
		terminal_line_replaced();
		line_index = new_line_index;
	}
}

/*
 * Function searches history back or forward for command line that starts
 * with characters up to cursor position
 *
 * back - true - searches backward
 * back - false - searches forward (more recent commands)
 */
static void terminal_match_hitory(bool back)
{
	char buf[line_buf_ix + 1];
	int line;
	int matching_line = -1;
	int dir = back ? 1 : -1;

	line = line_index + dir;
	while (matching_line == -1 && line >= 0) {
		int new_line_index;

		new_line_index = history_get_line(line, buf, line_buf_ix + 1);
		if (new_line_index < 0)
			break;

		if (0 == strncmp(line_buf, buf, line_buf_ix))
			matching_line = line;
		line += dir;
	}

	if (matching_line >= 0) {
		int pos = line_buf_ix;
		terminal_get_line_from_history(matching_line);
		/* move back to cursor position to original place */
		line_buf_ix = pos;
		terminal_move_cursor(pos - line_len);
	}
}

/*
 * Converts terminal character sequences to single value representing
 * keyboard keys
 */
static int terminal_convert_sequence(int c)
{
	int i;

	/* Not in sequence yet? */
	if (current_sequence_len == -1) {
		/* Is ansi sequence detected by 0x1B ? */
		if (isseqence(c)) {
			current_sequence_len++;
			return KEY_SEQUNCE_NOT_FINISHED;
		}

		return c;
	}

	/* Inside sequence */
	current_sequence[current_sequence_len++] = c;
	current_sequence[current_sequence_len] = '\0';
	for (i = 0; ansii_sequnces[i].code; ++i) {
		/* Matches so far? */
		if (0 != strncmp(current_sequence, ansii_sequnces[i].sequence,
							current_sequence_len))
			continue;

		/* Matches as a whole? */
		if (ansii_sequnces[i].sequence[current_sequence_len] == 0) {
			current_sequence_len = -1;
			return ansii_sequnces[i].code;
		}

		/* partial match (not whole sequence yet) */
		return KEY_SEQUNCE_NOT_FINISHED;
	}

	terminal_print("ansi char 0x%X %c\n", c);
	/*
	 * Sequence does not match
	 * mark that no in sequence any more, return char
	 */
	current_sequence_len = -1;
	return c;
}

typedef void (*terminal_action)(int c, line_callback process_line);

#define TERMINAL_ACTION(n) \
	static void n(int c, void (*process_line)(char *line))

TERMINAL_ACTION(terminal_action_null)
{
}

/* Mapping between keys and function */
typedef struct {
	int key;
	terminal_action func;
} KeyAction;

int action_keys[] = {
	KEY_SEQUNCE_NOT_FINISHED,
	KEY_LEFT,
	KEY_RIGHT,
	KEY_HOME,
	KEY_END,
	KEY_DELETE,
	KEY_CLEFT,
	KEY_CRIGHT,
	KEY_SUP,
	KEY_SDOWN,
	KEY_UP,
	KEY_DOWN,
	KEY_BACKSPACE,
	KEY_INSERT,
	KEY_PGUP,
	KEY_PGDOWN,
	KEY_CUP,
	KEY_CDOWN,
	KEY_SLEFT,
	KEY_SRIGHT,
	KEY_MLEFT,
	KEY_MRIGHT,
	KEY_MUP,
	KEY_MDOWN,
	KEY_STAB,
	KEY_M_n,
	KEY_M_p,
	KEY_C_C,
	KEY_C_D,
	KEY_C_L,
	'\t',
	'\r',
	'\n',
};

#define NELEM(x) ((int) (sizeof(x) / sizeof((x)[0])))

/*
 * current_actions holds all recognizable kes and actions for them
 * additional element (index 0) is used for default action
 */
static KeyAction current_actions[NELEM(action_keys) + 1];

/* KeyAction comparator by key, for qsort and bsearch */
static int KeyActionKeyCompare(const void *a, const void *b)
{
	return ((const KeyAction *) a)->key - ((const KeyAction *) b)->key;
}

/* Find action by key, NULL if no action for this key */
static KeyAction *terminal_get_action(int key)
{
	KeyAction a = { .key = key };

	return bsearch(&a, current_actions + 1, NELEM(action_keys), sizeof(a),
							KeyActionKeyCompare);
}

/* Sets new set of actions to use */
static void terminal_set_actions(const KeyAction *actions)
{
	int i;

	/* Make map with empty function for every key */
	for (i = 0; i < NELEM(action_keys); ++i) {
		/*
		 * + 1 due to 0 index reserved for default action that is
		 * called for non mapped key
		 */
		current_actions[i + 1].key = action_keys[i];
		current_actions[i + 1].func = terminal_action_null;
	}

	/* Sort action from 1 (index 0 - default action) */
	qsort(current_actions + 1, NELEM(action_keys), sizeof(KeyAction),
							KeyActionKeyCompare);
	/* Set default action (first in array) */
	current_actions[0] = *actions++;

	/* Copy rest of actions into their places */
	for (; actions->key; ++actions) {
		KeyAction *place = terminal_get_action(actions->key);

		if (place)
			place->func = actions->func;
	}
}

TERMINAL_ACTION(terminal_action_left)
{
	/* if not at the beginning move to previous character */
	if (line_buf_ix <= 0)
		return;
	line_buf_ix--;
	terminal_move_cursor(-1);
}

TERMINAL_ACTION(terminal_action_right)
{
	/*
	 * If not at the end, just print current character
	 * and modify position
	 */
	if (line_buf_ix < line_len)
		putchar(line_buf[line_buf_ix++]);
}

TERMINAL_ACTION(terminal_action_home)
{
	/* move to beginning of line and update position */
	printf("\r%s", prompt);
	line_buf_ix = 0;
}

TERMINAL_ACTION(terminal_action_end)
{
	/* if not at the end of line */
	if (line_buf_ix < line_len) {
		/* print everything from cursor */
		printf("%s", line_buf + line_buf_ix);
		/* just modify current position */
		line_buf_ix = line_len;
	}
}

TERMINAL_ACTION(terminal_action_del)
{
	terminal_delete_char();
}

TERMINAL_ACTION(terminal_action_word_left)
{
	int old_pos;
	/*
	 * Move by word left
	 *
	 * Are we at the beginning of line?
	 */
	if (line_buf_ix <= 0)
		return;

	old_pos = line_buf_ix;
	line_buf_ix--;
	/* skip spaces left */
	while (line_buf_ix && isspace(line_buf[line_buf_ix]))
		line_buf_ix--;

	/* skip all non spaces to the left */
	while (line_buf_ix > 0 &&
			!isspace(line_buf[line_buf_ix - 1]))
		line_buf_ix--;

	/* move cursor to new position */
	terminal_move_cursor(line_buf_ix - old_pos);
}

TERMINAL_ACTION(terminal_action_word_right)
{
	int old_pos;
	/*
	 * Move by word right
	 *
	 * are we at the end of line?
	 */
	if (line_buf_ix >= line_len)
		return;

	old_pos = line_buf_ix;
	/* skip all spaces */
	while (line_buf_ix < line_len && isspace(line_buf[line_buf_ix]))
		line_buf_ix++;

	/* skip all non spaces */
	while (line_buf_ix < line_len && !isspace(line_buf[line_buf_ix]))
		line_buf_ix++;
	/*
	 * Move cursor to right by printing text
	 * between old cursor and new
	 */
	if (line_buf_ix > old_pos)
		printf("%.*s", (int) (line_buf_ix - old_pos),
							line_buf + old_pos);
}

TERMINAL_ACTION(terminal_action_history_begin)
{
	terminal_get_line_from_history(-1);
}

TERMINAL_ACTION(terminal_action_history_end)
{
	if (line_index > 0)
		terminal_get_line_from_history(0);
}

TERMINAL_ACTION(terminal_action_history_up)
{
	terminal_get_line_from_history(line_index + 1);
}

TERMINAL_ACTION(terminal_action_history_down)
{
	if (line_index > 0)
		terminal_get_line_from_history(line_index - 1);
}

TERMINAL_ACTION(terminal_action_tab)
{
	/* tab processing */
	process_tab(line_buf, line_buf_ix);
}


TERMINAL_ACTION(terminal_action_backspace)
{
	if (line_buf_ix <= 0)
		return;

	if (line_buf_ix == line_len) {
		printf("\b \b");
		line_len = --line_buf_ix;
		line_buf[line_len] = 0;
	} else {
		putchar('\b');
		line_buf_ix--;
		line_len--;
		memmove(line_buf + line_buf_ix,
				line_buf + line_buf_ix + 1,
				line_len - line_buf_ix + 1);
		printf("%s \b", line_buf + line_buf_ix);
		terminal_move_cursor(line_buf_ix - line_len);
	}
}

TERMINAL_ACTION(terminal_action_find_history_forward)
{
	/* Search history forward */
	terminal_match_hitory(false);
}

TERMINAL_ACTION(terminal_action_find_history_backward)
{
	/* Search history forward */
	terminal_match_hitory(true);
}

TERMINAL_ACTION(terminal_action_ctrl_c)
{
	terminal_clear_line();
}

TERMINAL_ACTION(terminal_action_ctrl_d)
{
	if (line_len > 0) {
		terminal_delete_char();
	} else  {
		puts("");
		exit(0);
	}
}

TERMINAL_ACTION(terminal_action_clear_screen)
{
	terminal_clear_screen();
}

TERMINAL_ACTION(terminal_action_enter)
{
	/*
	 * On new line add line to history
	 * forget history position
	 */
	history_add_line(line_buf);
	line_len = 0;
	line_buf_ix = 0;
	line_index = -1;
	/* print new line */
	putchar(c);
	prompt = noprompt;
	process_line(line_buf);
	/* clear current line */
	line_buf[0] = '\0';
	prompt = current_prompt;
	printf("%s", prompt);
}

TERMINAL_ACTION(terminal_action_default)
{
	char str[2] = { c, 0 };

	if (!isprint(c))
		/*
		 * TODO: remove this print once all meaningful sequences
		 * are identified
		 */
		printf("char-0x%02x\n", c);
	else if (line_buf_ix < LINE_BUF_MAX - 1)
		terminal_insert_into_command_line(str);
}

/* Callback to call when user hit enter during prompt for */
static line_callback prompt_callback;

static KeyAction normal_actions[] = {
	{ 0, terminal_action_default },
	{ KEY_LEFT, terminal_action_left },
	{ KEY_RIGHT, terminal_action_right },
	{ KEY_HOME, terminal_action_home },
	{ KEY_END, terminal_action_end },
	{ KEY_DELETE, terminal_action_del },
	{ KEY_CLEFT, terminal_action_word_left },
	{ KEY_CRIGHT, terminal_action_word_right },
	{ KEY_SUP, terminal_action_history_begin },
	{ KEY_SDOWN, terminal_action_history_end },
	{ KEY_UP, terminal_action_history_up },
	{ KEY_DOWN, terminal_action_history_down },
	{ '\t', terminal_action_tab },
	{ KEY_BACKSPACE, terminal_action_backspace },
	{ KEY_M_n, terminal_action_find_history_forward },
	{ KEY_M_p, terminal_action_find_history_backward },
	{ KEY_C_C, terminal_action_ctrl_c },
	{ KEY_C_D, terminal_action_ctrl_d },
	{ KEY_C_L, terminal_action_clear_screen },
	{ '\r', terminal_action_enter },
	{ '\n', terminal_action_enter },
	{ 0, NULL },
};

TERMINAL_ACTION(terminal_action_answer)
{
	putchar(c);

	terminal_set_actions(normal_actions);
	/* Restore default prompt */
	current_prompt = prompt_buf;

	/* No prompt for prints */
	prompt = noprompt;
	line_buf_ix = 0;
	line_len = 0;
	/* Call user function with what was typed */
	prompt_callback(line_buf);

	line_buf[0] = 0;
	/* promot_callback could change current_prompt */
	prompt = current_prompt;

	printf("%s", prompt);
}

TERMINAL_ACTION(terminal_action_prompt_ctrl_c)
{
	printf("^C\n");
	line_buf_ix = 0;
	line_len = 0;
	line_buf[0] = 0;

	current_prompt = prompt_buf;
	prompt = current_prompt;
	terminal_set_actions(normal_actions);

	printf("%s", prompt);
}

static KeyAction prompt_actions[] = {
	{ 0, terminal_action_default },
	{ KEY_LEFT, terminal_action_left },
	{ KEY_RIGHT, terminal_action_right },
	{ KEY_HOME, terminal_action_home },
	{ KEY_END, terminal_action_end },
	{ KEY_DELETE, terminal_action_del },
	{ KEY_CLEFT, terminal_action_word_left },
	{ KEY_CRIGHT, terminal_action_word_right },
	{ KEY_BACKSPACE, terminal_action_backspace },
	{ KEY_C_C, terminal_action_prompt_ctrl_c },
	{ KEY_C_D, terminal_action_ctrl_d },
	{ '\r', terminal_action_answer },
	{ '\n', terminal_action_answer },
	{ 0, NULL },
};

void terminal_process_char(int c, line_callback process_line)
{
	KeyAction *a;

	c = terminal_convert_sequence(c);

	/* Get action for this key */
	a = terminal_get_action(c);

	/* No action found, get default one */
	if (a == NULL)
		a = &current_actions[0];

	a->func(c, process_line);
	fflush(stdout);
}

void terminal_prompt_for(const char *s, line_callback process_line)
{
	current_prompt = s;
	if (prompt != noprompt) {
		prompt = s;
		terminal_clear_line();
	}
	prompt_callback = process_line;
	terminal_set_actions(prompt_actions);
}

static struct termios origianl_tios;

static void terminal_cleanup(void)
{
	tcsetattr(0, TCSANOW, &origianl_tios);
}

void terminal_setup(void)
{
	struct termios tios;

	terminal_set_actions(normal_actions);

	tcgetattr(0, &origianl_tios);
	tios = origianl_tios;

	/*
	 * Turn off echo since all editing is done by hand,
	 * Ctrl-c handled internally
	 */
	tios.c_lflag &= ~(ICANON | ECHO | BRKINT | IGNBRK);
	tcsetattr(0, TCSANOW, &tios);

	/* Restore terminal at exit */
	atexit(terminal_cleanup);

	printf("%s", prompt);
	fflush(stdout);
}
