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

#define DT_DRV_COMPAT nuvoton_numaker_spi

#include <errno.h>
#include <zephyr/kernel.h>
#include <zephyr/drivers/reset.h>
#include <zephyr/drivers/clock_control.h>
#include <zephyr/drivers/clock_control/clock_control_numaker.h>
#include <zephyr/drivers/pinctrl.h>
#include <zephyr/logging/log.h>

LOG_MODULE_REGISTER(spi_numaker, CONFIG_SPI_LOG_LEVEL);

#include "spi_context.h"
#include <NuMicro.h>

#define SPI_NUMAKER_TX_NOP 0x00

struct spi_numaker_config {
	SPI_T *spi;
	bool is_qspi;
	const struct reset_dt_spec reset;
	/* clock configuration */
	uint32_t clk_modidx;
	uint32_t clk_src;
	uint32_t clk_div;
	const struct device *clk_dev;
	const struct pinctrl_dev_config *pincfg;
};

struct spi_numaker_data {
	struct spi_context ctx;
};

/*
 * CPOL/CPHA = 0/0 --> SPI_MODE_0
 * CPOL/CPHA = 0/1 --> SPI_MODE_1
 * CPOL/CPHA = 1/0 --> SPI_MODE_2
 * CPOL/CPHA = 1/1 --> SPI_MODE_3
 */
static const uint32_t smode_tbl[4] = {
	SPI_MODE_0, SPI_MODE_1, SPI_MODE_2, SPI_MODE_3
};

static const uint32_t qsmode_tbl[4] = {
	QSPI_MODE_0, QSPI_MODE_1, QSPI_MODE_2, QSPI_MODE_3
};

static int spi_numaker_configure(const struct device *dev, const struct spi_config *config)
{
	int mode;
	struct spi_numaker_data *data = dev->data;
	const struct spi_numaker_config *dev_cfg = dev->config;

	LOG_DBG("%s", __func__);
	if (spi_context_configured(&data->ctx, config)) {
		return 0;
	}

	if (SPI_MODE_GET(config->operation) & SPI_MODE_LOOP) {
		LOG_ERR("Loop back mode not support");
		return -ENOTSUP;
	}

	if (SPI_OP_MODE_GET(config->operation) == SPI_OP_MODE_SLAVE) {
		LOG_ERR("Slave mode not support");
		return -ENOTSUP;
	}

	/* Clear FIFO */
	SPI_ClearRxFIFO(dev_cfg->spi);
	SPI_ClearTxFIFO(dev_cfg->spi);

	if (SPI_MODE_GET(config->operation) & SPI_MODE_CPOL) {
		mode = (SPI_MODE_GET(config->operation) & SPI_MODE_CPHA) ? 3 : 2;
	} else {
		mode = (SPI_MODE_GET(config->operation) & SPI_MODE_CPHA) ? 1 : 0;
	}

	/* Make SPI module be ready to transfer */
	if (dev_cfg->is_qspi) {
		QSPI_Open((QSPI_T *)dev_cfg->spi,
			  (SPI_OP_MODE_GET(config->operation) == SPI_OP_MODE_SLAVE) ? QSPI_SLAVE
										    : QSPI_MASTER,
			  qsmode_tbl[mode],
			  SPI_WORD_SIZE_GET(config->operation), config->frequency);
	} else {
		SPI_Open(dev_cfg->spi,
			 (SPI_OP_MODE_GET(config->operation) == SPI_OP_MODE_SLAVE) ? SPI_SLAVE
										   : SPI_MASTER,
			 smode_tbl[mode],
			 SPI_WORD_SIZE_GET(config->operation), config->frequency);
	}

	/* Set Transfer LSB or MSB first */
	if ((config->operation) & SPI_TRANSFER_LSB) {
		SPI_SET_LSB_FIRST(dev_cfg->spi);
	} else {
		SPI_SET_MSB_FIRST(dev_cfg->spi);
	}

	/* full/half duplex */
	if (config->operation & SPI_HALF_DUPLEX) {
		/* half duplex, which results in 3-wire usage */
		SPI_ENABLE_3WIRE_MODE(dev_cfg->spi);
	} else {
		/* full duplex */
		SPI_DISABLE_3WIRE_MODE(dev_cfg->spi);
	}

	/* Active high CS logic */
	if (config->operation & SPI_CS_ACTIVE_HIGH) {
		SPI_SET_SS_HIGH(dev_cfg->spi);
	} else {
		SPI_SET_SS_LOW(dev_cfg->spi);
	}

	/* Enable the automatic hardware slave select function. Select the SS pin and configure as
	 * low-active.
	 */
	if (data->ctx.num_cs_gpios != 0) {
		SPI_EnableAutoSS(dev_cfg->spi, SPI_SS, SPI_SS_ACTIVE_LOW);
	} else {
		SPI_DisableAutoSS(dev_cfg->spi);
	}

	/* Be able to set TX/RX FIFO threshold, for ex: SPI_SetFIFO(dev_cfg->spi, 2, 2) */

	data->ctx.config = config;

	return 0;
}

