/* Copyright 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.
 */

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <termios.h>
#include <unistd.h>
#include <sys/stat.h>
#include <endian.h>

#include "comm-host.h"
#include "keyboard_config.h"
#include "ectool.h"

enum {
	/* Alloc this many more scans when needed */
	KEYSCAN_ALLOC_STEP	= 64,
	KEYSCAN_MAX_TESTS	= 10,	/* Maximum number of tests supported */
	KEYSCAN_MAX_INPUT_LEN	= 20,	/* Maximum characters we can receive */
};

/* A single entry of the key matrix */
struct matrix_entry {
	int row;	/* key matrix row */
	int col;	/* key matrix column */
	int keycode;	/* corresponding linux key code */
};

struct keyscan_test_item {
	uint32_t beat;			/* Beat number */
	uint8_t scan[KEYBOARD_COLS_MAX];	/* Scan data */
};

/* A single test, consisting of a list of key scans and expected ascii input */
struct keyscan_test {
	char *name;		/* name of test */
	char *expect;		/* resulting input we expect to see */
	int item_count;		/* number of items in data */
	int item_alloced;	/* number of items alloced in data */
	struct keyscan_test_item *items;	/* key data for EC */
};

/* A list of tests that we can run */
struct keyscan_info {
	unsigned int beat_us;	/* length of each beat in microseconds */
	struct keyscan_test tests[KEYSCAN_MAX_TESTS];	/* the tests */
	int test_count;		/* number of tests */
	struct matrix_entry *matrix;	/* the key matrix info */
	int matrix_count;	/* number of keys in matrix */
};

/**
 * Read the key matrix from the device tree
 *
 * Keymap entries in the fdt take the form of 0xRRCCKKKK where
 * RR=Row CC=Column KKKK=Key Code
 *
 * @param keyscan	keyscan information
 * @param path		path to device tree file containing data
 * @return 0 if ok, -1 on error
 */
static int keyscan_read_fdt_matrix(struct keyscan_info *keyscan,
				   const char *path)
{
	struct stat buf;
	uint32_t word;
	int upto;
	FILE *f;
	int err;

	/* Allocate memory for key matrix */
	if (stat(path, &buf)) {
		fprintf(stderr, "Cannot stat key matrix file '%s'\n", path);
		return -1;
	}
	keyscan->matrix_count = buf.st_size / 4;
	keyscan->matrix = (struct matrix_entry *)(calloc(
		keyscan->matrix_count, sizeof(*keyscan->matrix)));
	if (!keyscan->matrix) {
		fprintf(stderr, "Out of memory for key matrix\n");
		return -1;
	}

	f = fopen(path, "rb");
	if (!f) {
		fprintf(stderr, "Cannot open key matrix file '%s'\n", path);
		return -1;
	}

	/* Now read the data */
	upto = err = 0;
	while (fread(&word, 1, sizeof(word), f) == sizeof(word)) {
		struct matrix_entry *matrix = &keyscan->matrix[upto++];

		word = be32toh(word);
		matrix->row = word >> 24;
		matrix->col = (word >> 16) & 0xff;
		matrix->keycode = word & 0xffff;

		/* Hard-code some limits for now */
		if (matrix->row >= KEYBOARD_ROWS ||
		    matrix->col >= KEYBOARD_COLS_MAX) {
			fprintf(stderr, "Matrix pos out of range (%d,%d)\n",
				matrix->row, matrix->col);
			fclose(f);
			return -1;
		}
	}
	fclose(f);
	if (!err && upto != keyscan->matrix_count) {
		fprintf(stderr, "Read mismatch from matrix file '%s'\n", path);
		err = -1;
	}

	return err;
}

/*
 * translate Linux's KEY_... values into ascii. We change space into 0xfe
 * since we use the numeric value (&32) for space. That avoids ambiguity
 * when we see a space in a key sequence file.
 */
