/*
 * Copyright (c) 2020 STMicroelectronics
 * Copyright (c) 2023 Jeroen van Dooren, Nobleo Technology
 *
 * SPDX-License-Identifier: Apache-2.0
 */

/**
 * @brief Common part of DMAMUX drivers for stm32.
 * @note  api functions named dmamux_stm32_
 *        are calling the dma_stm32 corresponding function
 *        implemented in dma_stm32.c
 */

#include <soc.h>
#include <stm32_ll_dmamux.h>
#include <zephyr/init.h>
#include <zephyr/drivers/dma.h>
#include <zephyr/drivers/clock_control.h>
#include <zephyr/drivers/clock_control/stm32_clock_control.h>

#include "dma_stm32.h"
#ifdef CONFIG_DMA_STM32_BDMA
#include "dma_stm32_bdma.h"
#endif

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

#define DT_DRV_COMPAT st_stm32_dmamux

/* this is the configuration of one dmamux channel */
struct dmamux_stm32_channel {
	/* pointer to the associated dma instance */
	const struct device *dev_dma;
	/* ref of the associated dma stream for this instance */
	uint8_t dma_id;
};

/* the table of all the dmamux channel */
struct dmamux_stm32_data {
	void *callback_arg;
	void (*dmamux_callback)(void *arg, uint32_t id,
				int error_code);
};

/* this is the configuration of the dmamux IP */
struct dmamux_stm32_config {
#if DT_INST_NODE_HAS_PROP(0, clocks)
	struct stm32_pclken pclken;
#endif
	uint32_t base;
	uint8_t channel_nb;	/* total nb of channels */
	uint8_t gen_nb;	/* total nb of Request generator */
	uint8_t req_nb;	/* total nb of Peripheral Request inputs */
	const struct dmamux_stm32_channel *mux_channels;
};

typedef int (*dma_configure_fn)(const struct device *dev, uint32_t id, struct dma_config *config);
typedef int (*dma_start_fn)(const struct device *dev, uint32_t id);
typedef int (*dma_stop_fn)(const struct device *dev, uint32_t id);
typedef int (*dma_reload_fn)(const struct device *dev, uint32_t id,
			uint32_t src, uint32_t dst, size_t size);
typedef int (*dma_status_fn)(const struct device *dev, uint32_t id,
				struct dma_status *stat);

struct dmamux_stm32_dma_fops {
	dma_configure_fn configure;
	dma_start_fn start;
	dma_stop_fn stop;
	dma_reload_fn reload;
	dma_status_fn get_status;
};

#if (defined(CONFIG_DMA_STM32_V1) || defined(CONFIG_DMA_STM32_V2)) && \
	DT_NODE_HAS_STATUS_OKAY(DT_NODELABEL(dmamux1))
static const struct dmamux_stm32_dma_fops dmamux1 = {
	dma_stm32_configure,
	dma_stm32_start,
	dma_stm32_stop,
	dma_stm32_reload,
	dma_stm32_get_status,
};
#endif

#if defined(CONFIG_DMA_STM32_BDMA) && DT_NODE_HAS_STATUS_OKAY(DT_NODELABEL(dmamux2))
static const struct dmamux_stm32_dma_fops dmamux2 = {
	bdma_stm32_configure,
	bdma_stm32_start,
	bdma_stm32_stop,
	bdma_stm32_reload,
	bdma_stm32_get_status
};
#endif /* CONFIG_DMA_STM32_BDMA */

const struct dmamux_stm32_dma_fops *get_dma_fops(const struct dmamux_stm32_config *dev_config)
{
#if DT_NODE_HAS_STATUS_OKAY(DT_NODELABEL(dmamux1))
	if (dev_config->base == DT_REG_ADDR(DT_NODELABEL(dmamux1))) {
		return &dmamux1;
	}
#endif /* DT_NODE_HAS_STATUS_OKAY(DT_NODELABEL(dmamux1)) */

#if DT_NODE_HAS_STATUS_OKAY(DT_NODELABEL(dmamux2))
	if (dev_config->base == DT_REG_ADDR(DT_NODELABEL(dmamux2))) {
		return &dmamux2;
	}
#endif /* DT_NODE_HAS_STATUS_OKAY(DT_NODELABEL(dmamux2)) */

	__ASSERT(false, "Unknown dma base address %x", dev_config->base);
	return (void *)0;
}

