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

#define DT_DRV_COMPAT intel_sedi_spi

#include <zephyr/kernel.h>
#include <zephyr/drivers/spi.h>
#include <zephyr/pm/device.h>

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

#include "sedi_driver_spi.h"
#include "spi_context.h"


struct spi_sedi_config {
	DEVICE_MMIO_ROM;
	sedi_spi_t spi_device;
	void (*irq_config)(void);
};

struct spi_sedi_data {
	DEVICE_MMIO_RAM;
	struct spi_context ctx;
	bool tx_data_updated;
	bool rx_data_updated;
	uint32_t tx_dummy_len;
	uint32_t rx_dummy_len;
};

static int spi_sedi_configure(const struct device *dev,
			      const struct spi_config *config)
{
	struct spi_sedi_data *data = dev->data;
	const struct spi_sedi_config *info = dev->config;
	uint32_t word_size, cpol, cpha, loopback;

	if (spi_context_configured(&data->ctx, config) == true) {
		return 0;
	}

	word_size = SPI_WORD_SIZE_GET(config->operation);
	sedi_spi_control(info->spi_device, SEDI_SPI_IOCTL_DATA_WIDTH,
			 word_size);

	/* CPOL and CPHA */
	cpol = SPI_MODE_GET(config->operation) & SPI_MODE_CPOL;
	cpha = SPI_MODE_GET(config->operation) & SPI_MODE_CPHA;

	if ((cpol == 0) && (cpha == 0)) {
		sedi_spi_control(info->spi_device, SEDI_SPI_IOCTL_CPOL0_CPHA0,
				 0);
	} else if ((cpol == 0) && (cpha == 1U)) {
		sedi_spi_control(info->spi_device, SEDI_SPI_IOCTL_CPOL0_CPHA1,
				 0);
	} else if ((cpol == 1) && (cpha == 0U)) {
		sedi_spi_control(info->spi_device, SEDI_SPI_IOCTL_CPOL1_CPHA0,
				 0);
	} else {
		sedi_spi_control(info->spi_device, SEDI_SPI_IOCTL_CPOL1_CPHA1,
				 0);
	}

	/* MSB and LSB */
	if (config->operation & SPI_TRANSFER_LSB) {
		sedi_spi_control(info->spi_device, SEDI_SPI_IOCTL_LSB, 0);
	}

	/* Set loopack */
	loopback = SPI_MODE_GET(config->operation) & SPI_MODE_LOOP;
	sedi_spi_control(info->spi_device, SEDI_SPI_IOCTL_LOOPBACK, loopback);

	/* Set baudrate */
	sedi_spi_control(info->spi_device, SEDI_SPI_IOCTL_SPEED_SET,
			 config->frequency);

	sedi_spi_control(info->spi_device, SEDI_SPI_IOCTL_CS_HW, config->slave);

	data->ctx.config = config;
	spi_context_cs_control(&data->ctx, true);

	return 0;
}

