/* Copyright 2014 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.
 *
 * Megachips DisplayPort to HDMI protocol converter / level shifter driver.
 */

#include "config.h"
#include "console.h"
#include "common.h"
#include "ec_commands.h"
#include "mcdp28x0.h"
#include "queue.h"
#include "queue_policies.h"
#include "timer.h"
#include "usart-stm32f0.h"
#include "util.h"

#define CPRINTF(format, args...) cprintf(CC_USBPD, format, ## args)

static uint8_t mcdp_inbuf[MCDP_INBUF_MAX];

#undef MCDP_DEBUG

#ifdef MCDP_DEBUG
static inline void print_buffer(uint8_t *buf, int cnt)
{
	int i;
	CPRINTF("buf:");
	for (i = 0; i < cnt; i++) {
		if (i && !(i % 4))
			CPRINTF("\n    ");
		CPRINTF("[%02d]0x%02x ", i, buf[i]);
	}
	CPRINTF("\n");
}
#else
static inline void print_buffer(uint8_t *buf, int cnt) {}
#endif

static struct usart_config const usart_mcdp;

struct queue const usart_mcdp_rx_queue = QUEUE_DIRECT(MCDP_INBUF_MAX,
						      uint8_t,
						      usart_mcdp.producer,
						      null_consumer);
struct queue const usart_mcdp_tx_queue = QUEUE_DIRECT(MCDP_OUTBUF_MAX,
						      uint8_t,
						      null_producer,
						      usart_mcdp.consumer);

static struct usart_config const usart_mcdp = USART_CONFIG(CONFIG_MCDP28X0,
							   usart_rx_interrupt,
							   usart_tx_interrupt,
							   115200,
							   0,
							   usart_mcdp_rx_queue,
							   usart_mcdp_tx_queue);

/**
 * Compute checksum.
 *
 * @seed initial value of checksum.
 * @msg message bytes to compute checksum on.
 * @cnt count of message bytes.
 * @return partial checksum.
 */
static uint8_t compute_checksum(uint8_t seed, const uint8_t *msg, int cnt)
{
	int i;
	uint8_t chksum = seed;

	for (i = 0; i < cnt; i++)
		chksum += msg[i];
	return ~chksum + 1;
}

/**
 * transmit message over serial
 *
 * Packet consists of:
 *   msg[0]     == length of entire packet
 *   msg[1]     == 1st message byte (typically command)
 *   msg[cnt+1] == last message byte
 *   msg[cnt+2] == checksum
 *
 * @msg message bytes
 * @cnt count of message bytes
 * @return zero if success, error code otherwise
 */
static int tx_serial(const uint8_t *msg, int cnt)
{
	uint8_t out = cnt + 2;
	/* 1st byte (not in msg) is always cnt + 2, so seed chksum with that */
	uint8_t chksum = compute_checksum(cnt + 2, msg, cnt);

	if (queue_add_unit(&usart_mcdp_tx_queue, &out) != 1)
		return MCDP_ERROR_TX_CNT;

	if (queue_add_units(&usart_mcdp_tx_queue, msg, cnt) != cnt)
		return MCDP_ERROR_TX_BODY;

	print_buffer((uint8_t *)msg, cnt);

	if (queue_add_unit(&usart_mcdp_tx_queue, &chksum) != 1)
		return MCDP_ERROR_TX_CHKSUM;

	return MCDP_SUCCESS;
}

/**
 * receive message over serial
 *
 * While definitive documentation is lacking its believed the following receive
 * packet is always true.
 *
 * Packet consists of:
 *   msg[0]     == length of entire packet
 *   msg[1]     == 1st message byte (typically command)
 *   msg[cnt+2] == last message byte
 *   msg[cnt+3] == checksum
 *
 * @msg pointer to buffer to read message into
 * @cnt count of message bytes
 * @return zero if success, error code otherwise
 */
static int rx_serial(uint8_t *msg, int cnt)
{
	size_t read;
	int retry = 2;

	read = queue_remove_units(&usart_mcdp_rx_queue, msg, cnt);
	while ((read < cnt) && retry) {
		usleep(100*MSEC);
		read += queue_remove_units(&usart_mcdp_rx_queue, msg + read,
					   cnt - read);
		retry--;
	}

	print_buffer(msg, cnt);

	/* Some response sizes are dynamic so shrink cnt accordingly. */
	if (cnt > msg[0])
		cnt = msg[0];

	if (msg[cnt-1] != compute_checksum(0, msg, cnt-1))
		return MCDP_ERROR_CHKSUM;

	if (read != cnt) {
		CPRINTF("rx_serial: read bytes %d != %d cnt\n", read, cnt);
		return MCDP_ERROR_RX_BYTES;
	}

	return MCDP_SUCCESS;
}

