/*
 * Copyright 2017 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 <errno.h>
#include <getopt.h>
#include <poll.h>
#include <signal.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <unistd.h>

#include <libusb.h>

/* Command line options */
static uint16_t vid = 0x18d1;			/* Google */
static uint16_t pid = 0x5022;			/* Hammer */
static uint8_t ep_num = 4;			/* console endpoint */
static uint8_t extended_i2c_exercise;		/* non-zero to exercise */
static char *firmware_binary = "144.0_2.0.bin";	/* firmware blob */

/* Firmware binary blob related */
#define MAX_FW_PAGE_SIZE	512
#define MAX_FW_PAGE_COUNT	1024
#define MAX_FW_SIZE		(128 * 1024)

static uint8_t fw_data[MAX_FW_SIZE];
int fw_page_count;
int fw_page_size;
int fw_size;
uint8_t ic_type;
int iap_version;

/* Utility functions */
static int le_bytes_to_int(uint8_t *buf)
{
	return buf[0] + (int)(buf[1] << 8);
}

/* Command line parsing related */
static char *progname;
static char *short_opts = ":f:v:p:e:hd";
static const struct option long_opts[] = {
	/* name    hasarg *flag val */
	{"file",     1,   NULL, 'f'},
	{"vid",      1,   NULL, 'v'},
	{"pid",      1,   NULL, 'p'},
	{"ep",       1,   NULL, 'e'},
	{"help",     0,   NULL, 'h'},
	{"debug",    0,   NULL, 'd'},
	{NULL,       0,   NULL, 0},
};

static void usage(int errs)
{
	printf("\nUsage: %s [options]\n"
	       "\n"
	       "Firmware updater over USB for trackpad under hammer\n"
	       "\n"
	       "Options:\n"
	       "\n"
	       "  -f,--file   STR         Firmware binary (default %s)\n"
	       "  -v,--vid    HEXVAL      Vendor ID (default %04x)\n"
	       "  -p,--pid    HEXVAL      Product ID (default %04x)\n"
	       "  -e,--ep     NUM         Endpoint (default %d)\n"
	       "  -d,--debug              Exercise extended read I2C over USB\n"
	       "                          and print verbose debug messages.\n"
	       "  -h,--help               Show this message\n"
	       "\n", progname, firmware_binary, vid, pid, ep_num);

	exit(!!errs);
}

static void parse_cmdline(int argc, char *argv[])
{
	char *e = 0;
	int i, errorcnt = 0;

	progname = strrchr(argv[0], '/');
	if (progname)
		progname++;
	else
		progname = argv[0];

	opterr = 0;				/* quiet, you */
	while ((i = getopt_long(argc, argv, short_opts, long_opts, 0)) != -1) {
		switch (i) {
		case 'f':
			firmware_binary = optarg;
			break;
		case 'p':
			pid = (uint16_t) strtoull(optarg, &e, 16);
			if (!*optarg || (e && *e)) {
				printf("Invalid argument: \"%s\"\n", optarg);
				errorcnt++;
			}
			break;
		case 'v':
			vid = (uint16_t) strtoull(optarg, &e, 16);
			if (!*optarg || (e && *e)) {
				printf("Invalid argument: \"%s\"\n", optarg);
				errorcnt++;
			}
			break;
		case 'e':
			ep_num = (uint8_t) strtoull(optarg, &e, 0);
			if (!*optarg || (e && *e)) {
				printf("Invalid argument: \"%s\"\n", optarg);
				errorcnt++;
			}
			break;
		case 'd':
			extended_i2c_exercise = 1;
			break;
		case 'h':
			usage(errorcnt);
			break;
		case 0:				/* auto-handled option */
			break;
		case '?':
			if (optopt)
				printf("Unrecognized option: -%c\n", optopt);
			else
				printf("Unrecognized option: %s\n",
				       argv[optind - 1]);
			errorcnt++;
			break;
		case ':':
			printf("Missing argument to %s\n", argv[optind - 1]);
			errorcnt++;
			break;
		default:
			printf("Internal error at %s:%d\n", __FILE__, __LINE__);
			exit(1);
		}
	}

	if (errorcnt)
		usage(errorcnt);

}

/* USB transfer related */
static uint8_t rx_buf[1024];
static uint8_t tx_buf[1024];

