/*
 * Copyright (c) 2022 Nuvoton Technology Corporation.
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#define DT_DRV_COMPAT nuvoton_npcx_peci

#include <errno.h>
#include <soc.h>
#include <zephyr/device.h>
#include <zephyr/drivers/clock_control.h>
#include <zephyr/drivers/peci.h>
#include <zephyr/drivers/pinctrl.h>
#include <zephyr/kernel.h>

#include <zephyr/logging/log.h>
#include <zephyr/irq.h>
LOG_MODULE_REGISTER(peci_npcx, CONFIG_PECI_LOG_LEVEL);

#define PECI_TIMEOUT		 K_MSEC(300)
#define PECI_NPCX_MAX_TX_BUF_LEN 65
#define PECI_NPCX_MAX_RX_BUF_LEN 64

struct peci_npcx_config {
	/* peci controller base address */
	struct peci_reg *base;
	struct npcx_clk_cfg clk_cfg;
	const struct pinctrl_dev_config *pcfg;
};

struct peci_npcx_data {
	struct k_sem trans_sync_sem;
	struct k_sem lock;
	uint32_t peci_src_clk_freq;
	int trans_error;
};

enum npcx_peci_error_code {
	NPCX_PECI_NO_ERROR,
	NPCX_PECI_WR_ABORT_ERROR,
	NPCX_PECI_RD_CRC_ERROR,
};

static int peci_npcx_check_bus_idle(struct peci_reg *reg)
{
	if (IS_BIT_SET(reg->PECI_CTL_STS, NPCX_PECI_CTL_STS_START_BUSY)) {
		return -EBUSY;
	}

	return 0;
}

static int peci_npcx_wait_completion(const struct device *dev)
{
	struct peci_npcx_data *const data = dev->data;
	int ret;

	ret = k_sem_take(&data->trans_sync_sem, PECI_TIMEOUT);
	if (ret != 0) {
		LOG_ERR("%s: Timeout", __func__);
		return -ETIMEDOUT;
	}

	if (data->trans_error != NPCX_PECI_NO_ERROR) {
		return -EIO;
	}
	return 0;
}
static int peci_npcx_configure(const struct device *dev, uint32_t bitrate)
{
	const struct peci_npcx_config *const config = dev->config;
	struct peci_npcx_data *const data = dev->data;
	struct peci_reg *const reg = config->base;
	uint8_t bit_rate_divider;

	k_sem_take(&data->lock, K_FOREVER);

	/*
	 * The unit of the bitrate is in Kbps, need to convert it to bps when
	 * calculate the divider
	 */
	bit_rate_divider = DIV_ROUND_UP(data->peci_src_clk_freq, bitrate * 1000 * 4) - 1;
	/*
	 * Make sure the divider doesn't exceed the max valid value and is not lower than the
	 * minimal valid value.
	 */
	bit_rate_divider = CLAMP(bit_rate_divider, PECI_MAX_BIT_RATE_VALID_MIN,
				 NPCX_PECI_RATE_MAX_BIT_RATE_MASK);

	if (bit_rate_divider < PECI_HIGH_SPEED_MIN_VAL) {
		reg->PECI_RATE |= BIT(NPCX_PECI_RATE_EHSP);
	} else {
		reg->PECI_RATE &= ~BIT(NPCX_PECI_RATE_EHSP);
	}
	SET_FIELD(reg->PECI_RATE, NPCX_PECI_RATE_MAX_BIT_RATE, bit_rate_divider);

	k_sem_give(&data->lock);

	return 0;
}

static int peci_npcx_disable(const struct device *dev)
{
	struct peci_npcx_data *const data = dev->data;

	k_sem_take(&data->lock, K_FOREVER);

	irq_disable(DT_INST_IRQN(0));

	k_sem_give(&data->lock);

	return 0;
}

static int peci_npcx_enable(const struct device *dev)
{
	const struct peci_npcx_config *const config = dev->config;
	struct peci_npcx_data *const data = dev->data;
	struct peci_reg *const reg = config->base;

	k_sem_take(&data->lock, K_FOREVER);

	reg->PECI_CTL_STS = BIT(NPCX_PECI_CTL_STS_DONE) | BIT(NPCX_PECI_CTL_STS_CRC_ERR) |
			    BIT(NPCX_PECI_CTL_STS_ABRT_ERR);
	NVIC_ClearPendingIRQ(DT_INST_IRQN(0));
	irq_enable(DT_INST_IRQN(0));

	k_sem_give(&data->lock);

	return 0;
}