static int spi_numaker_txrx(const struct device *dev)
{
	struct spi_numaker_data *data = dev->data;
	const struct spi_numaker_config *dev_cfg = dev->config;
	struct spi_context *ctx = &data->ctx;
	uint32_t tx_frame, rx_frame;
	uint8_t word_size, spi_dfs;
	uint32_t time_out_cnt;

	LOG_DBG("%s", __func__);
	word_size = SPI_WORD_SIZE_GET(ctx->config->operation);

	switch (word_size) {
	case 8:
		spi_dfs = 1;
		break;
	case 16:
		spi_dfs = 2;
		break;
	case 24:
		spi_dfs = 3;
		break;
	case 32:
		spi_dfs = 4;
		break;
	default:
		spi_dfs = 0;
		LOG_ERR("Not support SPI WORD size as [%d] bits", word_size);
		return -EIO;
	}

	LOG_DBG("%s -->word_size [%d]", __func__, word_size);

	if (spi_context_tx_on(ctx)) {
		tx_frame = ((ctx->tx_buf == NULL) ? SPI_NUMAKER_TX_NOP
						  : UNALIGNED_GET((uint8_t *)(data->ctx.tx_buf)));
		/* Write to TX register */
		SPI_WRITE_TX(dev_cfg->spi, tx_frame);
		spi_context_update_tx(ctx, spi_dfs, 1);

		/* Check SPI busy status */
		time_out_cnt = SystemCoreClock; /* 1 second time-out */
		while (SPI_IS_BUSY(dev_cfg->spi)) {
			if (--time_out_cnt == 0) {
				LOG_ERR("Wait for SPI time-out");
				return -EIO;
			}
		}

		LOG_DBG("%s --> TX [0x%x] done", __func__, tx_frame);
	} else {
		/* Write dummy data to TX register */
		SPI_WRITE_TX(dev_cfg->spi, 0x00U);
		time_out_cnt = SystemCoreClock; /* 1 second time-out */
		while (SPI_IS_BUSY(dev_cfg->spi)) {
			if (--time_out_cnt == 0) {
				LOG_ERR("Wait for SPI time-out");
				return -EIO;
			}
		}
	}

	/* Read received data */
	if (spi_context_rx_on(ctx)) {
		if (SPI_GET_RX_FIFO_COUNT(dev_cfg->spi) > 0) {
			rx_frame = SPI_READ_RX(dev_cfg->spi);
			if (ctx->rx_buf != NULL) {
				UNALIGNED_PUT(rx_frame, (uint8_t *)data->ctx.rx_buf);
			}
			spi_context_update_rx(ctx, spi_dfs, 1);
			LOG_DBG("%s --> RX [0x%x] done", __func__, rx_frame);
		}
	}

	LOG_DBG("%s --> exit", __func__);
	return 0;
}

/* Remain TX/RX Data in spi_context TX/RX buffer */
static bool spi_numaker_remain_words(struct spi_numaker_data *data)
{
	return spi_context_tx_on(&data->ctx) || spi_context_rx_on(&data->ctx);
}

static int spi_numaker_transceive(const struct device *dev, const struct spi_config *config,
				  const struct spi_buf_set *tx_bufs,
				  const struct spi_buf_set *rx_bufs)
{
	struct spi_numaker_data *data = dev->data;
	struct spi_context *ctx = &data->ctx;
	const struct spi_numaker_config *dev_cfg = dev->config;
	int ret;

	LOG_DBG("%s", __func__);
	spi_context_lock(ctx, false, NULL, NULL, config);
	ctx->config = config;

	ret = spi_numaker_configure(dev, config);
	if (ret < 0) {
		goto done;
	}

	SPI_ENABLE(dev_cfg->spi);

	spi_context_buffers_setup(&data->ctx, tx_bufs, rx_bufs, 1);

	/* if cs is defined: software cs control, set active true */
	if (spi_cs_is_gpio(config)) {
		spi_context_cs_control(&data->ctx, true);
	}

	/* transceive tx/rx data */
	do {
		ret = spi_numaker_txrx(dev);
		if (ret < 0) {
			break;
		}
	} while (spi_numaker_remain_words(data));

	/* if cs is defined: software cs control, set active false */
	if (spi_cs_is_gpio(config)) {
		spi_context_cs_control(&data->ctx, false);
	}
	SPI_DISABLE(dev_cfg->spi);

done:
	spi_context_release(ctx, ret);
	LOG_DBG("%s --> [%d]", __func__, ret);
	return ret;
}