static int transceive(const struct device *dev, const struct spi_config *config,
		      const struct spi_buf_set *tx_bufs,
		      const struct spi_buf_set *rx_bufs, bool asynchronous,
		      spi_callback_t cb,
		      void *userdata)
{
	const struct spi_sedi_config *info = dev->config;
	struct spi_sedi_data *spi = dev->data;
	struct spi_context *ctx = &spi->ctx;
	int ret;
	uint32_t transfer_bytes = 0;
	uint8_t *data_out = NULL, *data_in = NULL;
	uint32_t i, dummy_len = 0;
	const struct spi_buf *buf;
	bool is_multibufs = false;

	spi_context_lock(&spi->ctx, asynchronous, cb, userdata, config);
	pm_device_busy_set(dev);

	/* Power up use default setting */
	ret = sedi_spi_set_power(info->spi_device, SEDI_POWER_FULL);
	if (ret) {
		goto out;
	}

	/* If need to configure, re-configure */
	spi_sedi_configure(dev, config);

	spi->tx_data_updated = false;
	spi->rx_data_updated = false;
	/* Set buffers info */
	spi_context_buffers_setup(&spi->ctx, tx_bufs, rx_bufs, 1);

	if ((ctx->tx_count > 1) || (ctx->rx_count > 1)) {
		is_multibufs = true;
	}

	if (ctx->tx_count > ctx->rx_count) {
		spi->tx_dummy_len = 0;
		for (i = ctx->rx_count; i < ctx->tx_count; i++) {
			buf = ctx->current_tx + i;
			dummy_len += buf->len;
		}
		spi->rx_dummy_len = dummy_len;
	} else if (ctx->tx_count < ctx->rx_count) {
		spi->rx_dummy_len = 0;
		for (i = ctx->tx_count; i < ctx->rx_count; i++) {
			buf = ctx->current_rx + i;
			dummy_len += buf->len;
		}
		spi->tx_dummy_len = dummy_len;
	} else {
		spi->tx_dummy_len = 0;
		spi->rx_dummy_len = 0;
	}

	if ((ctx->tx_len == 0) && (ctx->rx_len == 0)) {
		spi_context_cs_control(&spi->ctx, true);
		spi_context_complete(&spi->ctx, dev, 0);
		return 0;
	}

	/* For multiple buffers, using continuous mode */
	if (is_multibufs) {
		sedi_spi_control(info->spi_device, SEDI_SPI_IOCTL_BUFFER_SETS, 1);
	}

	if (ctx->tx_len == 0) {
		/* rx only, nothing to tx */
		data_out = NULL;
		data_in = (uint8_t *)ctx->rx_buf;
		transfer_bytes = ctx->rx_len;
		spi->tx_dummy_len -= transfer_bytes;
	} else if (ctx->rx_len == 0) {
		/* tx only, nothing to rx */
		data_out = (uint8_t *)ctx->tx_buf;
		data_in = NULL;
		transfer_bytes = ctx->tx_len;
		spi->rx_dummy_len -= transfer_bytes;
	} else if (ctx->tx_len == ctx->rx_len) {
		/* rx and tx are the same length */
		data_out = (uint8_t *)ctx->tx_buf;
		data_in = (uint8_t *)ctx->rx_buf;
		transfer_bytes = ctx->tx_len;
	} else if (ctx->tx_len > ctx->rx_len) {
		/* Break up the tx into multiple transfers so we don't have to
		 * rx into a longer intermediate buffer. Leave chip select
		 * active between transfers.
		 */
		data_out = (uint8_t *)ctx->tx_buf;
		data_in = ctx->rx_buf;
		transfer_bytes = ctx->rx_len;
	} else {
		/* Break up the rx into multiple transfers so we don't have to
		 * tx from a longer intermediate buffer. Leave chip select
		 * active between transfers.
		 */
		data_out = (uint8_t *)ctx->tx_buf;
		data_in = ctx->rx_buf;
		transfer_bytes = ctx->tx_len;
	}

	spi_context_cs_control(&spi->ctx, false);

	ret = sedi_spi_transfer(info->spi_device, data_out, data_in,
				transfer_bytes);

	if (ret != SEDI_DRIVER_OK) {
		goto out;
	}

	ret = spi_context_wait_for_completion(&spi->ctx);
	if (ret != 0) {
		sedi_spi_status_t spi_status = {0};

		sedi_spi_get_status(info->spi_device, &spi_status);

		/* SPI ABORT */
		sedi_spi_control(info->spi_device, SEDI_SPI_IOCTL_ABORT, 0);
		/* Toggle GPIO back */
		spi_context_cs_control(&spi->ctx, true);
	}
out:
	spi_context_release(&spi->ctx, ret);
	pm_device_busy_clear(dev);

	return ret;
}

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

#ifdef CONFIG_SPI_ASYNC
static int spi_sedi_transceive_async(const struct device *dev,
				     const struct spi_config *config,
				     const struct spi_buf_set *tx_bufs,
				     const struct spi_buf_set *rx_bufs,
				     spi_callback_t cb,
				     void *userdata)
{
	return transceive(dev, config, tx_bufs, rx_bufs, true, cb, userdata);
}
#endif /* CONFIG_SPI_ASYNC */

static int spi_sedi_release(const struct device *dev,
			    const struct spi_config *config)
{
	struct spi_sedi_data *spi = dev->data;

	if (!spi_context_configured(&spi->ctx, config)) {
		return -EINVAL;
	}

	spi_context_unlock_unconditionally(&spi->ctx);

	return 0;
}

extern void spi_isr(sedi_spi_t device);

void spi_sedi_callback(uint32_t event, void *param)
{
	const struct device *dev = (const struct device *)param;
	const struct spi_sedi_config *info = dev->config;
	struct spi_sedi_data *spi = dev->data;
	struct spi_context *ctx = &spi->ctx;
	int error;

	if (event == SEDI_SPI_EVENT_DATA_LOST) {
		error = -EIO;
	} else {
		error = 0;
	}

	if ((event == SEDI_SPI_EVENT_COMPLETE) ||
	    (event == SEDI_SPI_EVENT_DATA_LOST)) {
		spi_context_cs_control(&spi->ctx, true);
		spi_context_complete(&spi->ctx, dev, error);
	} else if (event == SEDI_SPI_EVENT_TX_FINISHED) {
		spi_context_update_tx(ctx, 1, ctx->tx_len);
		if (ctx->tx_len != 0) {
			sedi_spi_update_tx_buf(info->spi_device, ctx->tx_buf,
					       ctx->tx_len);
			if ((ctx->rx_len == 0) &&
			    (spi->rx_data_updated == false)) {
				/* Update rx length if always no rx */
				sedi_spi_update_rx_buf(info->spi_device, NULL,
						       spi->rx_dummy_len);
				spi->rx_data_updated = true;
			}
		} else if (spi->tx_data_updated == false) {
			sedi_spi_update_tx_buf(info->spi_device, NULL,
					       spi->tx_dummy_len);
			spi->tx_data_updated = true;
		}
	} else if (event == SEDI_SPI_EVENT_RX_FINISHED) {
		spi_context_update_rx(ctx, 1, ctx->rx_len);
		if (ctx->rx_len != 0) {
			sedi_spi_update_rx_buf(info->spi_device, ctx->rx_buf,
					       ctx->rx_len);
		}
	}
}

