/* Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

/* Console module for Chrome EC */

#include "clock.h"
#include "console.h"
#ifdef CONFIG_EXPERIMENTAL_CONSOLE
#include "crc8.h"
#endif /* defined(CONFIG_EXPERIMENTAL_CONSOLE) */
#include "link_defs.h"
#include "system.h"
#include "task.h"
#include "uart.h"
#include "usb_console.h"
#include "util.h"

#define MAX_ARGS_PER_COMMAND 10

#define PROMPT "> "

#ifdef CONFIG_EXPERIMENTAL_CONSOLE
#define EC_SYN 0xEC
#define EC_ACK 0xC0
#endif /* defined(CONFIG_EXPERIMENTAL_CONSOLE) */

/* ASCII control character; for example, CTRL('C') = ^C */
#define CTRL(c) ((c) - '@')

#ifdef CONFIG_CONSOLE_HISTORY
/* History buffers */
static char history[CONFIG_CONSOLE_HISTORY][CONFIG_CONSOLE_INPUT_LINE_SIZE];
static int history_next, history_pos;
#endif

/* Current console command line */
static char input_buf[CONFIG_CONSOLE_INPUT_LINE_SIZE];

/* Length of current line */
static int input_len;

/* Cursor position in current line */
static int input_pos;

/* Was last received character a carriage return? */
static int last_rx_was_cr;

#ifndef CONFIG_EXPERIMENTAL_CONSOLE
/* State of input escape code */
static enum {
	ESC_OUTSIDE,   /* Not in escape code */
	ESC_START,     /* Got ESC */
	ESC_BAD,       /* Bad escape sequence */
	ESC_BRACKET,   /* Got ESC [ */
	ESC_BRACKET_1, /* Got ESC [ 1 */
	ESC_BRACKET_3, /* Got ESC [ 3 */
	ESC_O,         /* Got ESC O */
} esc_state;
#endif /* !defined(CONFIG_EXPERIMENTAL_CONSOLE) */

/* Extended key code values, from multi-byte escape sequences */
enum extended_key_code {
	KEY_UP_ARROW = 0x100,
	KEY_DOWN_ARROW,
	KEY_RIGHT_ARROW,
	KEY_LEFT_ARROW,
	KEY_END,
	KEY_HOME,
	KEY_DEL
};

/**
 * Split a line of input into words.
 *
 * @param input		Input line; modified to add nulls after each word.
 * @param argc		Destination for number of words.
 * @param argv		Destination array for pointers to words; must be at
 *			least MAX_ARGS_PER_COMMAND entries long.
 *
 * @return EC_SUCCESS.  If more than MAX_ARGS_PER_COMMAND words are found,
 *	discards the excess and returns EC_ERROR_OVERFLOW.
 */
static int split_words(char *input, int *argc, char **argv)
{
	char *c;
	int in_word = 0;
	int in_line = 1;

	/* Parse input into words */
	*argc = 0;
	for (c = input; in_line; c++) {
		if (!*c)
			in_line = 0;
		if (isspace(*c) || !*c) {
			if (in_word) {
				/* Ending a word */
				*c = '\0';
				++*argc;
				in_word = 0;
			}
		} else if (*c == '#') {
			/* Comments start with hash and go to end of line */
			break;
		} else if (!in_word) {
			/* Starting a new word */
			if (*argc >= MAX_ARGS_PER_COMMAND)
				return EC_ERROR_OVERFLOW;

			argv[*argc] = c;
			in_word = 1;
		}
	}
	return EC_SUCCESS;
}

/**
 * Find a command by name.
 *
 * Allows partial matches, as long as the partial match is unique to one
 * command.  So "foo" will match "foobar" as long as there isn't also a
 * command "food".
 *
 * @param name		Command name to find.
 *
 * @return A pointer to the command structure, or NULL if no match found.
 */
static const struct console_command *find_command(char *name)
{
	const struct console_command *cmd, *match = NULL;
	int match_length = strlen(name);

	for (cmd = __cmds; cmd < __cmds_end; cmd++) {
		if (!strncasecmp(name, cmd->name, match_length)) {
			if (match)
				return NULL;
			/*
			 * Check if 'cmd->name' is of the same length as
			 * 'name'. If yes, then we have a full match.
			 */
			if (cmd->name[match_length] == '\0')
				return cmd;
			match = cmd;
		}
	}

	return match;
}