static int peci_npcx_transfer(const struct device *dev, struct peci_msg *msg)
{
	const struct peci_npcx_config *const config = dev->config;
	struct peci_npcx_data *const data = dev->data;
	struct peci_reg *const reg = config->base;
	struct peci_buf *peci_rx_buf = &msg->rx_buffer;
	struct peci_buf *peci_tx_buf = &msg->tx_buffer;
	enum peci_command_code cmd_code = msg->cmd_code;
	int ret = 0;

	k_sem_take(&data->lock, K_FOREVER);

	if (peci_tx_buf->len > PECI_NPCX_MAX_TX_BUF_LEN ||
	    peci_rx_buf->len > PECI_NPCX_MAX_RX_BUF_LEN) {
		ret = -EINVAL;
		goto out;
	}

	ret = peci_npcx_check_bus_idle(reg);
	if (ret != 0) {
		goto out;
	}

	reg->PECI_ADDR = msg->addr;
	reg->PECI_WR_LENGTH = peci_tx_buf->len;
	reg->PECI_RD_LENGTH = peci_rx_buf->len;
	reg->PECI_CMD = cmd_code;

	/*
	 * If command = PING command:
	 *      Tx buffer length = 0.
	 * Otherwise:
	 *      Tx buffer length = N-bytes data + 1 byte command code.
	 */
	if (peci_tx_buf->len != 0) {
		for (int i = 0; i < (peci_tx_buf->len - 1); i++) {
			reg->PECI_DATA_OUT[i] = peci_tx_buf->buf[i];
		}
	}

	/* Enable PECI transaction done interrupt */
	reg->PECI_CTL_STS |= BIT(NPCX_PECI_CTL_STS_DONE_EN);
	/* Start PECI transaction */
	reg->PECI_CTL_STS |= BIT(NPCX_PECI_CTL_STS_START_BUSY);

	ret = peci_npcx_wait_completion(dev);
	if (ret == 0) {
		int i;

		for (i = 0; i < peci_rx_buf->len; i++) {
			peci_rx_buf->buf[i] = reg->PECI_DATA_IN[i];
		}
		/*
		 * The application allocates N+1 bytes for rx_buffer.
		 * The read data block is stored at the offset 0 ~ (N-1).
		 * The read block FCS is stored at offset N.
		 */
		peci_rx_buf->buf[i] = reg->PECI_RD_FCS;
		LOG_DBG("Wr FCS:0x%02x|Rd FCS:0x%02x", reg->PECI_WR_FCS, reg->PECI_RD_FCS);
	}

out:
	k_sem_give(&data->lock);
	return ret;
}

static void peci_npcx_isr(const struct device *dev)
{
	const struct peci_npcx_config *const config = dev->config;
	struct peci_npcx_data *const data = dev->data;
	struct peci_reg *const reg = config->base;
	uint8_t status;

	status = reg->PECI_CTL_STS;
	LOG_DBG("PECI ISR status: 0x%02x", status);
	/*
	 * Disable the transaction done interrupt, also clear the status bits
	 * if they were set.
	 */
	reg->PECI_CTL_STS &= ~BIT(NPCX_PECI_CTL_STS_DONE_EN);

	if (IS_BIT_SET(status, NPCX_PECI_CTL_STS_ABRT_ERR)) {
		data->trans_error = NPCX_PECI_WR_ABORT_ERROR;
		LOG_ERR("PECI Nego or Wr FCS(0x%02x) error", reg->PECI_WR_FCS);
	} else if (IS_BIT_SET(status, NPCX_PECI_CTL_STS_CRC_ERR)) {
		data->trans_error = NPCX_PECI_RD_CRC_ERROR;
		LOG_ERR("PECI Rd FCS(0x%02x) error", reg->PECI_WR_FCS);
	} else {
		data->trans_error = NPCX_PECI_NO_ERROR;
	}

	k_sem_give(&data->trans_sync_sem);
}

static const struct peci_driver_api peci_npcx_driver_api = {
	.config = peci_npcx_configure,
	.enable = peci_npcx_enable,
	.disable = peci_npcx_disable,
	.transfer = peci_npcx_transfer,
};

static int peci_npcx_init(const struct device *dev)
{
	const struct device *const clk_dev = DEVICE_DT_GET(NPCX_CLK_CTRL_NODE);
	const struct peci_npcx_config *const config = dev->config;
	struct peci_npcx_data *const data = dev->data;
	int ret;

	if (!device_is_ready(clk_dev)) {
		LOG_ERR("%s device not ready", clk_dev->name);
		return -ENODEV;
	}

	ret = clock_control_on(clk_dev, (clock_control_subsys_t)&config->clk_cfg);
	if (ret < 0) {
		LOG_ERR("Turn on PECI clock fail %d", ret);
		return ret;
	}

	ret = clock_control_get_rate(clk_dev, (clock_control_subsys_t)&config->clk_cfg,
				     &data->peci_src_clk_freq);
	if (ret < 0) {
		LOG_ERR("Get PECI source clock rate error %d", ret);
		return ret;
	}

	ret = pinctrl_apply_state(config->pcfg, PINCTRL_STATE_DEFAULT);

	if (ret != 0) {
		LOG_ERR("NPCX PECI pinctrl init failed (%d)", ret);
		return ret;
	}

	k_sem_init(&data->trans_sync_sem, 0, 1);
	k_sem_init(&data->lock, 1, 1);

	IRQ_CONNECT(DT_INST_IRQN(0), DT_INST_IRQ(0, priority), peci_npcx_isr, DEVICE_DT_INST_GET(0),
		    0);

	return 0;
}

static struct peci_npcx_data peci_npcx_data0;

PINCTRL_DT_INST_DEFINE(0);

static const struct peci_npcx_config peci_npcx_config0 = {
	.base = (struct peci_reg *)DT_INST_REG_ADDR(0),
	.clk_cfg = NPCX_DT_CLK_CFG_ITEM(0),
	.pcfg = PINCTRL_DT_INST_DEV_CONFIG_GET(0),
};

DEVICE_DT_INST_DEFINE(0, &peci_npcx_init, NULL, &peci_npcx_data0, &peci_npcx_config0, POST_KERNEL,
		      CONFIG_PECI_INIT_PRIORITY, &peci_npcx_driver_api);

BUILD_ASSERT(DT_NUM_INST_STATUS_OKAY(DT_DRV_COMPAT) == 1,
	     "only one 'nuvoton_npcx_peci' compatible node can be supported");
