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

#define DT_DRV_COMPAT intel_sedi_dma

#include <errno.h>
#include <stdio.h>
#include <zephyr/kernel.h>
#include <zephyr/pm/device.h>
#include <string.h>
#include <zephyr/init.h>
#include <zephyr/drivers/dma.h>
#include <zephyr/devicetree.h>
#include <zephyr/cache.h>
#include <soc.h>

#include "sedi_driver_dma.h"
#include "sedi_driver_core.h"

#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(sedi_dma, CONFIG_DMA_LOG_LEVEL);

extern void dma_isr(sedi_dma_t dma_device);

struct dma_sedi_config_info {
	sedi_dma_t peripheral_id; /* Controller instance. */
	uint8_t chn_num;
	void (*irq_config)(void);
};

struct dma_sedi_driver_data {
	struct dma_config dma_configs[DMA_CHANNEL_NUM];
};

#define DEV_DATA(dev) ((struct dma_sedi_driver_data *const)(dev)->data)
#define DEV_CFG(dev) \
	((const struct dma_sedi_config_info *const)(dev)->config)

/*
 * this function will be called when dma transferring is completed
 * or error happened
 */
static void dma_handler(sedi_dma_t dma_device, int channel, int event_id,
			void *args)
{
	ARG_UNUSED(args);
	const struct device *dev = (const struct device *)args;
	struct dma_sedi_driver_data *const data = DEV_DATA(dev);
	struct dma_config *config = &(data->dma_configs[channel]);

	/* run user-defined callback */
	if (config->dma_callback) {
		if ((event_id == SEDI_DMA_EVENT_TRANSFER_DONE) &&
		    (config->complete_callback_en)) {
			config->dma_callback(dev, config->user_data,
					channel, 0);
		} else if (config->error_callback_en) {
			config->dma_callback(dev, config->user_data,
					channel, event_id);
		}
	}
}

/* map width to certain macros*/
static int width_index(uint32_t num_bytes, uint32_t *index)
{
	switch (num_bytes) {
	case 1:
		*index = DMA_TRANS_WIDTH_8;
		break;
	case 2:
		*index = DMA_TRANS_WIDTH_16;
		break;
	case 4:
		*index = DMA_TRANS_WIDTH_32;
		break;
	case 8:
		*index = DMA_TRANS_WIDTH_64;
		break;
	case 16:
		*index = DMA_TRANS_WIDTH_128;
		break;
	case 32:
		*index = DMA_TRANS_WIDTH_256;
		break;
	default:
		return -ENOTSUP;
	}

	return 0;
}

/* map burst size to certain macros*/
static int burst_index(uint32_t num_units, uint32_t *index)
{
	switch (num_units) {
	case 1:
		*index = DMA_BURST_TRANS_LENGTH_1;
		break;
	case 4:
		*index = DMA_BURST_TRANS_LENGTH_4;
		break;
	case 8:
		*index = DMA_BURST_TRANS_LENGTH_8;
		break;
	case 16:
		*index = DMA_BURST_TRANS_LENGTH_16;
		break;
	case 32:
		*index = DMA_BURST_TRANS_LENGTH_32;
		break;
	case 64:
		*index = DMA_BURST_TRANS_LENGTH_64;
		break;
	case 128:
		*index = DMA_BURST_TRANS_LENGTH_128;
		break;
	case 256:
		*index = DMA_BURST_TRANS_LENGTH_256;
		break;
	default:
		return -ENOTSUP;
	}

	return 0;
}

static void dma_config_convert(struct dma_config *config,
			       dma_memory_type_t *src_mem,
			       dma_memory_type_t *dst_mem,
			       uint8_t *sedi_dma_dir)
{

	*src_mem = DMA_SRAM_MEM;
	*dst_mem = DMA_SRAM_MEM;
	*sedi_dma_dir = MEMORY_TO_MEMORY;
	switch (config->channel_direction) {
	case MEMORY_TO_MEMORY:
	case MEMORY_TO_PERIPHERAL:
	case PERIPHERAL_TO_MEMORY:
	case PERIPHERAL_TO_PERIPHERAL:
		*sedi_dma_dir = config->channel_direction;
		break;
	case MEMORY_TO_HOST:
		*dst_mem = DMA_DRAM_MEM;
		break;
	case HOST_TO_MEMORY:
		*src_mem = DMA_DRAM_MEM;
		break;
#ifdef MEMORY_TO_IMR
	case MEMORY_TO_IMR:
		*dst_mem = DMA_UMA_MEM;
		break;
#endif
#ifdef IMR_TO_MEMORY
	case IMR_TO_MEMORY:
		*src_mem = DMA_UMA_MEM;
		break;
#endif
	}
}

