/*
 * Copyright 2023 Cypress Semiconductor Corporation (an Infineon company) or
 * an affiliate of Cypress Semiconductor Corporation
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#define DT_DRV_COMPAT infineon_cat1_spi

#define LOG_LEVEL CONFIG_SPI_LOG_LEVEL
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(cat1_spi);

#include "spi_context.h"

#include <zephyr/drivers/pinctrl.h>
#include <zephyr/drivers/spi.h>
#include <zephyr/kernel.h>

#include <cyhal_scb_common.h>
#include <cyhal_spi.h>

#define IFX_CAT1_SPI_LOCK_TMOUT_MS      (30 * 1000)
#define IFX_CAT1_SPI_DEFAULT_OVERSAMPLE (4)
#define IFX_CAT1_SPI_MIN_DATA_WIDTH     (8)
#define IFX_CAT1_SPI_MAX_DATA_WIDTH     (32)

/* Device config structure */
struct ifx_cat1_spi_config {
	CySCB_Type *reg_addr;
	const struct pinctrl_dev_config *pcfg;
	cy_stc_scb_spi_config_t scb_spi_config;
	uint8_t irq_priority;
};

/* Data structure */
struct ifx_cat1_spi_data {
	struct spi_context ctx;
	cyhal_spi_t obj; /* SPI CYHAL object */
	cyhal_resource_inst_t hw_resource;
	uint8_t dfs_value;
	size_t chunk_len;
};

static int32_t get_hw_block_num(CySCB_Type *reg_addr)
{
	uint32_t i;

	for (i = 0u; i < _SCB_ARRAY_SIZE; i++) {
		if (_CYHAL_SCB_BASE_ADDRESSES[i] == reg_addr) {
			return i;
		}
	}

	return -ENOMEM;
}

static uint8_t get_dfs_value(struct spi_context *ctx)
{
	switch (SPI_WORD_SIZE_GET(ctx->config->operation)) {
	case 8:
		return 1;
	case 16:
		return 2;
	case 32:
		return 4;
	default:
		return 1;
	}
}

static void transfer_chunk(const struct device *dev)
{
	struct ifx_cat1_spi_data *const data = dev->data;
	struct spi_context *ctx = &data->ctx;
	int ret = 0;
	size_t chunk_len = spi_context_max_continuous_chunk(ctx);

	if (chunk_len == 0) {
		goto exit;
	}

	data->chunk_len = chunk_len;

	cy_rslt_t result = cyhal_spi_transfer_async(
		&data->obj, ctx->tx_buf, spi_context_tx_buf_on(ctx) ? chunk_len : 0, ctx->rx_buf,
		spi_context_rx_buf_on(ctx) ? chunk_len : 0);
	if (result == CY_RSLT_SUCCESS) {
		return;
	}

	ret = -EIO;

exit:
	spi_context_cs_control(ctx, false);
	spi_context_complete(ctx, dev, ret);
}

static void spi_interrupt_callback(void *arg, cyhal_spi_event_t event)
{
	const struct device *dev = (const struct device *)arg;
	struct ifx_cat1_spi_data *const data = dev->data;
	struct spi_context *ctx = &data->ctx;

	if (event & CYHAL_SPI_IRQ_ERROR) {
#if defined(CONFIG_SPI_ASYNC)
		cyhal_spi_abort_async(&data->obj);
#endif
		spi_context_cs_control(ctx, false);
		spi_context_complete(ctx, dev, -EIO);
	}

	if (event & CYHAL_SPI_IRQ_DONE) {
		spi_context_update_tx(ctx, data->dfs_value, data->chunk_len);
		spi_context_update_rx(ctx, data->dfs_value, data->chunk_len);

		transfer_chunk(dev);
	}
}