static struct libusb_device_handle *devh;
static struct libusb_transfer *rx_transfer;
static struct libusb_transfer *tx_transfer;

static int claimed_iface;
static int iface_num = -1;
static int do_exit;

static void request_exit(const char *format, ...)
{
	va_list ap;
	va_start(ap, format);
	vfprintf(stderr, format, ap);
	va_end(ap);
	do_exit++;    /* Why need this ? */

	if (tx_transfer)
		libusb_free_transfer(tx_transfer);
	if (rx_transfer)
		libusb_free_transfer(rx_transfer);
	if (devh) {
		if (claimed_iface)
			libusb_release_interface(devh, iface_num);
		libusb_close(devh);
	}
	libusb_exit(NULL);
	exit(1);
}

#define DIE(msg, r)							\
	request_exit("%s: line %d, %s\n", msg, __LINE__,		\
		     libusb_error_name(r))

static void sighandler(int signum)
{
	request_exit("caught signal %d: %s\n", signum, strsignal(signum));
}

static int find_interface_with_endpoint(int want_ep_num)
{
	int iface_num = -1;
	int r, i, j, k;
	struct libusb_device *dev;
	struct libusb_config_descriptor *conf = 0;
	const struct libusb_interface *iface0;
	const struct libusb_interface_descriptor *iface;
	const struct libusb_endpoint_descriptor *ep;

	dev = libusb_get_device(devh);
	r = libusb_get_active_config_descriptor(dev, &conf);
	if (r < 0) {
		DIE("get_active_config", r);
		return -1;
	}

	for (i = 0; i < conf->bNumInterfaces; i++) {
		iface0 = &conf->interface[i];
		for (j = 0; j < iface0->num_altsetting; j++) {
			iface = &iface0->altsetting[j];
			for (k = 0; k < iface->bNumEndpoints; k++) {
				ep = &iface->endpoint[k];
				if (ep->bEndpointAddress == want_ep_num) {
					iface_num = i;
					break;
				}
			}
		}
	}

	libusb_free_config_descriptor(conf);
	return iface_num;
}

static void init_with_libusb(void)
{
	int r = 1;

	printf("init usb interface\n");
	r = libusb_init(NULL);
	if (r < 0)
		DIE("init", r);

	printf("open_device %04x:%04x\n", vid, pid);
	devh = libusb_open_device_with_vid_pid(NULL, vid, pid);
	if (!devh)
		request_exit("can't find device\n");

	iface_num = find_interface_with_endpoint(ep_num);
	if (iface_num < 0)
		request_exit("can't find interface owning EP %d\n", ep_num);

	printf("claim_interface %d to use endpoint %d\n", iface_num, ep_num);
	r = libusb_claim_interface(devh, iface_num);
	if (r < 0)
		DIE("claim interface", r);
	claimed_iface = 1;
}

static void register_sigaction(void)
{
	struct sigaction sigact;
	sigact.sa_handler = sighandler;
	sigemptyset(&sigact.sa_mask);
	sigact.sa_flags = 0;
	sigaction(SIGINT, &sigact, NULL);
	sigaction(SIGTERM, &sigact, NULL);
	sigaction(SIGQUIT, &sigact, NULL);
}

/* Transfer over libusb */
#define I2C_PORT_ON_HAMMER		0x00
#define I2C_ADDRESS_ON_HAMMER		0x15

static int check_read_status(int r, int expected, int actual)
{
	int i;
	if (r)
		printf("Warning: libusb_bulk_transfer return error : %d\n", r);
	if (actual != (expected + 4)) {
		printf("Warning: Not reading back %d bytes.\n", expected);
		r = 1;
	}

	/* Check transaction status as defined in usb_i2c.h */
	for (i = 0; i < 4; ++i)
		if (rx_buf[i] != 0)
			break;

	if (i != 4) {
		r = le_bytes_to_int(rx_buf);
		printf("Warning: Defined error code (%d) returned.\n", r);
	}

	if (r || extended_i2c_exercise) {
		printf("\nDumping the receive buffer:\n");
		printf("  Recv %d bytes from USB hosts.\n", actual);
		for (i = 0; i < actual; ++i)
			printf("    [%2d]bytes: 0x%0x\n", i, rx_buf[i]);
	}
	return r;
}

#define MAX_USB_PACKET_SIZE		64
#define PRIMITIVE_READING_SIZE		60