/* config basic dma */
static int dma_sedi_apply_common_config(sedi_dma_t dev, uint32_t channel,
					struct dma_config *config, uint8_t *dir)
{
	uint8_t direction = MEMORY_TO_MEMORY;
	dma_memory_type_t src_mem = DMA_SRAM_MEM, dst_mem = DMA_SRAM_MEM;

	dma_config_convert(config, &src_mem, &dst_mem, &direction);

	if (dir) {
		*dir = direction;
	}

	/* configure dma transferring direction*/
	sedi_dma_control(dev, channel, SEDI_CONFIG_DMA_DIRECTION,
			 direction);

	if (direction == MEMORY_TO_MEMORY) {
		sedi_dma_control(dev, channel, SEDI_CONFIG_DMA_SR_MEM_TYPE,
				 src_mem);
		sedi_dma_control(dev, channel, SEDI_CONFIG_DMA_DT_MEM_TYPE,
				 dst_mem);
	} else if (direction == MEMORY_TO_PERIPHERAL) {
		sedi_dma_control(dev, channel, SEDI_CONFIG_DMA_HS_DEVICE_ID,
				 config->dma_slot);
		sedi_dma_control(dev, channel, SEDI_CONFIG_DMA_HS_POLARITY,
				 DMA_HS_POLARITY_HIGH);
		sedi_dma_control(dev, channel,
				 SEDI_CONFIG_DMA_HS_DEVICE_ID_PER_DIR,
				 DMA_HS_PER_TX);
	} else if (direction == PERIPHERAL_TO_MEMORY) {
		sedi_dma_control(dev, channel, SEDI_CONFIG_DMA_HS_DEVICE_ID,
				 config->dma_slot);
		sedi_dma_control(dev, channel, SEDI_CONFIG_DMA_HS_POLARITY,
				 DMA_HS_POLARITY_HIGH);
		sedi_dma_control(dev, channel,
				 SEDI_CONFIG_DMA_HS_DEVICE_ID_PER_DIR,
				 DMA_HS_PER_RX);
	} else {
		return -1;
	}
	return 0;
}

static int dma_sedi_apply_single_config(sedi_dma_t dev, uint32_t channel,
					struct dma_config *config)
{
	int ret = 0;
	uint32_t temp = 0;

	ret = dma_sedi_apply_common_config(dev, channel, config, NULL);
	if (ret != 0) {
		goto INVALID_ARGS;
	}
	/* configurate dma width of source data*/
	ret = width_index(config->source_data_size, &temp);
	if (ret != 0) {
		goto INVALID_ARGS;
	}
	sedi_dma_control(dev, channel, SEDI_CONFIG_DMA_SR_TRANS_WIDTH, temp);

	/* configurate dma width of destination data*/
	ret = width_index(config->dest_data_size, &temp);
	if (ret != 0) {
		goto INVALID_ARGS;
	}
	sedi_dma_control(dev, channel, SEDI_CONFIG_DMA_DT_TRANS_WIDTH, temp);

	/* configurate dma burst size*/
	ret = burst_index(config->source_burst_length, &temp);
	if (ret != 0) {
		goto INVALID_ARGS;
	}
	sedi_dma_control(dev, channel, SEDI_CONFIG_DMA_BURST_LENGTH, temp);
	return 0;

INVALID_ARGS:
	return ret;
}

static int dma_sedi_chan_config(const struct device *dev, uint32_t channel,
				struct dma_config *config)
{
	if ((dev == NULL) || (channel >= DEV_CFG(dev)->chn_num)
		|| (config == NULL)
		|| (config->block_count != 1)) {
		goto INVALID_ARGS;
	}

	const struct dma_sedi_config_info *const info = DEV_CFG(dev);
	struct dma_sedi_driver_data *const data = DEV_DATA(dev);

	memcpy(&(data->dma_configs[channel]), config, sizeof(struct dma_config));

	/* initialize the dma controller, following the sedi api*/
	sedi_dma_event_cb_t cb = dma_handler;

	sedi_dma_init(info->peripheral_id, (int)channel, cb, (void *)dev);

	return 0;

INVALID_ARGS:
	return -1;
}