int dmamux_stm32_configure(const struct device *dev, uint32_t id,
				struct dma_config *config)
{
	/* device is the dmamux, id is the dmamux channel from 0 */
	const struct dmamux_stm32_config *dev_config = dev->config;
	const struct dmamux_stm32_dma_fops *dma_device = get_dma_fops(dev_config);

	/*
	 * request line ID for this mux channel is stored
	 * in the dma_slot parameter
	 */
	int request_id = config->dma_slot;

	if (request_id > dev_config->req_nb + dev_config->gen_nb) {
		LOG_ERR("request ID %d is not valid.", request_id);
		return -EINVAL;
	}

	/* check if this channel is valid */
	if (id >= dev_config->channel_nb) {
		LOG_ERR("channel ID %d is too big.", id);
		return -EINVAL;
	}

	/*
	 * Also configures the corresponding dma channel
	 * instance is given by the dev_dma
	 * stream is given by the index i
	 * config is directly this dma_config
	 */

	/*
	 * This dmamux channel 'id' is now used for this peripheral request
	 * It gives this mux request ID to the dma through the config.dma_slot
	 */
	if (dma_device->configure(dev_config->mux_channels[id].dev_dma,
			dev_config->mux_channels[id].dma_id, config) != 0) {
		LOG_ERR("cannot configure the dmamux.");
		return -EINVAL;
	}

	/* set the Request Line ID to this dmamux channel i */
	DMAMUX_Channel_TypeDef *dmamux =
			(DMAMUX_Channel_TypeDef *)dev_config->base;


	LL_DMAMUX_SetRequestID(dmamux, id, request_id);

	return 0;
}

int dmamux_stm32_start(const struct device *dev, uint32_t id)
{
	const struct dmamux_stm32_config *dev_config = dev->config;
	const struct dmamux_stm32_dma_fops *dma_device = get_dma_fops(dev_config);

	/* check if this channel is valid */
	if (id >= dev_config->channel_nb) {
		LOG_ERR("channel ID %d is too big.", id);
		return -EINVAL;
	}

	if (dma_device->start(dev_config->mux_channels[id].dev_dma,
		dev_config->mux_channels[id].dma_id) != 0) {
		LOG_ERR("cannot start the dmamux channel %d.", id);
		return -EINVAL;
	}

	return 0;
}

int dmamux_stm32_stop(const struct device *dev, uint32_t id)
{
	const struct dmamux_stm32_config *dev_config = dev->config;
	const struct dmamux_stm32_dma_fops *dma_device = get_dma_fops(dev_config);

	/* check if this channel is valid */
	if (id >= dev_config->channel_nb) {
		LOG_ERR("channel ID %d is too big.", id);
		return -EINVAL;
	}

	if (dma_device->stop(dev_config->mux_channels[id].dev_dma,
		dev_config->mux_channels[id].dma_id) != 0) {
		LOG_ERR("cannot stop the dmamux channel %d.", id);
		return -EINVAL;
	}

	return 0;
}

int dmamux_stm32_reload(const struct device *dev, uint32_t id,
			    uint32_t src, uint32_t dst, size_t size)
{
	const struct dmamux_stm32_config *dev_config = dev->config;
	const struct dmamux_stm32_dma_fops *dma_device = get_dma_fops(dev_config);

	/* check if this channel is valid */
	if (id >= dev_config->channel_nb) {
		LOG_ERR("channel ID %d is too big.", id);
		return -EINVAL;
	}

	if (dma_device->reload(dev_config->mux_channels[id].dev_dma,
		dev_config->mux_channels[id].dma_id,
		src, dst, size) != 0) {
		LOG_ERR("cannot reload the dmamux channel %d.", id);
		return -EINVAL;
	}

	return 0;
}

int dmamux_stm32_get_status(const struct device *dev, uint32_t id,
				struct dma_status *stat)
{
	const struct dmamux_stm32_config *dev_config = dev->config;
	const struct dmamux_stm32_dma_fops *dma_device = get_dma_fops(dev_config);

	/* check if this channel is valid */
	if (id >= dev_config->channel_nb) {
		LOG_ERR("channel ID %d is too big.", id);
		return -EINVAL;
	}

	if (dma_device->get_status(dev_config->mux_channels[id].dev_dma,
		dev_config->mux_channels[id].dma_id, stat) != 0) {
		LOG_ERR("cannot get the status of dmamux channel %d.", id);
		return -EINVAL;
	}

