/*
 * Pinephone keyboard I2C flashing tool.
 *
 * Copyright (C) 2021  Ondřej Jirman <megi@xff.cz>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * 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 "common.c"
#include "firmware/registers.h"

static int iic_fd = -1;

static void dump_log(void)
{
	int ret;
	uint8_t addr = 0xff;
	uint8_t buf[64];
	struct i2c_msg msgs[] = {
		{ KB_ADDR, 0, 1, &addr }, // set 0xff address
		{ KB_ADDR, I2C_M_RD, sizeof(buf), buf },
	};

	struct i2c_rdwr_ioctl_data msg = {
		.msgs = msgs,
		.nmsgs = sizeof(msgs) / sizeof(msgs[0])
	};

	ret = ioctl(iic_fd, I2C_RDWR, &msg);
	syscall_error(ret < 0, "I2C_RDWR failed");

	int i;
	for (i = 0; i < sizeof(buf) && buf[i]; i++);

	if (i > 0)
		xwrite(1, buf, i);
}

static void wr_buf(uint8_t addr, uint8_t* buf, size_t size)
{
	int ret;
	uint8_t mbuf[size + 1];

	mbuf[0] = addr;
	memcpy(mbuf + 1, buf, size);

	struct i2c_msg msgs[] = {
		{ KB_ADDR, 0, size + 1, mbuf },
	};

	struct i2c_rdwr_ioctl_data msg = {
		.msgs = msgs,
		.nmsgs = sizeof(msgs) / sizeof(msgs[0])
	};

	debug("WR[%02hhx]:", addr);
	for (int i = 0; i < size; i++)
		debug(" %02hhx", buf[i]);
	debug("\n");

	ret = ioctl(iic_fd, I2C_RDWR, &msg);
	syscall_error(ret < 0, "I2C_RDWR failed");
}

static void rd_buf(uint8_t addr, uint8_t* buf, size_t size)
{
	int ret;
	struct i2c_msg msgs[] = {
		{ KB_ADDR, 0, 1, &addr },
		{ KB_ADDR, I2C_M_RD, size, buf },
	};

	struct i2c_rdwr_ioctl_data msg = {
		.msgs = msgs,
		.nmsgs = sizeof(msgs) / sizeof(msgs[0])
	};

	ret = ioctl(iic_fd, I2C_RDWR, &msg);
	syscall_error(ret < 0, "I2C_RDWR failed");

	debug("RD[%02hhx]:", addr);
	for (int i = 0; i < size; i++) {
		if (i % 8 == 0 && i > 0)
			debug("\n       ");
		debug(" %02hhx", buf[i]);
	}
	debug("\n");
}

static int rd_buf_nofail(uint8_t addr, uint8_t* buf, size_t size)
{
	int ret;
	struct i2c_msg msgs[] = {
		{ KB_ADDR, 0, 1, &addr },
		{ KB_ADDR, I2C_M_RD, size, buf },
	};

	struct i2c_rdwr_ioctl_data msg = {
		.msgs = msgs,
		.nmsgs = sizeof(msgs) / sizeof(msgs[0])
	};

	ret = ioctl(iic_fd, I2C_RDWR, &msg);

	if (ret == 2) {
		debug("RD[%02hhx]:", addr);
		for (int i = 0; i < size; i++) {
			if (i % 8 == 0 && i > 0)
				debug("\n       ");
			debug(" %02hhx", buf[i]);
		}
		debug("\n");
	}

	return ret == 2 ? 0 : -1;
}

static uint8_t rd_reg(uint8_t addr)
{
	uint8_t reg;
	rd_buf(addr, &reg, 1);
	return reg;
}

static int wait_flash_cmd_done(uint8_t cmd)
{
	int ret;

	for (int i = 0; i < 10; i++) {
		uint8_t status;

		ret = rd_buf_nofail(REG_FLASH_CMD, &status, 1);
		if (ret == 0) {
			if (status == 0xffu)
				error("Flashing command 0x%02hhx failed", cmd);
			else if (status == 0x00)
				return 0;
		}

		usleep(5000);
	}
	
	error("Flashing command 0x%02hhx timed out", cmd);
}

static void read_rom_block(uint8_t* out, uint16_t addr)
{
	uint8_t read_rom_start[] = {
		addr & 0xff, // Addr L
		(addr >> 8) & 0xff, // Addr H
		0x00, // CRC-8
		REG_FLASH_UNLOCK_MAGIC, // Unlock
		REG_FLASH_CMD_READ_ROM, // Read ROM
	};

	wr_buf(REG_FLASH_ADDR_L, read_rom_start, sizeof read_rom_start);

	wait_flash_cmd_done(REG_FLASH_CMD_READ_ROM);

	rd_buf(REG_FLASH_DATA_START, out, 128);

	if (rd_reg(REG_FLASH_CRC8) != crc8(out, 128))
		error("CRC8 failure on ROM read");
}

static void write_rom_block(uint16_t addr, uint8_t* data)
{
	uint8_t write_rom_start[5] = {
		addr & 0xff, // Addr L
		(addr >> 8) & 0xff, // Addr H
		crc8(data, 128), // CRC-8
		REG_FLASH_UNLOCK_MAGIC, // Unlock
		REG_FLASH_CMD_WRITE_ROM, // Write ROM
	};

	wr_buf(REG_FLASH_DATA_START, data, 128);
	wr_buf(REG_FLASH_ADDR_L, write_rom_start, sizeof write_rom_start);

	usleep(5000);
	wait_flash_cmd_done(REG_FLASH_CMD_WRITE_ROM);
}

static void run_flash_cmd(uint8_t cmd)
{
	uint8_t cmd_data[] = {
		REG_FLASH_UNLOCK_MAGIC, // Unlock
		cmd, // Command
	};

	wr_buf(REG_FLASH_UNLOCK, cmd_data, sizeof cmd_data);

	usleep(10000);
	wait_flash_cmd_done(cmd);
}

static bool is_block_empty(uint8_t* buf)
{
	for (unsigned i = 0; i < 128; i++)
		if (buf[i] != 0xff)
			return false;

	return true;
}

static int is_kb_stock_connected(void)
{
	uint8_t devid[5];
	int ret;

	ret = rd_buf_nofail(REG_DEVID_K, devid, sizeof devid);
	if (ret)
		return 0;

	if (devid[REG_DEVID_K] != 'K' || devid[REG_DEVID_B] != 'B') // keyboard firmware magic
		return 0;
	if (!(devid[REG_FW_FEATURES] & REG_FW_FEATURES_STOCK_FW)) // stock firmware flag
		return 0;

	if (devid[REG_FW_REVISION] != 0x10)
		error("Unsupported stock pinephone keyboard firmware version %02hhx, expecting 0x10\n", devid[2]);
	if (!(devid[REG_FW_FEATURES] & REG_FW_FEATURES_FLASHING_MODE))
		error("Your stock pinephone keyboard firmware doesn't have flashing support\n");

	return 1;
}

static void usage(void)
{
	printf(
	       "Usage: ppkb-i2c-flasher [--rom-in <path>] [--rom-out <path>] [--verbose]\n"
	       "                        [--help] [<read|write|erase|info|reset>...]\n"
	       "\n"
	       "Options:\n"
	       "  -i, --rom-in <path>   Specify path to binary file you want to flash.\n"
	       "  -o, --rom-out <path>  Specify path where you want to store the contents\n"
	       "                        of code ROM read from the device.\n"
	       "  -s, --size <size>     Specify how many bytes of code rom to flash\n"
	       "                        starting from offset 0x4000 in the rom file.\n"
	       "  -e, --entry <manual|i2c|none>\n"
	       "                        Specify how to enter the stock firmware:\n"
	       "                        - manual: Ask the user to power-cycle the keyboard\n"
	       "                        - i2c: Send I2C command to make supporting user\n"
	       "                        - none: Assume stock firmware is already running\n"
	       "  -v, --verbose         Show details of what's going on.\n"
	       "  -h, --help            This help.\n"
	       "\n"
	       "Commands:\n"
	       "  info      Display information about the current firmware.\n"
	       "  read      Read ROM from the device to --rom-out file.\n"
	       "  write     Flash ROM file to the device from --rom-in.\n"
	       "  erase     Erase the user firmware.\n"
	       "  reset     Perform software reset of the MCU.\n"
	       "  usbiap    Restart to USB IAP mode.\n"
	       "\n"
	       "Format of the ROM files is a flat binary. Only the part of it starting\n"
	       "from 0x4000 will be flashed. Use -s to specify how many bytes to write.\n"
	       "The stock firmware between 0x2000 and 0x4000 will be preserved.\n"
	       "\n"
	       "Pinephone keyboard I2C flashing tool " VERSION "\n"
	       "Written by Ondrej Jirman <megi@xff.cz>, 2021\n"
	       "Licensed under GPLv3, see https://xff.cz/git/pinephone-keyboard/ for\n"
	       "more information.\n"
	);

	exit(2);
}

int main(int ac, char* av[])
{
	char* rom_in = NULL;
	char* rom_out = NULL;
	char* entry_type = "i2c";
	int size = 0x1200;
	int ret;

	while (1) {
		int option_index = 0;
		struct option long_options[] = {
			{ "rom-in",  required_argument, 0, 'i' },
			{ "rom-out", required_argument, 0, 'o' },
			{ "size",    required_argument, 0, 's' },
			{ "entry",   required_argument, 0, 'e' },
			{ "verbose", no_argument,       0, 'v' },
			{ "help",    no_argument,       0, 'h' },
			{ 0,         0,                 0,  0  }
		};

		int c = getopt_long(ac, av, "i:o:s:e:vh", long_options, &option_index);
		if (c == -1)
			break;

		switch (c) {
		case 'o':
			rom_out = strdup(optarg);
			break;
		case 'i':
			rom_in = strdup(optarg);
			break;
		case 'e':
			entry_type = strdup(optarg);
			break;
		case 's':
			if (strstr(optarg, "0x") == optarg) {
				errno = 0;
				char* next = NULL;
				size = strtol(optarg + 2, &next, 16);
				if (errno || next == optarg + 2) {
					printf("ERROR: Can't parse --size %s\n\n", optarg);
					usage();
				}
			} else {
				errno = 0;
				char* next = NULL;
				size = strtol(optarg, &next, 10);
				if (errno || next == optarg) {
					printf("ERROR: Can't parse --size %s\n\n", optarg);
					usage();
				}
			}
			break;
		case 'v':
			verbose = 1;
			break;
		case 'h':
		case '?':
		default:
			usage();
			break;
		}
	}

	if (optind == ac)
		usage();

	if (size < 128) {
		printf("ERROR: --size 0x%04x too small\n\n", size);
		usage();
	}

	if (size > 0x4000) {
		printf("ERROR: --size 0x%04x too large\n\n", size);
		usage();
	}

	if (size % 128 != 0) {
		printf("ERROR: --size 0x%04x is not multiple of 128\n\n", size);
		usage();
	}

	for (int i = optind; i < ac; i++) {
		if (!strcmp(av[i], "read")) {
			if (!rom_out) {
				printf("ERROR: You must specify target file to write rom contents to via --rom-out\n\n");
				usage();
			}
		} else if (!strcmp(av[i], "write")) {
			if (!rom_in) {
				printf("ERROR: You must source file for flashing via --rom-in\n\n");
				usage();
			}
		} else if (!strcmp(av[i], "info")) {
			;
		} else if (!strcmp(av[i], "reset")) {
			;
		} else if (!strcmp(av[i], "erase")) {
			;
		} else if (!strcmp(av[i], "usbiap")) {
			;
		} else {
			printf("ERROR: Unknown command: %s\n\n", av[i]);
			usage();
		}
	}

	printf("Opening keyboard I2C device\n");
	iic_fd = pogo_i2c_open();

	if (!is_kb_stock_connected()) {
		if (!strcmp(entry_type, "i2c")) {
			// send MCU reset command
			uint8_t cmd[] = {REG_SYS_COMMAND_MCU_RESET};
			wr_buf(REG_SYS_COMMAND, cmd, sizeof cmd);

			// stock firmware should report itself quickly
			//
			// tell firmware to block enrty to user app (we have 1s
			// window to do this after reset)
			int i;
			for (i = 0; i < 10; i++) {
				if (is_kb_stock_connected()) {
					uint8_t cmd[] = {REG_SYS_USER_APP_BLOCK_MAGIC};
					wr_buf(REG_SYS_USER_APP_BLOCK, cmd, sizeof cmd);
					break;
				}

				usleep(250000);
			}

			if (i == 10)
				error("Reset command issued over I2C failed, stock firmware failed to report itself within 2.5s");
		} else if (!strcmp(entry_type, "manual")) {
			printf("Please power off the keyboard by holding the keyboard power key for > 12s, then release the power key and press it shortly, once, to power it on again.\n");

			while (true) {
				if (is_kb_stock_connected()) {
					uint8_t cmd[] = {REG_SYS_USER_APP_BLOCK_MAGIC};
					wr_buf(REG_SYS_USER_APP_BLOCK, cmd, sizeof cmd);
					break;
				}

				usleep(250000);
			}
		} else if (!strcmp(entry_type, "none")) {
			error("Stock pinephone keyboard firmware not detected running on the keyboard\n");
		} else {
			error("Unknown entry method %s", entry_type);
		}

		// if after 1s the stock firmware is still running,
		// everything is ok
		usleep(1000000);
		if (!is_kb_stock_connected())
			error("Failed to block the user app from running");
	}

	for (int i = optind; i < ac; i++) {
		if (!strcmp(av[i], "read")) {
			printf("Reading code ROM\n");
			uint8_t rom[0x8000];
			for (unsigned i = 0; i < sizeof(rom); i += 128)
				read_rom_block(rom + i, i);

			int fd = open(rom_out, O_WRONLY | O_CREAT | O_TRUNC, 0666);
			if (fd >= 0) {
				ssize_t wr = write(fd, rom, sizeof rom);
				syscall_error(wr != sizeof(rom), "write failed");
				close(fd);
			}
		} else if (!strcmp(av[i], "write")) {
			int fd;

			uint8_t rom[0x8000];
			memset(rom, 0xff, sizeof rom);

			fd = open(rom_in, O_RDONLY);
			syscall_error(fd < 0, "open(%s) failed", rom_in);
			ssize_t len = read(fd, rom, 0x8000);
			syscall_error(len < 0, "read failed");
			close(fd);
			if (len != 0x8000)
				error("Invalid ROM file (%s) size (%d), must be 32768 bytes", rom_in, (int)len);

			printf("Flashing code ROM\n");
			for (unsigned i = 0x4000; i < 0x4000 + size; i += 128)
				write_rom_block(i, rom + i);

			uint8_t rd_rom[0x8000];
			for (unsigned i = 0x4000; i < 0x4000 + size; i += 128) {
				read_rom_block(rd_rom + i, i);

				if (memcmp(rd_rom + i, rom + i, 128)) {
					printf("WARNING: Block 0x%04x write failed, retrying...\n");
					error("Retries disabled");
				}
			}

			printf("Finishing flashing\n");
			run_flash_cmd(REG_FLASH_CMD_COMMIT);
		} else if (!strcmp(av[i], "info")) {
			uint8_t devid[5];

			rd_buf(0x00, devid, sizeof devid);

			printf("DEVID Register dump:\n");
			for (int i = 0; i < sizeof(devid); i++)
				printf("0x%02x: 0x%02hhx\n", i, devid[i]);
		} else if (!strcmp(av[i], "reset")) {
			printf("Restarting the MCU\n");

			// send MCU reset command
			uint8_t cmd[] = {REG_SYS_COMMAND_MCU_RESET};
			wr_buf(REG_SYS_COMMAND, cmd, sizeof cmd);
		} else if (!strcmp(av[i], "usbiap")) {
			printf("Restarting to USB IAP mode, if you don't have USB interface soldered on, you'll have to power-cycle the keyboard to get out of this flashing mode.\n");

			// send MCU reset command
			uint8_t cmd[] = {REG_SYS_COMMAND_USB_IAP};
			wr_buf(REG_SYS_COMMAND, cmd, sizeof cmd);
		} else if (!strcmp(av[i], "erase")) {
			run_flash_cmd(REG_FLASH_CMD_ERASE_ROM);
		} else {
			printf("ERROR: Unknown command: %s\n\n", av[i]);
			usage();
		}
	}

	return 0;
}