static const char const *errmsgs[] = {
	"OK",
	"Unknown error",
	"Unimplemented",
	"Overflow",
	"Timeout",
	"Invalid argument",
	"Busy",
	"Access Denied",
	"Not Powered",
	"Not Calibrated",
};

/**
 * Handle a line of input containing a single command.
 *
 * @param input		Input buffer; modified during parsing.
 *
 * @return EC_SUCCESS, or non-zero if error.
 */
static int handle_command(char *input)
{
	const struct console_command *cmd;
	char *argv[MAX_ARGS_PER_COMMAND];
	int argc = 0;
	int rv;
#ifdef CONFIG_EXPERIMENTAL_CONSOLE
	char *e = NULL;
	int i = 0;
	int j = 0;
	int command_len = 0;
	uint8_t packed_crc8 = 0;

	/* Need to perform some checking to see if our command is intact. */

	/* There's nothing to do if the buffer is empty. */
	if (!input_len)
		return EC_SUCCESS;

	/*
	 * Scan the first two characters in the command string looking for
	 * ampersands.  We need at least one to continue.
	 */
	if ((input_len < 2) || (input[0] != '&' && input[1] != '&'))
		goto command_has_error;

	/*
	 * Okay, we've found at least one.  We need to see if we actually got
	 * 2 ampersands in order to adjust our position properly.
	 */
	i = input[1] == '&' ? 2 : 1;

	/* Next, there should be 4 hex digits: XXYY + '&' */
	if (i+5 > input_len)
		goto command_has_error;
	/* Replace the '&' with null so we can call strtoi(). */
	input[i+4] = 0;
	j = strtoi(input + i, &e, 16);
	if (*e)
		goto command_has_error;
	/* command length = XX, CRC8 = YY */
	command_len = j >> 8;
	packed_crc8 = (uint8_t)j;
	i += 5;

	/* Lastly, verify the CRC8 of the command. */
	if (i+command_len > input_len)
		goto command_has_error;
	if (packed_crc8 != crc8(&input[i], command_len)) {
command_has_error:
		/* Send back the error string. */
		ccprintf("&&EE\n");
		return EC_ERROR_UNKNOWN;
	}

	/* Split input into words.  Ignore words past our limit. */
	split_words(&input[i], &argc, argv);
#else
	split_words(input, &argc, argv);
#endif /* defined(CONFIG_EXPERIMENTAL_CONSOLE) */

	/* If no command, nothing to do */
	if (!argc)
		return EC_SUCCESS;

	cmd = find_command(argv[0]);
	if (!cmd) {
		ccprintf("Command '%s' not found or ambiguous.\n", argv[0]);
		return EC_ERROR_UNKNOWN;
	}

#ifdef CONFIG_RESTRICTED_CONSOLE_COMMANDS
	if (console_is_restricted() && cmd->flags & CMD_FLAG_RESTRICTED)
		rv = EC_ERROR_ACCESS_DENIED;
	else
#endif
	rv = cmd->handler(argc, argv);
	if (rv == EC_SUCCESS)
		return rv;

	/* Print more info for errors */
	if (rv < ARRAY_SIZE(errmsgs))
		ccprintf("%s\n", errmsgs[rv]);
	else if (rv >= EC_ERROR_PARAM1 && rv < EC_ERROR_PARAM_COUNT)
		ccprintf("Parameter %d invalid\n", rv - EC_ERROR_PARAM1 + 1);
	else if (rv == EC_ERROR_PARAM_COUNT)
		ccputs("Wrong number of params\n");
	else if (rv != EC_SUCCESS)
		ccprintf("Command returned error %d\n", rv);

#ifdef CONFIG_CONSOLE_CMDHELP
	if (cmd->argdesc)
		ccprintf("Usage: %s %s\n", cmd->name, cmd->argdesc);
#endif
	return rv;
}

static void console_init(void)
{
	*input_buf = '\0';
	cflush();
#ifdef CONFIG_EXPERIMENTAL_CONSOLE
	ccprintf("Enhanced Console is enabled (v1.0.0); type HELP for help.\n");
#else
	ccprintf("Console is enabled; type HELP for help.\n");
#endif /* defined(CONFIG_EXPERIMENTAL_CONSOLE) */
	cflush();
	ccputs(PROMPT);
}