int spi_config(const struct device *dev, const struct spi_config *spi_cfg)
{
	cy_rslt_t result;
	struct ifx_cat1_spi_data *const data = dev->data;
	const struct ifx_cat1_spi_config *const config = dev->config;
	cy_stc_scb_spi_config_t scb_spi_config = config->scb_spi_config;
	struct spi_context *ctx = &data->ctx;
	bool spi_mode_cpol = false;
	bool spi_mode_cpha = false;

	/* check if configuration was changed from previous run, if so skip setup again */
	if (spi_context_configured(ctx, spi_cfg)) {
		/* Already configured. No need to do it again. */
		return 0;
	}

	if (SPI_MODE_GET(spi_cfg->operation) & SPI_MODE_LOOP) {
		return -ENOTSUP;
	}

	if (SPI_WORD_SIZE_GET(spi_cfg->operation) > IFX_CAT1_SPI_MAX_DATA_WIDTH) {
		LOG_ERR("Word size %d is greater than %d", SPI_WORD_SIZE_GET(spi_cfg->operation),
			IFX_CAT1_SPI_MAX_DATA_WIDTH);
		return -EINVAL;
	}

	if (SPI_WORD_SIZE_GET(spi_cfg->operation) < IFX_CAT1_SPI_MIN_DATA_WIDTH) {
		LOG_ERR("Word size %d is less than %d", SPI_WORD_SIZE_GET(spi_cfg->operation),
			IFX_CAT1_SPI_MIN_DATA_WIDTH);
		return -EINVAL;
	}

	if (SPI_OP_MODE_GET(spi_cfg->operation) == SPI_OP_MODE_SLAVE) {
		scb_spi_config.spiMode = CY_SCB_SPI_SLAVE;
		scb_spi_config.oversample = 0;
		scb_spi_config.enableMisoLateSample = false;
	} else {
		scb_spi_config.spiMode = CY_SCB_SPI_MASTER;
	}

	if (SPI_MODE_GET(spi_cfg->operation) & SPI_MODE_CPOL) {
		spi_mode_cpol = true;
	}

	if (SPI_MODE_GET(spi_cfg->operation) & SPI_MODE_CPHA) {
		spi_mode_cpha = true;
	}

	if (SPI_WORD_SIZE_GET(spi_cfg->operation)) {
		scb_spi_config.txDataWidth = SPI_WORD_SIZE_GET(spi_cfg->operation);
		scb_spi_config.rxDataWidth = SPI_WORD_SIZE_GET(spi_cfg->operation);
	}

	if (spi_mode_cpha) {
		scb_spi_config.sclkMode =
			spi_mode_cpol ? CY_SCB_SPI_CPHA1_CPOL1 : CY_SCB_SPI_CPHA1_CPOL0;
	} else {
		scb_spi_config.sclkMode =
			spi_mode_cpol ? CY_SCB_SPI_CPHA0_CPOL1 : CY_SCB_SPI_CPHA0_CPOL0;
	}

	scb_spi_config.enableMsbFirst = (spi_cfg->operation & SPI_TRANSFER_LSB) ? false : true;

	/* Force free resource */
	if (data->obj.base != NULL) {
		cyhal_spi_free(&data->obj);
	}

	/* Initialize the SPI peripheral */
	cyhal_spi_configurator_t spi_init_cfg = {.resource = &data->hw_resource,
						 .config = &scb_spi_config,
						 .gpios = {NC, {NC, NC, NC, NC}, NC, NC}};

	result = cyhal_spi_init_cfg(&data->obj, &spi_init_cfg);
	if (result != CY_RSLT_SUCCESS) {
		return -ENOTSUP;
	}

	/* Assigns a programmable divider to a selected IP block */
	en_clk_dst_t clk_idx = _cyhal_scb_get_clock_index(spi_init_cfg.resource->block_num);

	result = _cyhal_utils_peri_pclk_assign_divider(clk_idx, &data->obj.clock);
	if (result != CY_RSLT_SUCCESS) {
		return -ENOTSUP;
	}

	/* Configure Slave select polarity */
	if (SPI_OP_MODE_GET(spi_cfg->operation) == SPI_OP_MODE_SLAVE) {
		Cy_SCB_SPI_SetActiveSlaveSelectPolarity(data->obj.base, CY_SCB_SPI_SLAVE_SELECT0,
							scb_spi_config.ssPolarity);
	}

	/* Set the data rate */
	result = cyhal_spi_set_frequency(&data->obj, spi_cfg->frequency);
	if (result != CY_RSLT_SUCCESS) {
		return -EIO;
	}

	/* Write 0 when NULL buffer is provided for Tx/Rx */
	data->obj.write_fill = 0;

	/* Register common SPI callback */
	cyhal_spi_register_callback(&data->obj, spi_interrupt_callback, (void *)dev);
	cyhal_spi_enable_event(&data->obj, CYHAL_SPI_IRQ_DONE, config->irq_priority, true);

	/* Store spi config in context */
	ctx->config = spi_cfg;

	data->dfs_value = get_dfs_value(ctx);

	return 0;
}

static int transceive(const struct device *dev, const struct spi_config *spi_cfg,
		      const struct spi_buf_set *tx_bufs, const struct spi_buf_set *rx_bufs,
		      bool asynchronous, spi_callback_t cb, void *userdata)
{
	int result;
	struct ifx_cat1_spi_data *const data = dev->data;
	struct spi_context *ctx = &data->ctx;

	spi_context_lock(ctx, asynchronous, cb, userdata, spi_cfg);

	result = spi_config(dev, spi_cfg);
	if (result) {
		LOG_ERR("Error in SPI Configuration (result: 0x%x)", result);
		return result;
	}

	spi_context_buffers_setup(ctx, tx_bufs, rx_bufs, data->dfs_value);

	spi_context_cs_control(ctx, true);

	transfer_chunk(dev);

	result = spi_context_wait_for_completion(&data->ctx);

	spi_context_release(ctx, result);

	return result;
}

static int ifx_cat1_spi_transceive_sync(const struct device *dev, const struct spi_config *spi_cfg,
					const struct spi_buf_set *tx_bufs,
					const struct spi_buf_set *rx_bufs)
{
	return transceive(dev, spi_cfg, tx_bufs, rx_bufs, false, NULL, NULL);
}

