/* Copyright 2018 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.
 *
 * Cros Board Info
 */

#include "common.h"
#include "console.h"
#include "crc8.h"
#include "cros_board_info.h"
#include "gpio.h"
#include "host_command.h"
#include "i2c.h"
#include "util.h"

#define CPRINTS(format, args...) cprints(CC_SYSTEM, "CBI " format, ## args)

#define EEPROM_PAGE_WRITE_SIZE	16
#define EC_ERROR_CBI_CACHE_INVALID	EC_ERROR_INTERNAL_FIRST
static struct board_info bi;
static int cached_read_result = EC_ERROR_CBI_CACHE_INVALID;

static uint8_t cbi_crc8(const struct board_info *bi)
{
	return crc8((uint8_t *)&bi->head.crc + 1, bi->head.total_size - 4);
}

/*
 * Get board information from EEPROM
 */
static int do_read_board_info(void)
{
	uint8_t buf[256];
	uint8_t offset;

	CPRINTS("Reading board info");

	/* Read header */
	offset = 0;
	if (i2c_xfer(I2C_PORT_EEPROM, I2C_ADDR_EEPROM,
		     &offset, 1, buf, sizeof(bi.head), I2C_XFER_SINGLE)) {
		CPRINTS("Failed to read header");
		return EC_ERROR_INVAL;
	}
	memcpy(&bi.head, buf, sizeof(bi.head));

	/* Check magic */
	if (memcmp(bi.head.magic, cbi_magic, sizeof(bi.head.magic))) {
		CPRINTS("Bad magic");
		return EC_ERROR_INVAL;
	}

	/* check version */
	if (bi.head.major_version > CBI_VERSION_MAJOR) {
		CPRINTS("Version mismatch");
		return EC_ERROR_INVAL;
	}

	/* Check the data size. It's expected to support up to 64k but our
	 * buffer has practical limitation. */
	if (bi.head.total_size < sizeof(bi) ||
			bi.head.total_size > sizeof(buf)) {
		CPRINTS("Bad size: %d", bi.head.total_size);
		return EC_ERROR_OVERFLOW;
	}

	/* Read the rest */
	offset = sizeof(bi.head);
	if (i2c_xfer(I2C_PORT_EEPROM, I2C_ADDR_EEPROM, &offset, 1,
		     buf + sizeof(bi.head),
		     bi.head.total_size - sizeof(bi.head),
		     I2C_XFER_SINGLE)) {
		CPRINTS("Failed to read body");
		return EC_ERROR_INVAL;
	}

	/* Check CRC. This supports new fields unknown to this parser. */
	if (cbi_crc8((struct board_info *)&buf) != bi.head.crc) {
		CPRINTS("Bad CRC");
		return EC_ERROR_INVAL;
	}

	/* Save only the data we understand. */
	memcpy(&bi.head + 1, &buf[sizeof(bi.head)],
	       sizeof(bi) - sizeof(bi.head));
	/* If we're handling previous version, clear all new fields */

	return EC_SUCCESS;
}

static int read_board_info(void)
{
	if (cached_read_result != EC_ERROR_CBI_CACHE_INVALID)
		/* We already tried and know the result. Return the cached
		 * error code immediately to avoid wasteful reads. */
		return cached_read_result;

	cached_read_result = do_read_board_info();
	if (cached_read_result)
		/* On error (I2C or bad contents), retry a read */
		cached_read_result = do_read_board_info();

	return cached_read_result;
}

static int eeprom_is_write_protected(void)
{
	return !gpio_get_level(GPIO_WP_L);
}

static int write_board_info(void)
{
	uint8_t buf[sizeof(bi) + 1];
	/* The code is only tested for ST M24C02, whose page size for a single
	 * write is 16 byte. To support different EEPROMs, you may need to
	 * craft the i2c packets accordingly. */
	_Static_assert(sizeof(bi) <= EEPROM_PAGE_WRITE_SIZE,
		       "struct board_info exceeds page write size");
	int rv;

	if (eeprom_is_write_protected()) {
		CPRINTS("Failed to write for WP");
		return EC_ERROR_ACCESS_DENIED;
	}

	buf[0] = 0;	/* Offset 0 */
	memcpy(&buf[1], &bi, sizeof(bi));
	rv = i2c_xfer(I2C_PORT_EEPROM, I2C_ADDR_EEPROM, buf,
		     sizeof(bi) + 1, NULL, 0, I2C_XFER_SINGLE);
	if (rv) {
		CPRINTS("Failed to write for %d", rv);
		return rv;
	}

	return EC_SUCCESS;
}