static int console_putc(int c)
{
	int rv1 = uart_putc(c);
	int rv2 = usb_putc(c);

	return rv1 == EC_SUCCESS ? rv2 : rv1;
}

#ifndef CONFIG_EXPERIMENTAL_CONSOLE
static void move_cursor_right(void)
{
	if (input_pos == input_len)
		return;

	ccputs("\x1b[1C");
	input_pos++;
}

static void move_cursor_end(void)
{
	if (input_pos == input_len)
		return;

	ccprintf("\x1b[%dC", input_len - input_pos);
	input_pos = input_len;
}

static void move_cursor_left(void)
{
	if (input_pos == 0)
		return;

	ccputs("\x1b[1D");
	input_pos--;
}

static void move_cursor_begin(void)
{
	if (input_pos == 0)
		return;

	ccprintf("\x1b[%dD", input_pos);
	input_pos = 0;
}
#endif /* !defined(CONFIG_EXPERIMENTAL_CONSOLE) */

static void repeat_char(char c, int cnt)
{
	while (cnt--)
		console_putc(c);
}

#ifdef CONFIG_CONSOLE_HISTORY

/**
 * Load input history
 *
 * @param idx		History index to load
 */
static void load_history(int idx)
{
	/* Copy history */
	strzcpy(input_buf, history[idx], CONFIG_CONSOLE_INPUT_LINE_SIZE);

	/* Print history */
	move_cursor_begin();
	ccputs(input_buf);

	/* Clear everything past end of history */
	input_pos = strlen(input_buf);
	if (input_len > input_pos) {
		repeat_char(' ', input_len - input_pos);
		repeat_char('\b', input_len - input_pos);
	}
	input_len = input_pos;
}

/**
 * Save line to the next history slot
 */
static void save_history(void)
{
	strzcpy(history[history_next], input_buf,
		CONFIG_CONSOLE_INPUT_LINE_SIZE);
}

#endif /* CONFIG_CONSOLE_HISTORY */

#ifndef CONFIG_EXPERIMENTAL_CONSOLE
static void handle_backspace(void)
{
	if (!input_pos)
		return;  /* Already at beginning of line */

	/* Move cursor back */
	console_putc('\b');

	/* Print and move anything following the cursor position */
	if (input_pos != input_len) {
		ccputs(input_buf + input_pos);
		memmove(input_buf + input_pos - 1,
			input_buf + input_pos,
			input_len - input_pos + 1);
	} else {
		input_buf[input_len - 1] = '\0';
	}

	/* Space over last character and move cursor to correct position */
	console_putc(' ');
	repeat_char('\b', input_len - input_pos + 1);

	input_len--;
	input_pos--;
}

/**
 * Escape code handler
 *
 * @param c             Next received character.
 * @return		Key code, or -1 if character was eaten
 */
static int handle_esc(int c)
{
	switch (esc_state) {
	case ESC_START:
		if (c == '[') {
			esc_state = ESC_BRACKET;
			return -1;
		} else if (c == 'O') {
			esc_state = ESC_O;
			return -1;
		}
		break;

	case ESC_BRACKET:
		if (c == '1') {
			esc_state = ESC_BRACKET_1;
			return -1;
		} else if (c == '3') {
			esc_state = ESC_BRACKET_3;
			return -1;
		}

		if (c == 'A')
			return KEY_UP_ARROW;
		else if (c == 'B')
			return KEY_DOWN_ARROW;
		else if (c == 'C')
			return KEY_RIGHT_ARROW;
		else if (c == 'D')
			return KEY_LEFT_ARROW;
		break;

	case ESC_O:
		if (c == 'F')
			return KEY_END;
		break;

	case ESC_BRACKET_1:
		if (c == '~')
			return KEY_HOME;
		break;

	case ESC_BRACKET_3:
		if (c == '~')
			return KEY_DEL;
		break;

	default:
		break;
	}

	/* Check if the escape code is done */
	if (isalpha(c) || c == '~')
		esc_state = ESC_OUTSIDE;
	else
		esc_state = ESC_BAD;

	return -1;
}
#endif /* !defined(CONFIG_EXPERIMENTAL_CONSOLE) */

