/* SPDX-License-Identifier: GPL-2.0-only */

#include <arch/io.h>
#include <console/console.h>
#include <delay.h>
#include <ec/google/common/mec.h>
#include <string.h>
#include <timer.h>
#include <types.h>

#include "ec.h"

/* Mailbox ID */
#define EC_MAILBOX_ID			0x00f0

/* Version of mailbox interface */
#define EC_MAILBOX_VERSION		0

/* Command to start mailbox transaction */
#define EC_MAILBOX_START_COMMAND	0xda

/* Version of EC protocol */
#define EC_MAILBOX_PROTO_VERSION	3

/* Max number of bytes in protocol data payload */
#define EC_MAILBOX_DATA_SIZE		32

/* Number of header bytes to be counted as data bytes */
#define EC_MAILBOX_DATA_EXTRA		2

/* Maximum timeout */
#define EC_MAILBOX_TIMEOUT_MS		MSECS_PER_SEC

/* EC response flags */
#define EC_CMDR_DATA		BIT(0)	/* Data ready for host to read */
#define EC_CMDR_PENDING		BIT(1)	/* Write pending to EC */
#define EC_CMDR_BUSY		BIT(2)	/* EC is busy processing a command */
#define EC_CMDR_CMD		BIT(3)	/* Last host write was a command */

/* Request to EC */
struct wilco_ec_request {
	uint8_t struct_version;		/* version (=3) */
	uint8_t checksum;		/* sum of all bytes must be 0 */
	uint16_t mailbox_id;		/* mailbox identifier */
	uint8_t mailbox_version;	/* mailbox version (=0) */
	uint8_t reserved1;		/* unused (=0) */
	uint16_t data_size;		/* length (data + 2 bytes of header) */
	uint8_t command;		/* mailbox command */
	uint8_t reserved2;		/* unused (=0) */
} __packed;

/* Response from EC */
struct wilco_ec_response {
	uint8_t struct_version;		/* version (=3) */
	uint8_t checksum;		/* sum of all bytes must be 0 */
	uint16_t result;		/* result code */
	uint16_t data_size;		/* length of data buffer (always 32) */
	uint8_t reserved[3];		/* unused (=0) */
	uint8_t data[EC_MAILBOX_DATA_SIZE];
} __packed;

struct wilco_ec_message {
	uint8_t command;		/* mailbox command code */
	uint8_t result;			/* request result */
	size_t request_size;		/* bytes to send to the EC */
	size_t response_size;		/* bytes expected from the EC */
	enum wilco_ec_msg_type type;	/* message type */
	/*
	 * This data buffer will contain the request data when passed to
	 * wilco_ec_message() and will contain the response data on return.
	 */
	uint8_t data[EC_MAILBOX_DATA_SIZE];
};

static bool wilco_ec_response_timed_out(void)
{
	uint8_t mask = EC_CMDR_PENDING | EC_CMDR_BUSY;
	struct stopwatch sw;

	stopwatch_init_msecs_expire(&sw, EC_MAILBOX_TIMEOUT_MS);

	while (inb(CONFIG_EC_BASE_HOST_COMMAND) & mask) {
		if (stopwatch_expired(&sw)) {
			printk(BIOS_ERR, "%s: Command timeout\n", __func__);
			return true; /* Timed out */
		}
		mdelay(1);
	}

	return false; /* Did not time out */
}

static uint8_t wilco_ec_checksum(void *data, size_t size)
{
	uint8_t *data_bytes = (uint8_t *)data;
	uint8_t checksum = 0;
	size_t i;

	for (i = 0; i < size; i++)
		checksum += data_bytes[i];

	return checksum;
}

static void wilco_ec_prepare(struct wilco_ec_message *msg,
			     struct wilco_ec_request *rq)
{
	memset(rq, 0, sizeof(*rq));

	/* Fill in request packet */
	rq->struct_version = EC_MAILBOX_PROTO_VERSION;
	rq->mailbox_id = EC_MAILBOX_ID;
	rq->mailbox_version = EC_MAILBOX_VERSION;
	rq->data_size = msg->request_size + EC_MAILBOX_DATA_EXTRA;
	rq->command = msg->command;