static const unsigned char kbd_plain_xlate[] = {
	0xff, 0x1b, '1',  '2',  '3',  '4',  '5',  '6',
	'7',  '8',  '9',  '0',  '-',  '=', '\b', '\t',	/* 0x00 - 0x0f */
	'q',  'w',  'e',  'r',  't',  'y',  'u',  'i',
	'o',  'p',  '[',  ']', '\r', 0xff,  'a',  's',  /* 0x10 - 0x1f */
	'd',  'f',  'g',  'h',  'j',  'k',  'l',  ';',
	'\'',  '`', 0xff, '\\', 'z',  'x',  'c',  'v',	/* 0x20 - 0x2f */
	'b',  'n',  'm',  ',' ,  '.', '/', 0xff, 0xff, 0xff,
	0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,	/* 0x30 - 0x3f */
	0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,  '7',
	'8',  '9',  '-',  '4',  '5',  '6',  '+',  '1',	/* 0x40 - 0x4f */
	'2',  '3',  '0',  '.', 0xff, 0xff, 0xff, 0xff,
	0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,	/* 0x50 - 0x5F */
	'\r', 0xff, 0xff, '\0'
};

/**
 * Add a new key to a scan
 *
 * Given a new key, this looks it up in the matrix and adds it to the scan,
 * so that if this scan were reported by the EC, the AP would the given key.
 *
 * The format of keys is a list of ascii characters, or & followed by a numeric
 * ascii value, or * followed by a numeric keycode value. Spaces are ignored
 * (use '*32' for space).
 *
 * Examples:
 * a        - a
 * &20      - space
 * *58      - KEY_CAPSLOCK
 *
 * @param keyscan	keyscan information
 * @param keysp		point to the current key string on entry; on exit it
 *			is updated to point to just after the string, plus any
 *			following space
 * @param path		path to device tree file containing data
 * @return 0 if ok, -1 on error
 */
static int keyscan_add_to_scan(struct keyscan_info *keyscan, char **keysp,
			       uint8_t scan[])
{
	const uint8_t *pos;
	struct matrix_entry *matrix;
	int keycode = -1, i;
	char *keys = *keysp;
	int key = ' ';

	if (*keys == '&') {
		/* Numeric ascii code */
		keys++;
		key = strtol(keys, &keys, 10);
		if (!key || keys == *keysp) {
			fprintf(stderr, "Invalid numeric ascii\n");
			return -1;
		}
		if (*keys == ' ')
			keys++;
		else if (*keys) {
			fprintf(stderr, "Expect space after numeric ascii\n");
			return -1;
		}
	} else if (*keys == '*') {
		/* Numeric ascii code */
		keys++;
		keycode = strtol(keys, &keys, 10);
		if (!keycode || keys == *keysp) {
			fprintf(stderr, "Invalid numeric keycode\n");
			return -1;
		}
		if (*keys == ' ')
			keys++;
		else if (*keys) {
			fprintf(stderr, "Expect space after num. keycode\n");
			return -1;
		}
	} else {
		key = *keys++;
	}

	/* Convert keycode to key if needed */
	if (keycode == -1) {
		pos = (uint8_t *)(strchr((char *)kbd_plain_xlate, key));
		if (!pos) {
			fprintf(stderr, "Key '%c' not found in xlate table\n",
				key);
			return -1;
		}
		keycode = pos - kbd_plain_xlate;
	}

	/* Look up keycode in matrix */
	for (i = 0, matrix = keyscan->matrix; i < keyscan->matrix_count;
			i++, matrix++) {
		if (matrix->keycode == keycode) {
#ifdef DEBUG
			printf("%d: %d,%d\n", matrix->keycode, matrix->row,
			       matrix->col);
#endif
			scan[matrix->col] |= 1 << matrix->row;
			*keysp = keys;
			return 0;
		}
	}
	fprintf(stderr, "Key '%c' (keycode %d) not found in matrix\n", key,
		keycode);

	return -1;
}

/**
 * Add a new keyscan to the given test
 *
 * This processes a new keyscan, consisting of a beat number and a sequence
 * of keys.
 *
 * The format of keys is a beat number followed by a list of keys, each
 * either ascii characters, or & followed by a numeric ascii value, or *
 * followed by a numeric keycode value. Spaces are ignored (use '*32' for
 * space).
 *
 * Examples:
 * 0 abc      - beat 0, press a, b and c
 * 4 a &20    - beat 4, press a and space
 * 8 *58 &13  - beat 8, press KEY_CAPSLOCK
 *
 * @param keyscan	keyscan information
 * @param linenum	line number we are reading from (for error reporting)
 * @param test		test to add this scan to
 * @param keys		key string to process
 * @return 0 if ok, -1 on error
 */