static void console_handle_char(int c)
{
#ifdef CONFIG_EXPERIMENTAL_CONSOLE
	/*
	 * If we receive a EC_SYN, we should respond immediately with a EC_ACK.
	 * This handshake lets the interpreter know that this is an enhanced
	 * image.
	 */
	if (c == EC_SYN) {
		console_putc(EC_ACK);
		return;
	}
#endif /* defined(CONFIG_EXPERIMENTAL_CONSOLE) */

	/* Translate CR and CRLF to LF (newline) */
	if (c == '\r') {
		last_rx_was_cr = 1;
		c = '\n';
	} else if (c == '\n' && last_rx_was_cr) {
		last_rx_was_cr = 0;
		return;
	} else {
		last_rx_was_cr = 0;
	}

#ifndef CONFIG_EXPERIMENTAL_CONSOLE
	/* Handle terminal escape sequences (ESC [ ...) */
	if (c == 0x1B) {
		esc_state = ESC_START;
		return;
	} else if (esc_state) {
		c = handle_esc(c);
		if (c != -1)
			esc_state = ESC_OUTSIDE;
	}
#endif /* !defined(CONFIG_EXPERIMENTAL_CONSOLE) */

	switch (c) {
#ifndef CONFIG_EXPERIMENTAL_CONSOLE
	case KEY_DEL:
		if (input_pos == input_len)
			break;  /* Already at end */

		move_cursor_right();

		/* Drop through to backspace handling */
	case '\b':
	case 0x7f:
		handle_backspace();
		break;
#endif /* !defined(CONFIG_EXPERIMENTAL_CONSOLE) */

	case '\n':
#ifndef CONFIG_EXPERIMENTAL_CONSOLE
		/* Terminate this line */
		console_putc('\n');
#endif /* !defined(CONFIG_EXPERIMENTAL_CONSOLE) */

#ifdef CONFIG_CONSOLE_HISTORY
		/* Save command in history buffer */
		if (input_len) {
			save_history();
			history_next = (history_next + 1) %
				CONFIG_CONSOLE_HISTORY;
			history_pos = history_next;
		}
#endif

		/* Handle command */
		handle_command(input_buf);

		/* Start new line */
		input_pos = input_len = 0;
		input_buf[0] = '\0';

#ifndef CONFIG_EXPERIMENTAL_CONSOLE
		/* Reprint prompt */
		ccputs(PROMPT);
#endif /* !defined(CONFIG_EXPERIMENTAL_CONSOLE) */
		break;

#ifndef CONFIG_EXPERIMENTAL_CONSOLE
	case CTRL('A'):
	case KEY_HOME:
		move_cursor_begin();
		break;

	case CTRL('B'):
	case KEY_LEFT_ARROW:
		move_cursor_left();
		break;

	case CTRL('E'):
	case KEY_END:
		move_cursor_end();
		break;

	case CTRL('F'):
	case KEY_RIGHT_ARROW:
		move_cursor_right();
		break;

	case CTRL('K'):
		/* Kill to end of line */
		if (input_pos == input_len)
			break;

		repeat_char(' ', input_len - input_pos);
		repeat_char('\b', input_len - input_pos);
		input_len = input_pos;
		input_buf[input_len] = '\0';
		break;

	case CTRL('L'):
		/* Reprint current */
		ccputs("\x0c" PROMPT);
		ccputs(input_buf);
		repeat_char('\b', input_len - input_pos);
		break;

#ifdef CONFIG_CONSOLE_HISTORY

	case CTRL('P'):
	case KEY_UP_ARROW:
		/* History previous */
		if (history_pos == history_next)
			save_history();

		if (--history_pos < 0)
			history_pos = CONFIG_CONSOLE_HISTORY - 1;

		load_history(history_pos);
		break;

	case CTRL('N'):
	case KEY_DOWN_ARROW:
		/* History next */
		if (history_pos == history_next)
			save_history();

		if (++history_pos >= CONFIG_CONSOLE_HISTORY)
			history_pos = 0;

		load_history(history_pos);
		break;

#endif /* CONFIG_CONSOLE_HISTORY */
#endif /* !defined(CONFIG_EXPERIMENTAL_CONSOLE) */

	default:
		/* Ignore non-printing characters */
		if (!isprint(c))
			break;

#ifndef CONFIG_EXPERIMENTAL_CONSOLE
		/* Ignore if line is full (leaving room for terminating null) */
		if (input_len >= sizeof(input_buf) - 1)
			break;

		/* Print character */
		console_putc(c);
#endif /* !defined(CONFIG_EXPERIMENTAL_CONSOLE) */

		/* If not at end of line, print rest of line and move it down */
		if (input_pos != input_len) {
			ccputs(input_buf + input_pos);
			memmove(input_buf + input_pos + 1,
				input_buf + input_pos,
				input_len - input_pos + 1);
			repeat_char('\b', input_len - input_pos);
		}

		/* Add character to buffer and terminate it */
		input_buf[input_pos++] = c;
		input_buf[++input_len] = '\0';
	}
}

