/*
 * Copyright 2014 Google Inc.
 *
 * 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 2 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.
 */

#include <endian.h>
#include <gdb.h>
#include <libpayload.h>

/* MMIO word size is not standardized, but *usually* 32 (even on ARM64) */
typedef u32 mmio_word_t;

static const int timeout_us = 100 * 1000;
static const char output_overrun[] = "GDB output buffer overrun (try "
				     "increasing reply.size)!\n";

/* Serial-specific glue code... add more transport layers here when desired. */

static void gdb_raw_putchar(u8 c)
{
	serial_putchar(c);
}

static int gdb_raw_getchar(void)
{
	u64 start = timer_us(0);

	while (!serial_havechar())
		if (timer_us(start) > timeout_us)
			return -1;

	return serial_getchar();
}

void gdb_transport_init(void)
{
	console_remove_output_driver(serial_putchar);
}

void gdb_transport_teardown(void)
{
	serial_console_init();
}

/* Hex digit character <-> number conversion (illegal chars undefined!). */

static u8 from_hex(unsigned char c)
{
	static const s8 values[] = {
		-1, 10, 11, 12, 13, 14, 15, -1,
		-1, -1, -1, -1, -1, -1, -1, -1,
		 0,  1,  2,  3,  4,  5,  6,  7,
		 8,  9, -1, -1, -1, -1, -1, -1,
	};

	return values[c & 0x1f];
}

static char to_hex(u8 v)
{
	static const char digits[] = "0123456789abcdef";

	return digits[v & 0xf];
}

/* Message encode/decode functions (must access whole aligned words for MMIO) */

void gdb_message_encode_bytes(struct gdb_message *message, const void *data,
			      int length)
{
	die_if(message->used + length * 2 > message->size, output_overrun);
	const mmio_word_t *aligned =
		(mmio_word_t *)ALIGN_DOWN((uintptr_t)data, sizeof(*aligned));
	mmio_word_t word = be32toh(readl(aligned++));
	while (length--) {
		u8 byte = (word >> ((((void *)aligned - data) - 1) * 8));
		message->buf[message->used++] = to_hex(byte >> 4);
		message->buf[message->used++] = to_hex(byte & 0xf);
		if (length && ++data == (void *)aligned)
			word = be32toh(readl(aligned++));
	}
}

void gdb_message_decode_bytes(const struct gdb_message *message, int offset,
			      void *data, int length)
{
	die_if(offset + 2 * length > message->used, "Decode overrun in GDB "
		"message: %.*s", message->used, message->buf);
	mmio_word_t *aligned =
		(mmio_word_t *)ALIGN_DOWN((uintptr_t)data, sizeof(*aligned));
	int shift = ((void *)(aligned + 1) - data) * 8;
	mmio_word_t word = be32toh(readl(aligned)) >> shift;
	while (length--) {
		word <<= 8;
		word |= from_hex(message->buf[offset++]) << 4;
		word |= from_hex(message->buf[offset++]);
		if (++data - (void *)aligned == sizeof(*aligned))
			writel(htobe32(word), aligned++);
	}
	if (data != (void *)aligned) {
		shift = ((void *)(aligned + 1) - data) * 8;
		clrsetbits_be32(aligned, ~((1 << shift) - 1), word << shift);
	}
}

void gdb_message_encode_zero_bytes(struct gdb_message *message, int length)
{
	die_if(message->used + length * 2 > message->size, output_overrun);
	memset(message->buf + message->used, '0', length * 2);
	message->used += length * 2;
}

void gdb_message_add_string(struct gdb_message *message, const char *string)
{
	message->used += strlcpy((char *)message->buf + message->used,
			     string, message->size - message->used);

	/* Check >= instead of > to account for strlcpy's trailing '\0'. */
	die_if(message->used >= message->size, output_overrun);
}

void gdb_message_encode_int(struct gdb_message *message, uintptr_t val)
{
	int length = sizeof(uintptr_t) * 2 - __builtin_clz(val) / 4;
	die_if(message->used + length > message->size, output_overrun);
	while (length--)
		message->buf[message->used++] =
			to_hex((val >> length * 4) & 0xf);
}

uintptr_t gdb_message_decode_int(const struct gdb_message *message, int offset,
				 int length)
{
	uintptr_t val = 0;

	die_if(length > sizeof(uintptr_t) * 2, "GDB decoding invalid number: "
	       "%.*s", message->used, message->buf);

	while (length--) {
		val <<= 4;
		val |= from_hex(message->buf[offset++]);
	}

	return val;
}

/* Like strtok/strsep: writes back offset argument, returns original offset. */
int gdb_message_tokenize(const struct gdb_message *message, int *offset)
{
	int token = *offset;
	while (!strchr(",;:", message->buf[(*offset)++]))
		die_if(*offset >= message->used, "Undelimited token in GDB "
				"message at offset %d: %.*s",
				token, message->used, message->buf);
	return token;
}

/* High-level send/receive functions. */

void gdb_get_command(struct gdb_message *command)
{
	enum command_state {
		STATE_WAITING,
		STATE_COMMAND,
		STATE_CHECKSUM0,
		STATE_CHECKSUM1,
	};

	u8 checksum = 0;
	u8 running_checksum = 0;
	enum command_state state = STATE_WAITING;

	while (1) {
		int c = gdb_raw_getchar();
		if (c < 0) {
			/*
			 * Timeout waiting for a byte. Reset the
			 * state machine.
			 */
			state = STATE_WAITING;
			continue;
		}

		switch (state) {
		case STATE_WAITING:
			if (c == '$') {
				running_checksum = 0;
				command->used = 0;
				state = STATE_COMMAND;
			}
			break;
		case STATE_COMMAND:
			if (c == '#') {
				state = STATE_CHECKSUM0;
				break;
			}
			die_if(command->used >= command->size, "GDB input buf"
			       "fer overrun (try increasing command.size)!\n");
			command->buf[command->used++] = c;
			running_checksum += c;
			break;
		case STATE_CHECKSUM0:
			checksum = from_hex(c) << 4;
			state = STATE_CHECKSUM1;
			break;
		case STATE_CHECKSUM1:
			checksum += from_hex(c);
			if (running_checksum == checksum) {
				gdb_raw_putchar('+');
				return;
			} else {
				state = STATE_WAITING;
				gdb_raw_putchar('-');
			}
			break;
		}
	}
}

void gdb_send_reply(const struct gdb_message *reply)
{
	int i;
	int retries = 1 * 1000 * 1000 / timeout_us;
	u8 checksum = 0;

	for (i = 0; i < reply->used; i++)
		checksum += reply->buf[i];

	do {
		gdb_raw_putchar('$');
		for (i = 0; i < reply->used; i++)
			gdb_raw_putchar(reply->buf[i]);
		gdb_raw_putchar('#');
		gdb_raw_putchar(to_hex(checksum >> 4));
		gdb_raw_putchar(to_hex(checksum & 0xf));
	} while (gdb_raw_getchar() != '+' && retries--);
}