static int keyscan_process_keys(struct keyscan_info *keyscan, int linenum,
				struct keyscan_test *test, char *keys)
{
	struct keyscan_test_item *item;
	unsigned long int beat;

	/* Allocate more items if needed */
	if (!test->item_alloced || test->item_count == test->item_alloced) {
		int size, new_size;

		size = test->item_alloced * sizeof(struct keyscan_test_item);
		new_size = size + KEYSCAN_ALLOC_STEP *
					  sizeof(struct keyscan_test_item);
		test->items = (struct keyscan_test_item *)(realloc(test->items,
								   new_size));
		if (!test->items) {
			fprintf(stderr, "Out of memory realloc()\n");
			return -1;
		}
		memset((char *)test->items + size, '\0', new_size - size);
		test->item_alloced += KEYSCAN_ALLOC_STEP;
		new_size = size;
	}

	/* read the beat number */
	item = &test->items[test->item_count];
	beat = strtol(keys, &keys, 10);
	item->beat = beat;

	/* Read keys and add them to our scan */
	if (*keys == ' ') {
		keys++;
		while (*keys) {
			if (keyscan_add_to_scan(keyscan, &keys, item->scan)) {
				fprintf(stderr, "Line %d: Cannot parse"
					" key input '%s'\n", linenum,
					keys);
				return -1;
			}
		}
	} else if (*keys) {
		fprintf(stderr, "Line %d: Need space after beat\n",
			linenum);
		return -1;
	}
	test->item_count++;

	return 0;
}

/* These are the commands we understand in a key sequence file */
enum keyscan_cmd {
	KEYSCAN_CMD_TEST,	/* start a new test */
	KEYSCAN_CMD_ENDTEST,	/* end a test */
	KEYSCAN_CMD_SEQ,	/* add a keyscan to a test sequence */
	KEYSCAN_CMD_EXPECT,	/* indicate what input is expected */

	KEYSCAN_CMD_COUNT
};

/* Names for each of the keyscan commands */
static const char *keyscan_cmd_name[KEYSCAN_CMD_COUNT] = {
	"test",
	"endtest",
	"seq",
	"expect",
};

/**
 * Read a command from a string and return it
 *
 * @param str	String containing command
 * @param len	Length of command string
 * @return detected command, or -1 if none
 */
static enum keyscan_cmd keyscan_read_cmd(const char *str, int len)
{
	int i;

	for (i = 0; i < KEYSCAN_CMD_COUNT; i++) {
		if (!strncmp(keyscan_cmd_name[i], str, len))
			return (enum keyscan_cmd)(i);
	}

	return (enum keyscan_cmd)(-1);
}

/**
 * Process a key sequence file a build up a list of tets from it
 *
 * @param f		File containing keyscan info
 * @param keyscan	keyscan information
 * @return 0 if ok, -1 on error
 */
static int keyscan_process_file(FILE *f, struct keyscan_info *keyscan)
{
	struct keyscan_test *cur_test;
	char line[256];
	char *str;
	int linenum;

	keyscan->test_count = 0;

	linenum = 0;
	cur_test = NULL;
	while (str = fgets(line, sizeof(line), f), str) {
		char *args, *end;
		int cmd, len;

		linenum++;
		len = strlen(str);

		/* chomp the trailing newline */
		if (len > 0 && str[len - 1] == '\n') {
			len--;
			str[len] = '\0';
		}

		/* deal with blank lines and comments */
		if (!len || *str == '#')
			continue;

		/* get the command */
		for (end = str; *end && *end != ' '; end++)
			;

		cmd = keyscan_read_cmd(str, end - str);
		args = end + (*end != '\0');
		switch (cmd) {
		case KEYSCAN_CMD_TEST:
			/* Start a new test */
			if (keyscan->test_count == KEYSCAN_MAX_TESTS) {
				fprintf(stderr, "KEYSCAN_MAX_TESTS "
					"exceeded\n");
				return -1;
			}
			cur_test = &keyscan->tests[keyscan->test_count];
			cur_test->name = strdup(args);
			if (!cur_test->name) {
				fprintf(stderr, "Line %d: out of memory\n",
					linenum);
			}
			break;
		case KEYSCAN_CMD_EXPECT:
			/* Get expect string */
			if (!cur_test) {
				fprintf(stderr, "Line %d: expect should be "
					"inside test\n", linenum);
				return -1;
			}
			cur_test->expect = strdup(args);
			if (!cur_test->expect) {
				fprintf(stderr, "Line %d: out of memory\n",
					linenum);
			}
			break;
		case KEYSCAN_CMD_ENDTEST:
			/* End of a test */
			keyscan->test_count++;
			cur_test = NULL;
			break;
		case KEYSCAN_CMD_SEQ:
			if (keyscan_process_keys(keyscan, linenum, cur_test,
					args)) {
				fprintf(stderr, "Line %d: Cannot parse key "
					"input '%s'\n", linenum, args);
				return -1;
			}
			break;
		default:
			fprintf(stderr, "Line %d: Uknown command '%1.*s'\n",
				linenum, (int)(end - str), str);
			return -1;
		}
	}

	return 0;
}