#if defined(CONFIG_SPI_ASYNC)
static int ifx_cat1_spi_transceive_async(const struct device *dev, const struct spi_config *spi_cfg,
					 const struct spi_buf_set *tx_bufs,
					 const struct spi_buf_set *rx_bufs, spi_callback_t cb,
					 void *userdata)
{
	return transceive(dev, spi_cfg, tx_bufs, rx_bufs, true, cb, userdata);
}
#endif

static int ifx_cat1_spi_release(const struct device *dev, const struct spi_config *spi_cfg)
{
	struct ifx_cat1_spi_data *const data = dev->data;

	cyhal_spi_free(&data->obj);

	return 0;
}

static const struct spi_driver_api ifx_cat1_spi_api = {
	.transceive = ifx_cat1_spi_transceive_sync,
#if defined(CONFIG_SPI_ASYNC)
	.transceive_async = ifx_cat1_spi_transceive_async,
#endif
	.release = ifx_cat1_spi_release,
};

static int ifx_cat1_spi_init(const struct device *dev)
{
	struct ifx_cat1_spi_data *const data = dev->data;
	const struct ifx_cat1_spi_config *const config = dev->config;
	int ret;

	/* Dedicate SCB HW resource */
	data->hw_resource.type = CYHAL_RSC_SCB;
	data->hw_resource.block_num = get_hw_block_num(config->reg_addr);

	/* Configure dt provided device signals when available */
	ret = pinctrl_apply_state(config->pcfg, PINCTRL_STATE_DEFAULT);
	if (ret < 0) {
		return ret;
	}

	/* Configure slave select (master) */
	spi_context_cs_configure_all(&data->ctx);

	spi_context_unlock_unconditionally(&data->ctx);

	return 0;
}

#define IFX_CAT1_SPI_INIT(n)                                                                       \
	PINCTRL_DT_INST_DEFINE(n);                                                                 \
	static struct ifx_cat1_spi_data spi_cat1_data_##n = {                                      \
		SPI_CONTEXT_INIT_LOCK(spi_cat1_data_##n, ctx),                                     \
		SPI_CONTEXT_INIT_SYNC(spi_cat1_data_##n, ctx),                                     \
		SPI_CONTEXT_CS_GPIOS_INITIALIZE(DT_DRV_INST(n), ctx)};                             \
	static struct ifx_cat1_spi_config spi_cat1_config_##n = {                                  \
		.reg_addr = (CySCB_Type *)DT_INST_REG_ADDR(n),                                     \
		.pcfg = PINCTRL_DT_INST_DEV_CONFIG_GET(n),                                         \
		.scb_spi_config =                                                                  \
			{.spiMode = CY_SCB_SPI_MASTER,       /* overwrite by cfg  */               \
			 .sclkMode = CY_SCB_SPI_CPHA0_CPOL0, /* overwrite by cfg  */               \
			 .rxDataWidth = 8,                   /* overwrite by cfg  */               \
			 .txDataWidth = 8,                   /* overwrite by cfg  */               \
			 .enableMsbFirst = true,             /* overwrite by cfg  */               \
			 .subMode = DT_INST_PROP_OR(n, sub_mode, CY_SCB_SPI_MOTOROLA),             \
			 .oversample =                                                             \
				 DT_INST_PROP_OR(n, oversample, IFX_CAT1_SPI_DEFAULT_OVERSAMPLE),  \
			 .enableFreeRunSclk = DT_INST_PROP_OR(n, enable_free_run_sclk, false),     \
			 .enableInputFilter = DT_INST_PROP_OR(n, enable_input_filter, false),      \
			 .enableMisoLateSample =                                                   \
				 DT_INST_PROP_OR(n, enable_miso_late_sample, true),                \
			 .enableTransferSeperation =                                               \
				 DT_INST_PROP_OR(n, enable_transfer_seperation, false),            \
			 .enableWakeFromSleep = DT_INST_PROP_OR(n, enableWakeFromSleep, false),    \
			 .ssPolarity = DT_INST_PROP_OR(n, ss_polarity, CY_SCB_SPI_ACTIVE_LOW),     \
			 .rxFifoTriggerLevel = DT_INST_PROP_OR(n, rx_fifo_trigger_level, 0),       \
			 .rxFifoIntEnableMask = DT_INST_PROP_OR(n, rx_fifo_int_enable_mask, 0),    \
			 .txFifoTriggerLevel = DT_INST_PROP_OR(n, tx_fifo_trigger_level, 0),       \
			 .txFifoIntEnableMask = DT_INST_PROP_OR(n, tx_fifo_int_enable_mask, 0),    \
			 .masterSlaveIntEnableMask =                                               \
				 DT_INST_PROP_OR(n, master_slave_int_enable_mask, 0)},             \
                                                                                                   \
		.irq_priority = DT_INST_IRQ(n, priority),                                          \
	};                                                                                         \
	DEVICE_DT_INST_DEFINE(n, &ifx_cat1_spi_init, NULL, &spi_cat1_data_##n,                     \
			      &spi_cat1_config_##n, POST_KERNEL,                                   \
			      CONFIG_KERNEL_INIT_PRIORITY_DEVICE, &ifx_cat1_spi_api);

DT_INST_FOREACH_STATUS_OKAY(IFX_CAT1_SPI_INIT)