static int libusb_single_write_and_read(
		const uint8_t *to_write, uint16_t write_length,
		uint8_t *to_read, uint16_t read_length)
{
	int r;
	int tx_ready;
	int remains;
	int sent_bytes = 0;
	int actual_length = -1;
	int offset = read_length > PRIMITIVE_READING_SIZE ? 6 : 4;
	tx_transfer = rx_transfer = 0;

	memmove(tx_buf + offset, to_write, write_length);
	tx_buf[0] = I2C_PORT_ON_HAMMER | ((write_length >> 8) << 4);
	tx_buf[1] = I2C_ADDRESS_ON_HAMMER;
	tx_buf[2] = write_length & 0xff;
	if (read_length > PRIMITIVE_READING_SIZE) {
		tx_buf[3] = (read_length & 0x7f) | (1 << 7);
		tx_buf[4] = read_length >> 7;
		if (extended_i2c_exercise) {
			printf("Triggering extended reading."
				"rc:%0x, rc1:%0x\n",
				tx_buf[3], tx_buf[4]);
			printf("Expecting %d Bytes.\n",
				(tx_buf[3] & 0x7f) | (tx_buf[4] << 7));
		}
	} else {
		tx_buf[3] = read_length;
	}

	/*
	 * TODO: This loop is probably not required as we write the whole block
	 * in one transaction.
	 */
	while (sent_bytes < (offset + write_length)) {
		tx_ready = remains = (offset + write_length) - sent_bytes;

		r = libusb_bulk_transfer(devh,
				(ep_num | LIBUSB_ENDPOINT_OUT),
				tx_buf + sent_bytes, tx_ready,
				&actual_length, 5000);
		if (r == 0 && actual_length == tx_ready) {
			r = libusb_bulk_transfer(devh,
					(ep_num | LIBUSB_ENDPOINT_IN),
					rx_buf, sizeof(rx_buf),
					&actual_length, 5000);
		}
		r = check_read_status(
				r, (remains == tx_ready) ? read_length : 0,
				actual_length);
		if (r)
			break;
		sent_bytes += tx_ready;
	}
	return r;
}

/* Control Elan trackpad I2C over USB */
#define ETP_I2C_INF_LENGTH		2

static int elan_write_and_read(
		int reg, uint8_t *buf, int read_length,
		int with_cmd, int cmd)
{

	tx_buf[0] = (reg >> 0) & 0xff;
	tx_buf[1] = (reg >> 8) & 0xff;
	if (with_cmd) {
		tx_buf[2] = (cmd >> 0) & 0xff;
		tx_buf[3] = (cmd >> 8) & 0xff;
	}
	return libusb_single_write_and_read(
			tx_buf, with_cmd ? 4 : 2, rx_buf, read_length);
}

static int elan_read_block(int reg, uint8_t *buf, int read_length)
{
	return elan_write_and_read(reg, buf, read_length, 0, 0);
}

static int elan_read_cmd(int reg)
{
	return elan_read_block(reg, rx_buf, ETP_I2C_INF_LENGTH);
}

static int elan_write_cmd(int reg, int cmd)
{
	return elan_write_and_read(reg, rx_buf, 0, 1, cmd);
}

/* Elan trackpad firmware information related */
#define ETP_I2C_IAP_VERSION_CMD		0x0110
#define ETP_I2C_FW_VERSION_CMD		0x0102
#define ETP_I2C_IAP_CHECKSUM_CMD	0x0315
#define ETP_I2C_FW_CHECKSUM_CMD		0x030F
#define ETP_I2C_OSM_VERSION_CMD		0x0103

static int elan_get_version(int is_iap)
{
	elan_read_cmd(
		is_iap ? ETP_I2C_IAP_VERSION_CMD : ETP_I2C_FW_VERSION_CMD);
	return le_bytes_to_int(rx_buf + 4);
}

static void elan_get_ic_page_count(void)
{
	elan_read_cmd(ETP_I2C_OSM_VERSION_CMD);

	ic_type = rx_buf[5];
	printf("ic_type: %02x\n", ic_type);

	switch (ic_type) {
	case 0x09:
		fw_page_count = 768;
		break;
	case 0x0D:
		fw_page_count = 896;
		break;
	case 0x00:
	case 0x10:
	case 0x14:
		fw_page_count = 1024;
		break;
	default:
		request_exit("The IC type is not supported.\n");
	}

	iap_version = elan_get_version(1);
	if (ic_type == 0x14 && iap_version >= 2) {
		fw_page_count /= 8;
		fw_page_size = 512;
	} else if (ic_type >= 0x0D && iap_version >= 1) {
		fw_page_count /= 2;
		fw_page_size = 128;
	} else {
		fw_page_size = 64;
	}
}