/**
 * Print out a list of all tests
 *
 * @param keyscan	keyscan information
 */
static void keyscan_print(struct keyscan_info *keyscan)
{
	int testnum;
	int i;

	for (testnum = 0; testnum < keyscan->test_count; testnum++) {
		struct keyscan_test *test = &keyscan->tests[testnum];

		printf("Test: %s\n", test->name);
		for (i = 0; i < test->item_count; i++) {
			struct keyscan_test_item *item;
			int j;

			item = &test->items[i];
			printf("%2d  %7d:  ", i, item->beat);
			for (j = 0; j < sizeof(item->scan); j++)
				printf("%02x ", item->scan[j]);
			printf("\n");
		}
		printf("\n");
	}
}

/**
 * Set the terminal to raw mode, or cooked
 *
 * @param tty_fd	Terminal file descriptor to change
 * @Param raw		0 for cooked, non-zero for raw
 */
static void set_to_raw(int tty_fd, int raw)
{
	struct termios tty_attr;
	int value;

	value = fcntl(tty_fd, F_GETFL);

	tcgetattr(tty_fd, &tty_attr);
	if (raw) {
		tty_attr.c_lflag &= ~ICANON;
		value |= O_NONBLOCK;
	} else {
		tty_attr.c_lflag |= ICANON;
		value &= ~O_NONBLOCK;
	}
	tcsetattr(tty_fd, TCSANOW, &tty_attr);
	fcntl(tty_fd, F_SETFL, value);
}

/**
 * Read input for a whlie until wee see no more
 *
 * @param fd		File descriptor for input
 * @param input		Place to put input string
 * @param max_len	Maximum length of input string
 * @param wait		Number of microseconds to wait for input
 */
static void keyscan_get_input(int fd, char *input, int max_len, int wait)
{
	int len;

	usleep(wait);
	input[0] = '\0';
	len = read(fd, input, max_len - 1);
	if (len > 0)
		input[len] = '\0';
}

static int keyscan_send_sequence(struct keyscan_info *keyscan,
				 struct keyscan_test *test)
{
	struct ec_params_keyscan_seq_ctrl *req;
	struct keyscan_test_item *item;
	int upto, size, rv;

	size = sizeof(*req) + sizeof(item->scan);
	req = (struct ec_params_keyscan_seq_ctrl *)malloc(size);
	if (!req) {
		fprintf(stderr, "Out of memory for message\n");
		return -1;
	}
	for (upto = rv = 0, item = test->items; rv >= 0 &&
			upto < test->item_count; upto++, item++) {
		req->cmd = EC_KEYSCAN_SEQ_ADD;
		req->add.time_us = item->beat * keyscan->beat_us;
		memcpy(req->add.scan, item->scan, sizeof(item->scan));
		rv = ec_command(EC_CMD_KEYSCAN_SEQ_CTRL, 0, req, size, NULL, 0);
	}
	free(req);
	if (rv < 0)
		return rv;

	return 0;
}

/**
 * Run a single test
 *
 * @param keyscan	keyscan information
 * @param test		test to run
 * @return 0 if test passes, -ve if some error occurred
 */