static const struct spi_driver_api sedi_spi_api = {
	.transceive = spi_sedi_transceive,
#ifdef CONFIG_SPI_ASYNC
	.transceive_async = spi_sedi_transceive_async,
#endif  /* CONFIG_SPI_ASYNC */
	.release = spi_sedi_release,
};

static int spi_sedi_init(const struct device *dev)
{
	const struct spi_sedi_config *info = dev->config;
	struct spi_sedi_data *spi = dev->data;
	int ret;

	DEVICE_MMIO_MAP(dev, K_MEM_CACHE_NONE);

	ret = sedi_spi_init(info->spi_device, spi_sedi_callback, (void *)dev,
			DEVICE_MMIO_GET(dev));
	if (ret != SEDI_DRIVER_OK) {
		return -ENODEV;
	}

	/* Init and connect IRQ */
	info->irq_config();

	spi_context_unlock_unconditionally(&spi->ctx);

	return 0;
}

#ifdef CONFIG_PM_DEVICE

static int spi_suspend_device(const struct device *dev)
{
	const struct spi_sedi_config *config = dev->config;

	if (pm_device_is_busy(dev)) {
		return -EBUSY;
	}

	int ret = sedi_spi_set_power(config->spi_device, SEDI_POWER_SUSPEND);

	if (ret != SEDI_DRIVER_OK) {
		return -EIO;
	}

	return 0;
}

static int spi_resume_device_from_suspend(const struct device *dev)
{
	const struct spi_sedi_config *config = dev->config;
	int ret;

	ret = sedi_spi_set_power(config->spi_device, SEDI_POWER_FULL);
	if (ret != SEDI_DRIVER_OK) {
		return -EIO;
	}

	pm_device_busy_clear(dev);

	return 0;
}

static int spi_sedi_device_ctrl(const struct device *dev,
				enum pm_device_action action)
{
	int ret = 0;

	switch (action) {
	case PM_DEVICE_ACTION_SUSPEND:
		ret = spi_suspend_device(dev);
		break;
	case PM_DEVICE_ACTION_RESUME:
		ret = spi_resume_device_from_suspend(dev);
		break;
	default:
		ret = -ENOTSUP;
	}

	return ret;
}

#endif /* CONFIG_PM_DEVICE */

#define SPI_SEDI_IRQ_FLAGS_SENSE0(n) 0
#define SPI_SEDI_IRQ_FLAGS_SENSE1(n) DT_INST_IRQ(n, sense)
#define SPI_SEDI_IRQ_FLAGS(n) \
	_CONCAT(SPI_SEDI_IRQ_FLAGS_SENSE, DT_INST_IRQ_HAS_CELL(n, sense))(n)

#define CREATE_SEDI_SPI_INSTANCE(num)					       \
	static void spi_##num##_irq_init(void)			               \
	{								       \
		IRQ_CONNECT(DT_INST_IRQN(num),				       \
			    DT_INST_IRQ(num, priority),			       \
			    spi_isr, num, SPI_SEDI_IRQ_FLAGS(num));	       \
		irq_enable(DT_INST_IRQN(num));				       \
	}								       \
	static struct spi_sedi_data spi_##num##_data = {		       \
		SPI_CONTEXT_INIT_LOCK(spi_##num##_data, ctx),	               \
		SPI_CONTEXT_INIT_SYNC(spi_##num##_data, ctx),	               \
	};								       \
	const static struct spi_sedi_config spi_##num##_config = {	               \
		DEVICE_MMIO_ROM_INIT(DT_DRV_INST(num)),                        \
		.spi_device = num, .irq_config = spi_##num##_irq_init,         \
	};								       \
	PM_DEVICE_DEFINE(spi_##num, spi_sedi_device_ctrl);		       \
	DEVICE_DT_INST_DEFINE(num,					       \
			      &spi_sedi_init,				       \
			      PM_DEVICE_GET(spi_##num),		               \
			      &spi_##num##_data,			       \
			      &spi_##num##_config,			       \
			      POST_KERNEL,				       \
			      CONFIG_SPI_INIT_PRIORITY,			       \
			      &sedi_spi_api);

DT_INST_FOREACH_STATUS_OKAY(CREATE_SEDI_SPI_INSTANCE)