int cbi_get_board_version(uint32_t *version)
{
	if (read_board_info())
		return EC_ERROR_UNKNOWN;
	*version = bi.version;
	return EC_SUCCESS;
}

int cbi_get_sku_id(uint32_t *sku_id)
{
	if (read_board_info())
		return EC_ERROR_UNKNOWN;
	*sku_id = bi.sku_id;
	return EC_SUCCESS;
}

int cbi_get_oem_id(uint32_t *oem_id)
{
	if (read_board_info())
		return EC_ERROR_UNKNOWN;
	*oem_id = bi.oem_id;
	return EC_SUCCESS;
}

int board_get_version(void)
{
	uint32_t version;
	if (cbi_get_board_version(&version))
		return -1;
	return version;
}

static int hc_cbi_get(struct host_cmd_handler_args *args)
{
	const struct __ec_align4 ec_params_get_cbi *p = args->params;

	if (p->flag & CBI_GET_RELOAD)
		cached_read_result = EC_ERROR_CBI_CACHE_INVALID;

	if (read_board_info())
		return EC_RES_ERROR;

	switch (p->type) {
	case CBI_DATA_BOARD_VERSION:
		*(uint32_t *)args->response = bi.version;
		break;
	case CBI_DATA_OEM_ID:
		*(uint32_t *)args->response = bi.oem_id;
		break;
	case CBI_DATA_SKU_ID:
		*(uint32_t *)args->response = bi.sku_id;
		break;
	default:
		return EC_RES_INVALID_PARAM;
	}
	args->response_size = sizeof(uint32_t);

	return EC_RES_SUCCESS;
}
DECLARE_HOST_COMMAND(EC_CMD_GET_CROS_BOARD_INFO,
		     hc_cbi_get,
		     EC_VER_MASK(0));

static int hc_cbi_set(struct host_cmd_handler_args *args)
{
	const struct __ec_align4 ec_params_set_cbi *p = args->params;
	int rv;

	if (p->flag & CBI_SET_INIT) {
		memset(&bi, 0, sizeof(bi));
		memcpy(&bi.head.magic, cbi_magic, sizeof(cbi_magic));
		cached_read_result = EC_SUCCESS;
	} else {
		if (read_board_info())
			return EC_RES_ERROR;
	}

	switch (p->type) {
	case CBI_DATA_BOARD_VERSION:
		if (p->data > UINT16_MAX)
			return EC_RES_INVALID_PARAM;
		bi.version = p->data;
		break;
	case CBI_DATA_OEM_ID:
		if (p->data > UINT8_MAX)
			return EC_RES_INVALID_PARAM;
		bi.oem_id = p->data;
		break;
	case CBI_DATA_SKU_ID:
		if (p->data > UINT8_MAX)
			return EC_RES_INVALID_PARAM;
		bi.sku_id = p->data;
		break;
	default:
		return EC_RES_INVALID_PARAM;
	}

	/* Whether we're modifying existing data or creating new one,
	 * we take over the format. */
	bi.head.major_version = CBI_VERSION_MAJOR;
	bi.head.minor_version = CBI_VERSION_MINOR;
	bi.head.total_size = sizeof(bi);
	bi.head.crc = cbi_crc8(&bi);

	/* Skip write if client asks so. */
	if (p->flag & CBI_SET_NO_SYNC)
		return EC_RES_SUCCESS;

	rv = write_board_info();
	if (rv)
		return (rv == EC_ERROR_ACCESS_DENIED) ?
				EC_RES_ACCESS_DENIED : EC_RES_ERROR;

	return EC_RES_SUCCESS;
}
DECLARE_HOST_COMMAND(EC_CMD_SET_CROS_BOARD_INFO,
		     hc_cbi_set,
		     EC_VER_MASK(0));

static int command_dump_cbi(int argc, char **argv)
{
	ccprintf("BOARD_VERSION: %u (0x%x)\n", bi.version, bi.version);
	ccprintf("OEM_ID: %u (0x%x)\n", bi.oem_id, bi.oem_id);
	ccprintf("SKU_ID: %u (0x%x)\n", bi.sku_id, bi.sku_id);
	return EC_SUCCESS;
}
DECLARE_CONSOLE_COMMAND(cbi, command_dump_cbi, NULL, NULL);