static int rx_serial_ack(void)
{
	int rv = rx_serial(mcdp_inbuf, 3);
	if (rv)
		return rv;

	if (mcdp_inbuf[1] != MCDP_CMD_ACK)
		return MCDP_ERROR_RX_ACK;

	return rv;
}

void mcdp_enable(void)
{
	usart_init(&usart_mcdp);
}

void mcdp_disable(void)
{
	usart_shutdown(&usart_mcdp);
}

int mcdp_get_info(struct mcdp_info  *info)
{
	const uint8_t msg[2] = {MCDP_CMD_APPSTEST, 0x28};
	int rv = tx_serial(msg, sizeof(msg));

	if (rv)
		return rv;

	rv = rx_serial_ack();
	if (rv)
		return rv;

	/* chksum is unreliable ... don't check */
	rx_serial(mcdp_inbuf, MCDP_RSP_LEN(MCDP_LEN_GETINFO));

	memcpy(info, &mcdp_inbuf[2], MCDP_LEN_GETINFO);

	return rv;
}

#ifdef CONFIG_CMD_MCDP
static int mcdp_get_dev_id(char *dev, uint8_t dev_id, int dev_cnt)
{
	uint8_t msg[2];
	int rv;
	msg[0] = MCDP_CMD_GETDEVID;
	msg[1] = dev_id;

	rv = tx_serial(msg, sizeof(msg));
	if (rv)
		return rv;

	rv = rx_serial(mcdp_inbuf, sizeof(mcdp_inbuf));
	if (rv)
		return rv;

	memcpy(dev, &mcdp_inbuf[2], mcdp_inbuf[0] - 3);
	dev[mcdp_inbuf[0] - 3] = '\0';
	return EC_SUCCESS;
}

static int mcdp_appstest(uint8_t cmd, int paramc, char **paramv)
{
	uint8_t msg[6];
	char *e;
	int i;
	int rv = MCDP_SUCCESS;

	/* setup any appstest params */
	msg[0] = MCDP_CMD_APPSTESTPARAM;
	for (i = 0; i < paramc; i++) {
		uint32_t param = strtoi(paramv[i], &e, 10);
		if (*e)
			return EC_ERROR_PARAM1;
		msg[1] = i + 1;
		msg[2] = (param >> 24) & 0xff;
		msg[3] = (param >> 16) & 0xff;
		msg[4] = (param >>  8) & 0xff;
		msg[5] = (param >>  0) & 0xff;
		rv = tx_serial(msg, sizeof(msg));
		if (rv)
			return rv;

		rv = rx_serial_ack();
		if (rv)
			return rv;
	}

	msg[0] = MCDP_CMD_APPSTEST;
	msg[1] = cmd;
	rv = tx_serial(msg, 2);
	if (rv)
		return rv;

	rv = rx_serial_ack();
	if (rv)
		return rv;

	/* magic */
	rx_serial(mcdp_inbuf, sizeof(mcdp_inbuf));
	rx_serial(mcdp_inbuf, sizeof(mcdp_inbuf));

	return EC_SUCCESS;
}

int command_mcdp(int argc, char **argv)
{
	int rv = EC_SUCCESS;
	char *e;

	if (argc < 2)
		return EC_ERROR_PARAM_COUNT;

	mcdp_enable();
	if (!strncasecmp(argv[1], "info", 4)) {
		struct mcdp_info info;
		rv = mcdp_get_info(&info);
		if (!rv)
			ccprintf("family:%04x chipid:%04x irom:%d.%d.%d "
				 "fw:%d.%d.%d\n",
				 MCDP_FAMILY(info.family),
				 MCDP_CHIPID(info.chipid),
				 info.irom.major, info.irom.minor,
				 info.irom.build,
				 info.fw.major, info.fw.minor, info.fw.build);
	} else if (!strncasecmp(argv[1], "devid", 4)) {
		uint8_t dev_id = strtoi(argv[2], &e, 10);
		char dev[32];
		if (*e)
			return EC_ERROR_PARAM2;
		else
			rv = mcdp_get_dev_id(dev, dev_id, 32);
		if (!rv)
			ccprintf("devid[%d] = %s\n", dev_id, dev);
	} else if (!strncasecmp(argv[1], "appstest", 4)) {
		uint8_t cmd = strtoi(argv[2], &e, 10);
		if (*e)
			return EC_ERROR_PARAM2;
		else
			rv = mcdp_appstest(cmd, argc - 3, &argv[3]);
		if (!rv)
			ccprintf("appstest[%d] completed\n", cmd);
	} else {
		return EC_ERROR_PARAM1;
	}

	mcdp_disable();
	if (rv)
		ccprintf("mcdp_error:%d\n", rv);

	return EC_SUCCESS;
}
DECLARE_CONSOLE_COMMAND(mcdp, command_mcdp,
			"info|devid <id>|appstest <cmd> [<params>]",
			"USB PD");
#endif /* CONFIG_CMD_MCDP */
