/*
 * Copyright 2023-2024 NXP
 * All rights reserved.
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#include <zephyr/drivers/dma.h>
#include <zephyr/drivers/dma/dma_mcux_pxp.h>
#include <zephyr/devicetree.h>

#include <fsl_pxp.h>
#ifdef CONFIG_HAS_MCUX_CACHE
#include <fsl_cache.h>
#endif

#define DT_DRV_COMPAT nxp_pxp

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

struct dma_mcux_pxp_config {
	PXP_Type *base;
	void (*irq_config_func)(const struct device *dev);
};

struct dma_mcux_pxp_data {
	void *user_data;
	dma_callback_t dma_callback;
	uint32_t ps_buf_addr;
	uint32_t ps_buf_size;
	uint32_t out_buf_addr;
	uint32_t out_buf_size;
};

static void dma_mcux_pxp_irq_handler(const struct device *dev)
{
	const struct dma_mcux_pxp_config *config = dev->config;
	struct dma_mcux_pxp_data *data = dev->data;

	PXP_ClearStatusFlags(config->base, kPXP_CompleteFlag);
#ifdef CONFIG_HAS_MCUX_CACHE
	DCACHE_InvalidateByRange((uint32_t)data->out_buf_addr, data->out_buf_size);
#endif
	if (data->dma_callback) {
		data->dma_callback(dev, data->user_data, 0, 0);
	}
}

/* Configure a channel */
static int dma_mcux_pxp_configure(const struct device *dev, uint32_t channel,
				  struct dma_config *config)
{
	const struct dma_mcux_pxp_config *dev_config = dev->config;
	struct dma_mcux_pxp_data *dev_data = dev->data;
	pxp_ps_buffer_config_t ps_buffer_cfg;
	pxp_output_buffer_config_t output_buffer_cfg;
	uint8_t bytes_per_pixel;
	pxp_rotate_degree_t rotate;
	pxp_flip_mode_t flip;

	ARG_UNUSED(channel);
	if (config->channel_direction != MEMORY_TO_MEMORY) {
		return -ENOTSUP;
	}
	/*
	 * Use the DMA slot value to get the pixel format and rotation
	 * settings
	 */
	switch ((config->dma_slot & DMA_MCUX_PXP_CMD_MASK) >> DMA_MCUX_PXP_CMD_SHIFT) {
	case DMA_MCUX_PXP_CMD_ROTATE_0:
		rotate = kPXP_Rotate0;
		break;
	case DMA_MCUX_PXP_CMD_ROTATE_90:
		rotate = kPXP_Rotate90;
		break;
	case DMA_MCUX_PXP_CMD_ROTATE_180:
		rotate = kPXP_Rotate180;
		break;
	case DMA_MCUX_PXP_CMD_ROTATE_270:
		rotate = kPXP_Rotate270;
		break;
	default:
		return -ENOTSUP;
	}
	switch ((config->dma_slot & DMA_MCUX_PXP_FMT_MASK) >> DMA_MCUX_PXP_FMT_SHIFT) {
	case DMA_MCUX_PXP_FMT_RGB565:
		ps_buffer_cfg.pixelFormat = kPXP_PsPixelFormatRGB565;
		output_buffer_cfg.pixelFormat = kPXP_OutputPixelFormatRGB565;
		bytes_per_pixel = 2;
		break;
	case DMA_MCUX_PXP_FMT_RGB888:
#if (!(defined(FSL_FEATURE_PXP_HAS_NO_EXTEND_PIXEL_FORMAT) && \
	FSL_FEATURE_PXP_HAS_NO_EXTEND_PIXEL_FORMAT)) && \
	(!(defined(FSL_FEATURE_PXP_V3) && FSL_FEATURE_PXP_V3))
		ps_buffer_cfg.pixelFormat = kPXP_PsPixelFormatARGB8888;
#else
		ps_buffer_cfg.pixelFormat = kPXP_PsPixelFormatRGB888;