	/* Checksum header and data */
	rq->checksum = wilco_ec_checksum(rq, sizeof(*rq));
	rq->checksum += wilco_ec_checksum(msg->data, msg->request_size);
	rq->checksum = -rq->checksum;
}

static int wilco_ec_transfer(struct wilco_ec_message *msg)
{
	struct wilco_ec_request rq;
	struct wilco_ec_response rs;
	uint8_t checksum;
	size_t skip_size;

	/* Prepare request packet */
	wilco_ec_prepare(msg, &rq);

	/* Write request header */
	mec_io_bytes(MEC_IO_WRITE, CONFIG_EC_BASE_PACKET, 0, &rq, sizeof(rq));

	/* Write request data */
	mec_io_bytes(MEC_IO_WRITE, CONFIG_EC_BASE_PACKET, sizeof(rq),
		     msg->data, msg->request_size);

	/* Start the command */
	outb(EC_MAILBOX_START_COMMAND, CONFIG_EC_BASE_HOST_COMMAND);

	/* Some commands will put the EC into a state where it cannot respond */
	if (msg->type == WILCO_EC_MSG_NO_RESPONSE) {
		printk(BIOS_DEBUG, "%s: EC does not respond to this command\n",
		       __func__);
		return 0;
	}

	/* Wait for it to complete */
	if (wilco_ec_response_timed_out()) {
		printk(BIOS_ERR, "%s: response timed out\n", __func__);
		return -1;
	}

	/* Check result */
	msg->result = inb(CONFIG_EC_BASE_HOST_DATA);
	if (msg->result != 0) {
		printk(BIOS_ERR, "%s: bad response: 0x%02x\n",
		       __func__, msg->result);
		return -1;
	}

	/* Read back response */
	checksum = mec_io_bytes(MEC_IO_READ, CONFIG_EC_BASE_PACKET, 0,
				&rs, sizeof(rs));
	if (checksum) {
		printk(BIOS_ERR, "%s: bad checksum %02x\n", __func__, checksum);
		return -1;
	}
	msg->result = rs.result;

	/* EC always returns EC_MAILBOX_DATA_SIZE bytes */
	if (rs.data_size > EC_MAILBOX_DATA_SIZE) {
		printk(BIOS_ERR, "%s: packet too long (%d bytes, expected %d)",
		       __func__, rs.data_size, EC_MAILBOX_DATA_SIZE);
		return -1;
	}

	/* Skip response data bytes as requested */
	skip_size = (msg->type == WILCO_EC_MSG_DEFAULT) ? 1 : 0;

	if (msg->response_size > rs.data_size - skip_size) {
		printk(BIOS_ERR, "%s: data too short (%zu bytes, expected %zu)",
		       __func__, rs.data_size - skip_size, msg->response_size);
		return -1;
	}

	memcpy(msg->data, rs.data + skip_size, msg->response_size);

	/* Return actual amount of data received */
	return msg->response_size;
}

int wilco_ec_mailbox(enum wilco_ec_msg_type type, uint8_t command,
		     const void *request_data, size_t request_size,
		     void *response_data, size_t response_size)
{
	struct wilco_ec_message msg = {
		.command = command,
		.request_size = request_size,
		.response_size = response_size,
		.type = type,
	};
	int ret;

	if (request_size > EC_MAILBOX_DATA_SIZE) {
		printk(BIOS_ERR, "%s: provided request data too large: %zu\n",
		       __func__, request_size);
		return -1;
	}
	if (response_size > EC_MAILBOX_DATA_SIZE) {
		printk(BIOS_ERR, "%s: expected response data too large: %zu\n",
		       __func__, response_size);
		return -1;
	}
	if (request_size && !request_data) {
		printk(BIOS_ERR, "%s: request data missing\n", __func__);
		return -1;
	}
	if (response_size && !response_data) {
		printk(BIOS_ERR, "%s: request data missing\n", __func__);
		return -1;
	}

	/* Copy request data if present */
	if (request_size)
		memcpy(msg.data, request_data, request_size);

	/* Do the EC transfer */
	ret = wilco_ec_transfer(&msg);

	/* Copy response data if present */
	if (ret > 0 && response_size)
		memcpy(response_data, msg.data, response_size);

	/* Return error if message result is non-zero */
	if (ret >= 0 && msg.result)
		ret = -1;

	return ret;
}