static int elan_get_checksum(int is_iap)
{
	elan_read_cmd(
		is_iap ? ETP_I2C_IAP_CHECKSUM_CMD : ETP_I2C_FW_CHECKSUM_CMD);
	return le_bytes_to_int(rx_buf + 4);
}

static uint16_t elan_get_fw_info(void)
{
	int fw_version = -1;
	uint16_t iap_checksum = 0xffff;
	uint16_t fw_checksum = 0xffff;

	printf("Querying device info...\n");
	fw_checksum = elan_get_checksum(0);
	iap_checksum = elan_get_checksum(1);
	fw_version = elan_get_version(0);
	iap_version = elan_get_version(1);
	printf("IAP  version: %4x, FW  version: %4x\n",
			iap_version, fw_version);
	printf("IAP checksum: %4x, FW checksum: %4x\n",
			iap_checksum, fw_checksum);
	return fw_checksum;
}

/* Update preparation */
#define ETP_I2C_IAP_RESET_CMD		0x0314
#define ETP_I2C_IAP_RESET		0xF0F0
#define ETP_I2C_IAP_CTRL_CMD		0x0310
#define ETP_I2C_MAIN_MODE_ON		(1 << 9)
#define ETP_I2C_IAP_CMD			0x0311
#define ETP_I2C_IAP_PASSWORD		0x1EA5
#define ETP_I2C_IAP_TYPE_CMD		0x0304

static int elan_in_main_mode(void)
{
	elan_read_cmd(ETP_I2C_IAP_CTRL_CMD);
	return le_bytes_to_int(rx_buf + 4) & ETP_I2C_MAIN_MODE_ON;
}

static int elan_read_write_iap_type(void)
{
	for (int retry = 0; retry < 3; ++retry) {
		uint16_t val;

		if (elan_write_cmd(ETP_I2C_IAP_TYPE_CMD,
				   fw_page_size / 2))
			return -1;

		if (elan_read_cmd(ETP_I2C_IAP_TYPE_CMD))
			return -1;

		val = le_bytes_to_int(rx_buf + 4);
		if (val == fw_page_size / 2) {
			printf("%s: OK\n", __func__);
			return 0;
		}

	}
	return -1;
}

static void elan_prepare_for_update(void)
{
	printf("%s\n", __func__);

	int initial_mode = elan_in_main_mode();
	if (!initial_mode) {
		printf("In IAP mode, reset IC.\n");
		elan_write_cmd(ETP_I2C_IAP_RESET_CMD, ETP_I2C_IAP_RESET);
		usleep(30 * 1000);
	}

	/* Send the passphrase */
	elan_write_cmd(ETP_I2C_IAP_CMD, ETP_I2C_IAP_PASSWORD);
	usleep((initial_mode ? 100 : 30) * 1000);

	/* We should be in the IAP mode now */
	if (elan_in_main_mode())
		request_exit("Failure to enter IAP mode, still in main mode\n");

	if (ic_type >= 0x0D && iap_version >= 1) {
		if (elan_read_write_iap_type())
			request_exit("Failure to set IAP mode\n");
	}

	/* Send the passphrase again */
	elan_write_cmd(ETP_I2C_IAP_CMD, ETP_I2C_IAP_PASSWORD);
	usleep(30 * 1000);

	/* Verify the password */
	if (elan_read_cmd(ETP_I2C_IAP_CMD))
		request_exit("cannot read iap password.\n");
	if (le_bytes_to_int(rx_buf + 4) != ETP_I2C_IAP_PASSWORD)
		request_exit("Got an unexpected IAP password %4x\n",
			le_bytes_to_int(rx_buf + 4));
}

/* Firmware block update */
#define ETP_IAP_START_ADDR		0x0083

static uint16_t elan_calc_checksum(uint8_t *data, int length)
{
	uint16_t checksum = 0;
	for (int i = 0; i < length; i += 2)
		checksum += ((uint16_t)(data[i+1]) << 8) | (data[i]);
	return checksum;
}