static int run_test(struct keyscan_info *keyscan, struct keyscan_test *test)
{
	struct ec_params_keyscan_seq_ctrl ctrl;
	char input[KEYSCAN_MAX_INPUT_LEN];
	struct ec_result_keyscan_seq_ctrl *resp;
	int wait_us;
	int size;
	int rv;
	int fd = 0;
	int i;

	/* First clear the sequence */
	ctrl.cmd = EC_KEYSCAN_SEQ_CLEAR;
	rv = ec_command(EC_CMD_KEYSCAN_SEQ_CTRL, 0, &ctrl, sizeof(ctrl),
			NULL, 0);
	if (rv < 0)
		return rv;

	rv = keyscan_send_sequence(keyscan, test);
	if (rv < 0)
		return rv;

	/* Start it */
	set_to_raw(fd, 1);
	ctrl.cmd = EC_KEYSCAN_SEQ_START;
	rv = ec_command(EC_CMD_KEYSCAN_SEQ_CTRL, 0, &ctrl, sizeof(ctrl),
			NULL, 0);
	if (rv < 0)
		return rv;

	/* Work out how long we need to wait */
	wait_us = 100 * 1000;	/* Wait 100ms to at least */
	if (test->item_count) {
		struct keyscan_test_item *ksi;

		ksi = &test->items[test->item_count - 1];
		wait_us += ksi->beat * keyscan->beat_us;
	}

	/* Wait for input */
	keyscan_get_input(fd, input, sizeof(input), wait_us);
	set_to_raw(fd, 0);

	/* Ask EC for results */
	size = sizeof(*resp) + test->item_count;
	resp = (struct ec_result_keyscan_seq_ctrl *)(malloc(size));
	if (!resp) {
		fprintf(stderr, "Out of memory for results\n");
		return -1;
	}
	ctrl.cmd = EC_KEYSCAN_SEQ_COLLECT;
	ctrl.collect.start_item = 0;
	ctrl.collect.num_items = test->item_count;
	rv = ec_command(EC_CMD_KEYSCAN_SEQ_CTRL, 0, &ctrl, sizeof(ctrl),
			resp, size);
	if (rv < 0)
		return rv;

	/* Check what scans were skipped */
	for (i = 0; i < resp->collect.num_items; i++) {
		struct ec_collect_item *item;
		struct keyscan_test_item *ksi;

		item = &resp->collect.item[i];
		ksi = &test->items[i];
		if (!(item->flags & EC_KEYSCAN_SEQ_FLAG_DONE))
			printf(" [skip %d at beat %u] ", i, ksi->beat);
	}

	if (strcmp(input, test->expect)) {
		printf("Expected '%s', got '%s' ", test->expect, input);
		return -1;
	}

	return 0;
}

/**
 * Run all tests
 *
 * @param keyscan	keyscan information
 * @return 0 if ok, -1 on error
 */
static int keyscan_run_tests(struct keyscan_info *keyscan)
{
	int testnum;
	int any_err = 0;

	for (testnum = 0; testnum < keyscan->test_count; testnum++) {
		struct keyscan_test *test = &keyscan->tests[testnum];
		int err;

		fflush(stdout);
		err = run_test(keyscan, test);
		any_err |= err;
		if (err) {
			printf("%d: %s:  : FAIL\n", testnum, test->name);
		}
	}

	return any_err ? -1 : 0;
}

int cmd_keyscan(int argc, char *argv[])
{
	struct keyscan_info keyscan;
	FILE *f;
	int err;

	argc--;
	argv++;
	if (argc < 2) {
		fprintf(stderr, "Must specify beat period and filename\n");
		return -1;
	}
	memset(&keyscan, '\0', sizeof(keyscan));
	keyscan.beat_us = atoi(argv[0]);
	if (keyscan.beat_us < 100)
		fprintf(stderr, "Warning: beat period is normally > 100us\n");
	f = fopen(argv[1], "r");
	if (!f) {
		perror("Cannot open file\n");
		return -1;
	}

	/* TODO(crosbug.com/p/23826): Read key matrix from fdt */
	err = keyscan_read_fdt_matrix(&keyscan, "test/test-matrix.bin");
	if (!err)
		err = keyscan_process_file(f, &keyscan);
	if (!err)
		keyscan_print(&keyscan);
	if (!err)
		err = keyscan_run_tests(&keyscan);
	fclose(f);

	return err;
}