#endif
		output_buffer_cfg.pixelFormat = kPXP_OutputPixelFormatRGB888;
		bytes_per_pixel = 3;
		break;
	case DMA_MCUX_PXP_FMT_ARGB8888:
		ps_buffer_cfg.pixelFormat = kPXP_PsPixelFormatARGB8888;
		output_buffer_cfg.pixelFormat = kPXP_OutputPixelFormatARGB8888;
		bytes_per_pixel = 4;
		break;
	default:
		return -ENOTSUP;
	}
	/*
	 * Use the DMA linked_channel value to get the flip settings.
	 */
	switch ((config->linked_channel & DMA_MCUX_PXP_FLIP_MASK) >> DMA_MCUX_PXP_FLIP_SHIFT) {
	case DMA_MCUX_PXP_FLIP_DISABLE:
		flip = kPXP_FlipDisable;
		break;
	case DMA_MCUX_PXP_FLIP_HORIZONTAL:
		flip = kPXP_FlipHorizontal;
		break;
	case DMA_MCUX_PXP_FLIP_VERTICAL:
		flip = kPXP_FlipVertical;
		break;
	case DMA_MCUX_PXP_FLIP_BOTH:
		flip = kPXP_FlipBoth;
		break;
	default:
		return -ENOTSUP;
	}
	DCACHE_CleanByRange((uint32_t)config->head_block->source_address,
			    config->head_block->block_size);

	/*
	 * Some notes on how specific fields of the DMA config are used by
	 * the PXP:
	 * head block source address: PS buffer source address
	 * head block destination address: Output buffer address
	 * head block block size: size of destination and source buffer
	 * source data size: width of source buffer in bytes (pitch)
	 * source burst length: height of source buffer in pixels
	 * dest data size: width of destination buffer in bytes (pitch)
	 * dest burst length: height of destination buffer in pixels
	 */
	ps_buffer_cfg.swapByte = false;
	ps_buffer_cfg.bufferAddr = config->head_block->source_address;
	ps_buffer_cfg.bufferAddrU = 0U;
	ps_buffer_cfg.bufferAddrV = 0U;
	ps_buffer_cfg.pitchBytes = config->source_data_size;
	PXP_SetProcessSurfaceBufferConfig(dev_config->base, &ps_buffer_cfg);

	output_buffer_cfg.interlacedMode = kPXP_OutputProgressive;
	output_buffer_cfg.buffer0Addr = config->head_block->dest_address;
	output_buffer_cfg.buffer1Addr = 0U;
	output_buffer_cfg.pitchBytes = config->dest_data_size;
	output_buffer_cfg.width = (config->dest_data_size / bytes_per_pixel);
	output_buffer_cfg.height = config->dest_burst_length;
	PXP_SetOutputBufferConfig(dev_config->base, &output_buffer_cfg);
	/* We only support a process surface that covers the full buffer */
	PXP_SetProcessSurfacePosition(dev_config->base, 0U, 0U, output_buffer_cfg.width,
				      output_buffer_cfg.height);
	/* Setup rotation */
	PXP_SetRotateConfig(dev_config->base, kPXP_RotateProcessSurface, rotate, flip);

	dev_data->ps_buf_addr = config->head_block->source_address;
	dev_data->ps_buf_size = config->head_block->block_size;
	dev_data->out_buf_addr = config->head_block->dest_address;
	dev_data->out_buf_size = config->head_block->block_size;
	dev_data->dma_callback = config->dma_callback;
	dev_data->user_data = config->user_data;
	return 0;
}

static int dma_mcux_pxp_start(const struct device *dev, uint32_t channel)
{
	const struct dma_mcux_pxp_config *config = dev->config;
	struct dma_mcux_pxp_data *data = dev->data;
#ifdef CONFIG_HAS_MCUX_CACHE
	DCACHE_CleanByRange((uint32_t)data->ps_buf_addr, data->ps_buf_size);
#endif

	ARG_UNUSED(channel);
	PXP_Start(config->base);
	return 0;
}

static DEVICE_API(dma, dma_mcux_pxp_api) = {
	.config = dma_mcux_pxp_configure,
	.start = dma_mcux_pxp_start,
};

static int dma_mcux_pxp_init(const struct device *dev)
{
	const struct dma_mcux_pxp_config *config = dev->config;

	PXP_Init(config->base);
	PXP_SetProcessSurfaceBackGroundColor(config->base, 0U);
	/* Disable alpha surface and CSC1 */
	PXP_SetAlphaSurfacePosition(config->base, 0xFFFFU, 0xFFFFU, 0U, 0U);
	PXP_EnableCsc1(config->base, false);
	PXP_EnableInterrupts(config->base, kPXP_CompleteInterruptEnable);
	config->irq_config_func(dev);
	return 0;
}

#define DMA_INIT(n)                                                                                \
	static void dma_pxp_config_func##n(const struct device *dev)                               \
	{                                                                                          \
		IF_ENABLED(DT_INST_IRQ_HAS_IDX(n, 0),                                              \
			   (IRQ_CONNECT(DT_INST_IRQN(n), DT_INST_IRQ(n, priority),                 \
					dma_mcux_pxp_irq_handler, DEVICE_DT_INST_GET(n), 0);       \
			    irq_enable(DT_INST_IRQ(n, irq));))                                     \
	}                                                                                          \
                                                                                                   \
	static const struct dma_mcux_pxp_config dma_config_##n = {                                 \
		.base = (PXP_Type *)DT_INST_REG_ADDR(n),                                           \
		.irq_config_func = dma_pxp_config_func##n,                                         \
	};                                                                                         \
                                                                                                   \
	static struct dma_mcux_pxp_data dma_data_##n;                                              \
                                                                                                   \
	DEVICE_DT_INST_DEFINE(n, dma_mcux_pxp_init, NULL, &dma_data_##n, &dma_config_##n,          \
			      PRE_KERNEL_1, CONFIG_DMA_INIT_PRIORITY, &dma_mcux_pxp_api);

DT_INST_FOREACH_STATUS_OKAY(DMA_INIT)