static int elan_get_iap_addr(void)
{
	return le_bytes_to_int(fw_data + ETP_IAP_START_ADDR * 2) * 2;
}

#define ETP_I2C_IAP_REG_L		0x01
#define ETP_I2C_IAP_REG_H		0x06

#define ETP_FW_IAP_PAGE_ERR		(1 << 5)
#define ETP_FW_IAP_INTF_ERR		(1 << 4)

static int elan_write_fw_block(uint8_t *raw_data, uint16_t checksum)
{
	uint8_t page_store[MAX_FW_PAGE_SIZE + 4];
	int rv;

	page_store[0] = ETP_I2C_IAP_REG_L;
	page_store[1] = ETP_I2C_IAP_REG_H;
	memcpy(page_store + 2, raw_data, fw_page_size);
	page_store[fw_page_size + 2 + 0] = (checksum >> 0) & 0xff;
	page_store[fw_page_size + 2 + 1] = (checksum >> 8) & 0xff;

	rv = libusb_single_write_and_read(
			page_store, fw_page_size + 4, rx_buf, 0);
	if (rv)
		return rv;
	usleep((fw_page_size >= 512 ? 50 : 35) * 1000);
	elan_read_cmd(ETP_I2C_IAP_CTRL_CMD);
	rv = le_bytes_to_int(rx_buf + 4);
	if (rv & (ETP_FW_IAP_PAGE_ERR | ETP_FW_IAP_INTF_ERR)) {
		printf("IAP reports failed write : %x\n", rv);
		return rv;
	}
	return 0;
}


static uint16_t elan_update_firmware(void)
{
	uint16_t checksum = 0, block_checksum;
	int rv;

	printf("%s\n", __func__);

	for (int i = elan_get_iap_addr(); i < fw_size; i += fw_page_size) {
		printf("\rUpdating page %3d...", i / fw_page_size);
		fflush(stdout);
		block_checksum = elan_calc_checksum(fw_data + i, fw_page_size);
		rv = elan_write_fw_block(fw_data + i, block_checksum);
		if (rv)
			request_exit("Failed to update.\n");
		checksum += block_checksum;
		printf(" Updated, checksum: %d", checksum);
		fflush(stdout);
	}
	return checksum;
}

static void pretty_print_buffer(uint8_t *buf, int len)
{
	int i;

	printf("Buffer = 0x");
	for (i = 0; i < len; ++i)
		printf("%02X", buf[i]);
	printf("\n");
}

int main(int argc, char *argv[])
{
	uint16_t local_checksum;
	uint16_t remote_checksum;

	parse_cmdline(argc, argv);
	init_with_libusb();
	register_sigaction();

	/*
	 * Judge IC type  and get page count first.
	 * Then check the FW file.
	 */
	elan_get_ic_page_count();
	fw_size = fw_page_count * fw_page_size;
	printf("FW has %d bytes x %d pages\n", fw_page_size, fw_page_count);

	/* Read the FW file */
	FILE *f = fopen(firmware_binary, "rb");
	if (!f)
		request_exit("Cannot find binary: %s\n", firmware_binary);
	if (fread(fw_data, 1, fw_size, f) != (unsigned int)fw_size)
		request_exit("binary size mismatch, expect %d\n", fw_size);

	/*
	 * It is possible that you are not able to get firmware info. This
	 * might due to an incomplete update last time
	 */
	elan_get_fw_info();

	/* Trigger an I2C transaction of expecting reading of 633 bytes. */
	if (extended_i2c_exercise) {
		tx_buf[0] = 0x05;
		tx_buf[1] = 0x00;
		tx_buf[2] = 0x3C;
		tx_buf[3] = 0x02;
		tx_buf[4] = 0x06;
		tx_buf[5] = 0x00;
		libusb_single_write_and_read(tx_buf, 6, rx_buf, 633);
		pretty_print_buffer(rx_buf, 637);
	}

	/* Get the trackpad ready for receiving update */
	elan_prepare_for_update();

	local_checksum = elan_update_firmware();
	/* Wait for a reset */
	usleep(600 * 1000);
	remote_checksum = elan_get_checksum(1);
	if (remote_checksum != local_checksum)
		printf("checksum diff local=[%04X], remote=[%04X]\n",
				local_checksum, remote_checksum);

	/* Print the updated firmware information */
	elan_get_fw_info();
	return 0;
}