	return 0;
}

static int dmamux_stm32_init(const struct device *dev)
{
	const struct dmamux_stm32_config *config = dev->config;
#if DT_INST_NODE_HAS_PROP(0, clocks)
	const struct device *const clk = DEVICE_DT_GET(STM32_CLOCK_CONTROL_NODE);

	if (!device_is_ready(clk)) {
		LOG_ERR("clock control device not ready");
		return -ENODEV;
	}

	if (clock_control_on(clk,
		(clock_control_subsys_t) &config->pclken) != 0) {
		LOG_ERR("clock op failed\n");
		return -EIO;
	}
#endif /* DT_INST_NODE_HAS_PROP(0, clocks) */

#if DT_NODE_HAS_STATUS_OKAY(DT_NODELABEL(dmamux1))
	/* DMA 1 and DMA2 for DMAMUX1, BDMA for DMAMUX2 */
	if (config->base == DT_REG_ADDR(DT_NODELABEL(dmamux1))) {
		/* DMAs assigned to DMAMUX channels at build time might not be ready. */
#if DT_NODE_HAS_STATUS_OKAY(DT_NODELABEL(dma1))
		if (device_is_ready(DEVICE_DT_GET(DT_NODELABEL(dma1))) == false) {
			return -ENODEV;
		}
#endif
#if DT_NODE_HAS_STATUS_OKAY(DT_NODELABEL(dma2))
		if (device_is_ready(DEVICE_DT_GET(DT_NODELABEL(dma2))) == false) {
			return -ENODEV;
		}
#endif
	}
#endif /* DT_NODE_HAS_STATUS_OKAY(DT_NODELABEL(dmamux1)) */

#if DT_NODE_HAS_STATUS_OKAY(DT_NODELABEL(dmamux2)) && DT_NODE_HAS_STATUS_OKAY(DT_NODELABEL(bdma1))
	if (config->base == DT_REG_ADDR(DT_NODELABEL(dmamux2))) {
		if (device_is_ready(DEVICE_DT_GET(DT_NODELABEL(bdma1))) == false) {
			return -ENODEV;
		}
	}
#endif
	return 0;
}

static DEVICE_API(dma, dma_funcs) = {
	.reload		 = dmamux_stm32_reload,
	.config		 = dmamux_stm32_configure,
	.start		 = dmamux_stm32_start,
	.stop		 = dmamux_stm32_stop,
	.get_status	 = dmamux_stm32_get_status,
};

/*
 * Each dmamux channel is hardwired to one dma controllers dma channel.
 * DMAMUX_CHANNEL_INIT_X macros resolve this mapping at build time for each
 * dmamux channel using the dma dt properties dma_offset and dma_requests,
 * such that it can be stored in dmamux_stm32_channels_X configuration.
 * The Macros to get the corresponding dma device binding and dma channel
 * for a given dmamux channel, are currently valid for series having
 * up to 2 dmamuxes and up to 3 dmas.
 */

#define DMA_1_BEGIN_DMAMUX_CHANNEL DT_PROP_OR(DT_NODELABEL(dma1), dma_offset, 0)
#define DMA_1_END_DMAMUX_CHANNEL (DMA_1_BEGIN_DMAMUX_CHANNEL + \
				DT_PROP_OR(DT_NODELABEL(dma1), dma_requests, 0))
#define DEV_DMA1 COND_CODE_1(DT_NODE_HAS_STATUS_OKAY(DT_NODELABEL(dma1)), \
			     DEVICE_DT_GET(DT_NODELABEL(dma1)), NULL)

#define DMA_2_BEGIN_DMAMUX_CHANNEL DT_PROP_OR(DT_NODELABEL(dma2), dma_offset, 0)
#define DMA_2_END_DMAMUX_CHANNEL (DMA_2_BEGIN_DMAMUX_CHANNEL + \
				DT_PROP_OR(DT_NODELABEL(dma2), dma_requests, 0))
#define DEV_DMA2 COND_CODE_1(DT_NODE_HAS_STATUS_OKAY(DT_NODELABEL(dma2)), \
			     DEVICE_DT_GET(DT_NODELABEL(dma2)), NULL)

#define BDMA_1_BEGIN_DMAMUX_CHANNEL DT_PROP_OR(DT_NODELABEL(bdma1), dma_offset, 0)
#define BDMA_1_END_DMAMUX_CHANNEL (BDMA_1_BEGIN_DMAMUX_CHANNEL + \
				DT_PROP_OR(DT_NODELABEL(bdma1), dma_requests, 0))