static int spi_numaker_release(const struct device *dev, const struct spi_config *config)
{
	struct spi_numaker_data *data = dev->data;
	struct spi_context *ctx = &data->ctx;

	if (!spi_context_configured(ctx, config)) {
		return -EINVAL;
	}
	spi_context_unlock_unconditionally(ctx);

	return 0;
}

static const struct spi_driver_api spi_numaker_driver_api = {
	.transceive = spi_numaker_transceive,
	.release = spi_numaker_release
};

static int spi_numaker_init(const struct device *dev)
{
	struct spi_numaker_data *data = dev->data;
	const struct spi_numaker_config *dev_cfg = dev->config;
	int err = 0;
	struct numaker_scc_subsys scc_subsys;

	SYS_UnlockReg();

	/* CLK controller */
	memset(&scc_subsys, 0x00, sizeof(scc_subsys));
	scc_subsys.subsys_id = NUMAKER_SCC_SUBSYS_ID_PCC;
	scc_subsys.pcc.clk_modidx = dev_cfg->clk_modidx;
	scc_subsys.pcc.clk_src = dev_cfg->clk_src;
	scc_subsys.pcc.clk_div = dev_cfg->clk_div;

	/* Equivalent to CLK_EnableModuleClock() */
	err = clock_control_on(dev_cfg->clk_dev, (clock_control_subsys_t)&scc_subsys);
	if (err != 0) {
		goto done;
	}
	/* Equivalent to CLK_SetModuleClock() */
	err = clock_control_configure(dev_cfg->clk_dev, (clock_control_subsys_t)&scc_subsys, NULL);
	if (err != 0) {
		goto done;
	}

	err = pinctrl_apply_state(dev_cfg->pincfg, PINCTRL_STATE_DEFAULT);
	if (err) {
		LOG_ERR("Failed to apply pinctrl state");
		goto done;
	}

	err = spi_context_cs_configure_all(&data->ctx);
	if (err < 0) {
		goto done;
	}

	spi_context_unlock_unconditionally(&data->ctx);

	/* Reset this module, same as BSP's SYS_ResetModule(id_rst) */
	if (!device_is_ready(dev_cfg->reset.dev)) {
		LOG_ERR("reset controller not ready");
		err = -ENODEV;
		goto done;
	}

	/* Reset SPI to default state */
	reset_line_toggle_dt(&dev_cfg->reset);

done:
	SYS_LockReg();
	return err;
}

#define NUMAKER_SPI_INIT(inst)                                                                     \
	PINCTRL_DT_INST_DEFINE(inst);                                                              \
	static struct spi_numaker_data spi_numaker_data_##inst = {                                 \
		SPI_CONTEXT_INIT_LOCK(spi_numaker_data_##inst, ctx),                               \
		SPI_CONTEXT_INIT_SYNC(spi_numaker_data_##inst, ctx),                               \
		SPI_CONTEXT_CS_GPIOS_INITIALIZE(DT_DRV_INST(inst), ctx)};                          \
	static struct spi_numaker_config spi_numaker_config_##inst = {                             \
		.spi = (SPI_T *)DT_INST_REG_ADDR(inst),                                            \
		.is_qspi = DT_INST_NODE_HAS_PROP(inst, qspi),                                      \
		.reset = RESET_DT_SPEC_INST_GET(inst),                                             \
		.clk_modidx = DT_INST_CLOCKS_CELL(inst, clock_module_index),                       \
		.clk_src = DT_INST_CLOCKS_CELL(inst, clock_source),                                \
		.clk_div = DT_INST_CLOCKS_CELL(inst, clock_divider),                               \
		.clk_dev = DEVICE_DT_GET(DT_PARENT(DT_INST_CLOCKS_CTLR(inst))),                    \
		.pincfg = PINCTRL_DT_INST_DEV_CONFIG_GET(inst),                                    \
	};                                                                                         \
	DEVICE_DT_INST_DEFINE(inst, &spi_numaker_init, NULL, &spi_numaker_data_##inst,             \
			      &spi_numaker_config_##inst, POST_KERNEL, CONFIG_SPI_INIT_PRIORITY,   \
			      &spi_numaker_driver_api);

DT_INST_FOREACH_STATUS_OKAY(NUMAKER_SPI_INIT)