static int dma_sedi_reload(const struct device *dev, uint32_t channel,
			      uint64_t src, uint64_t dst, size_t size)
{
	if ((dev == NULL) || (channel >= DEV_CFG(dev)->chn_num)) {
		LOG_ERR("dma reload failed for invalid args");
		return -ENOTSUP;
	}

	int ret = 0;
	struct dma_sedi_driver_data *const data = DEV_DATA(dev);
	struct dma_config *config = &(data->dma_configs[channel]);
	struct dma_block_config *block_config;

	if ((config == NULL) || (config->head_block == NULL)) {
		LOG_ERR("dma reload failed, no config found");
		return -ENOTSUP;
	}
	block_config = config->head_block;

	if ((config->block_count == 1) || (block_config->next_block == NULL)) {
		block_config->source_address = src;
		block_config->dest_address = dst;
		block_config->block_size = size;
	} else {
		LOG_ERR("no reload support for multi-linkedlist mode");
		return -ENOTSUP;
	}
	return ret;
}

static int dma_sedi_start(const struct device *dev, uint32_t channel)
{
	if ((dev == NULL) || (channel >= DEV_CFG(dev)->chn_num)) {
		LOG_ERR("dma transferring failed for invalid args");
		return -ENOTSUP;
	}

	int ret = -1;
	const struct dma_sedi_config_info *const info = DEV_CFG(dev);
	struct dma_sedi_driver_data *const data = DEV_DATA(dev);
	struct dma_config *config = &(data->dma_configs[channel]);
	struct dma_block_config *block_config = config->head_block;
	uint64_t src_addr, dst_addr;

	if (config->block_count == 1) {
		/* call sedi start function */
		ret = dma_sedi_apply_single_config(info->peripheral_id,
						   channel, config);
		if (ret) {
			goto ERR;
		}
		src_addr = block_config->source_address;
		dst_addr = block_config->dest_address;

		ret = sedi_dma_start_transfer(info->peripheral_id, channel,
						src_addr, dst_addr, block_config->block_size);
	} else {
		LOG_ERR("MULTIPLE_BLOCK CONFIG is not set");
		goto ERR;
	}

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

	return ret;

ERR:
	LOG_ERR("dma transfer failed");
	return ret;
}

static int dma_sedi_stop(const struct device *dev, uint32_t channel)
{
	const struct dma_sedi_config_info *const info = DEV_CFG(dev);

	LOG_DBG("stopping dma: %p, %d", dev, channel);
	sedi_dma_abort_transfer(info->peripheral_id, channel);

	return 0;
}

static const struct dma_driver_api dma_funcs = { .config = dma_sedi_chan_config,
						 .start = dma_sedi_start,
						 .stop = dma_sedi_stop,
						 .reload = dma_sedi_reload,
						 .get_status = NULL
};

static int dma_sedi_init(const struct device *dev)
{
	const struct dma_sedi_config_info *const config = DEV_CFG(dev);

	config->irq_config();

	return 0;
}

#define DMA_DEVICE_INIT_SEDI(inst) \
	static void dma_sedi_##inst##_irq_config(void);			\
									\
	static struct dma_sedi_driver_data dma_sedi_dev_data_##inst; \
	static const struct dma_sedi_config_info dma_sedi_config_data_##inst = { \
		.peripheral_id = DT_INST_PROP(inst, peripheral_id), \
		.chn_num = DT_INST_PROP(inst, dma_channels), \
		.irq_config = dma_sedi_##inst##_irq_config \
	}; \
	DEVICE_DT_DEFINE(DT_INST(inst, DT_DRV_COMPAT), &dma_sedi_init, \
	      NULL, &dma_sedi_dev_data_##inst, &dma_sedi_config_data_##inst, PRE_KERNEL_2, \
	      CONFIG_KERNEL_INIT_PRIORITY_DEVICE, (void *)&dma_funcs); \
									\
	static void dma_sedi_##inst##_irq_config(void)			\
	{								\
		IRQ_CONNECT(DT_INST_IRQN(inst),				\
			    DT_INST_IRQ(inst, priority), dma_isr,	\
			    (void *)DT_INST_PROP(inst, peripheral_id),			\
			    DT_INST_IRQ(inst, sense));			\
		irq_enable(DT_INST_IRQN(inst));				\
	}

DT_INST_FOREACH_STATUS_OKAY(DMA_DEVICE_INIT_SEDI)