#define DEV_BDMA COND_CODE_1(DT_NODE_HAS_STATUS_OKAY(DT_NODELABEL(bdma1)), \
			     DEVICE_DT_GET(DT_NODELABEL(bdma1)), NULL)

#define DEV_DMA_BINDING(mux_channel) \
	((mux_channel < DMA_1_END_DMAMUX_CHANNEL) ? DEV_DMA1 : DEV_DMA2)

#define DEV_BDMA_BINDING(mux_channel) \
	(DEV_BDMA)


#define DMA_CHANNEL(mux_channel) \
	((mux_channel < DMA_1_END_DMAMUX_CHANNEL) ? \
	 (mux_channel + 1) : (mux_channel - DMA_2_BEGIN_DMAMUX_CHANNEL + 1))

#define BDMA_CHANNEL(mux_channel) \
	((mux_channel < BDMA_1_END_DMAMUX_CHANNEL) ? \
	 (mux_channel) : 0 /* not supported */)

/*
 * H7 series implements DMAMUX1 and DMAMUX2
 * DMAMUX1 is used by DMA1 and DMA2
 * DMAMUX2 is used by BDMA
 *
 * Note: Instance Number (or index) has no guarantee to which dmamux it refers
 */

#define INIT_DMAMUX1_CHANNEL(x, ...) \
	{ .dev_dma = DEV_DMA_BINDING(x), .dma_id = DMA_CHANNEL(x), }
#define INIT_DMAMUX2_CHANNEL(x, ...) \
	{ .dev_dma = DEV_BDMA_BINDING(x), .dma_id = BDMA_CHANNEL(x), }

#if DT_SAME_NODE(DT_DRV_INST(0), DT_NODELABEL(dmamux1))
#define INIT_INST0_CHANNEL(x, ...) INIT_DMAMUX1_CHANNEL(x, ...)
#define INIT_INST1_CHANNEL(x, ...) INIT_DMAMUX2_CHANNEL(x, ...)
#else
#define INIT_INST0_CHANNEL(x, ...) INIT_DMAMUX2_CHANNEL(x, ...)
#define INIT_INST1_CHANNEL(x, ...) INIT_DMAMUX1_CHANNEL(x, ...)
#endif

#define DMAMUX_CHANNELS_INIT(index, count)                \
	LISTIFY(count, INIT_INST##index##_CHANNEL, (,))

#define DMAMUX_INIT(index)									\
	static const struct dmamux_stm32_channel						\
		dmamux_stm32_channels_##index[DT_INST_PROP(index, dma_channels)] = {		\
			DMAMUX_CHANNELS_INIT(index, DT_INST_PROP(index, dma_channels))		\
	};											\
												\
	const struct dmamux_stm32_config dmamux_stm32_config_##index = {			\
		IF_ENABLED(DT_INST_NODE_HAS_PROP(index, clocks),				\
			   (.pclken = STM32_DT_INST_CLOCK_INFO(index),))			\
		.base = DT_INST_REG_ADDR(index),						\
		.channel_nb = DT_INST_PROP(index, dma_channels),				\
		.gen_nb = DT_INST_PROP(index, dma_generators),					\
		.req_nb = DT_INST_PROP(index, dma_requests),					\
		.mux_channels = dmamux_stm32_channels_##index,					\
	};											\
												\
	static struct dmamux_stm32_data dmamux_stm32_data_##index;				\
												\
	DEVICE_DT_INST_DEFINE(index,								\
			      dmamux_stm32_init,						\
			      NULL,								\
			      &dmamux_stm32_data_##index, &dmamux_stm32_config_##index,		\
			      PRE_KERNEL_1, CONFIG_DMAMUX_STM32_INIT_PRIORITY,			\
			      &dma_funcs);

DT_INST_FOREACH_STATUS_OKAY(DMAMUX_INIT)

/*
 * Make sure that this driver is initialized after the DMA driver (higher priority)
 */
BUILD_ASSERT(CONFIG_DMAMUX_STM32_INIT_PRIORITY >= CONFIG_DMA_INIT_PRIORITY,
	     "CONFIG_DMAMUX_STM32_INIT_PRIORITY must be higher than CONFIG_DMA_INIT_PRIORITY");