void console_has_input(void)
{
#ifdef CONFIG_LOW_POWER_IDLE
	/* Notify the clock module that the console is in use. */
	clock_refresh_console_in_use();
#endif

	/* Wake up the console task */
	task_wake(TASK_ID_CONSOLE);
}

void console_task(void *u)
{
	console_init();

	while (1) {
		int c;

		while (1) {
			c = uart_getc();
			if (c == -1)
				break;
			console_handle_char(c);
		}

		while (1) {
			c = usb_getc();
			if (c == -1)
				break;
			console_handle_char(c);
		}

		task_wait_event(-1);  /* Wait for more input */
	}
}

/*****************************************************************************/
/* Console commands */

/* Command handler - prints help. */
static int command_help(int argc, char **argv)
{
	const int ncmds = __cmds_end - __cmds;
	const int cols = 5;			/* printing in five columns */
	const int rows = (ncmds + cols - 1) / cols;
	int i, j;

#ifdef CONFIG_CONSOLE_CMDHELP
	if (argc == 2) {
		const struct console_command *cmd;

		if (!strcasecmp(argv[1], "list")) {
#ifdef CONFIG_CONSOLE_COMMAND_FLAGS
			ccputs("Command     Flags   Description\n");
			for (i = 0; i < ncmds; i++) {
				ccprintf(" %-14s %x %s\n",
					 __cmds[i].name, __cmds[i].flags,
					 __cmds[i].help);
				cflush();
			}
#else
			ccputs("Known commands:\n");
			for (i = 0; i < ncmds; i++) {
				ccprintf("  %-15s%s\n",
					 __cmds[i].name, __cmds[i].help);
				cflush();
			}
#endif
			ccputs("HELP CMD = help on CMD.\n");
			return EC_SUCCESS;
		}
		cmd = find_command(argv[1]);
		if (!cmd) {
			ccprintf("Command '%s' not found or ambiguous.\n",
				 argv[1]);
			return EC_ERROR_UNKNOWN;
		}
		ccprintf("Usage: %s %s\n", cmd->name,
			 (cmd->argdesc ? cmd->argdesc : ""));
		if (cmd->help)
			ccprintf("%s\n", cmd->help);
		return EC_SUCCESS;
	}
#endif

	ccputs("Known commands:\n");
	for (i = 0; i < rows; i++) {
		ccputs(" ");
		for (j = 0; j < cols; j++) {
			int index = j * rows + i;
			if (index >= ncmds)
				break;
#ifdef CONFIG_RESTRICTED_CONSOLE_COMMANDS
			if (console_is_restricted() &&
			    __cmds[index].flags & CMD_FLAG_RESTRICTED)
				ccprintf("-%-14s", __cmds[index].name);
			else
#endif
				ccprintf(" %-14s", __cmds[index].name);
		}
		ccputs("\n");
		cflush();
	}

#ifdef CONFIG_CONSOLE_CMDHELP
	ccputs("HELP LIST = more info; ");
	ccputs("HELP CMD = help on CMD.\n");
#endif

	return EC_SUCCESS;
}
DECLARE_SAFE_CONSOLE_COMMAND(help, command_help,
			     "[ list | <name> ]",
			     "Print command help");

#ifdef CONFIG_CONSOLE_HISTORY
static int command_history(int argc, char **argv)
{
	int i;

	for (i = 0; i < CONFIG_CONSOLE_HISTORY; i++) {
		int idx = (history_next + i) % CONFIG_CONSOLE_HISTORY;
		if (history[idx][0])
			ccprintf("%s\n", history[idx]);
	}

	return EC_SUCCESS;
}
DECLARE_SAFE_CONSOLE_COMMAND(history, command_history,
			     NULL,
			     "Print console history");
#endif
