/*
 * Copyright (c) 2023 Renesas Electronics Corporation
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#define DT_DRV_COMPAT  renesas_smartbond_display

#include <zephyr/drivers/pinctrl.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/irq.h>
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/clock_control.h>
#include <zephyr/drivers/clock_control/smartbond_clock_control.h>
#include <zephyr/drivers/display.h>
#include <zephyr/sys/util.h>
#include <zephyr/drivers/dma.h>
#include <da1469x_lcdc.h>
#include <DA1469xAB.h>
#include <da1469x_pd.h>
#include <zephyr/linker/devicetree_regions.h>

#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(smartbond_display, CONFIG_DISPLAY_LOG_LEVEL);

#define SMARTBOND_IRQN		DT_INST_IRQN(0)
#define SMARTBOND_IRQ_PRIO  DT_INST_IRQ(0, priority)

#define LCDC_SMARTBOND_CLK_DIV(_freq)                           \
	((32000000U % (_freq)) ? (96000000U / (_freq)) : (32000000U / (_freq)))

#define LCDC_SMARTBOND_IS_PLL_REQUIRED \
	!!(32000000U % DT_PROP(DT_INST_CHILD(0, display_timings), clock_frequency))

#define DISPLAY_SMARTBOND_IS_DMA_PREFETCH_ENABLED \
	DT_INST_ENUM_IDX_OR(0, dma_prefetch, 0)

#define LCDC_LAYER0_OFFSETX_REG_SET_FIELD(_field, _var, _val)\
	((_var)) =	\
	((_var) & ~(LCDC_LCDC_LAYER0_OFFSETX_REG_ ## _field ## _Msk)) |	\
	(((_val) << LCDC_LCDC_LAYER0_OFFSETX_REG_ ## _field ## _Pos) &	\
	LCDC_LCDC_LAYER0_OFFSETX_REG_ ## _field ## _Msk)

#define DISPLAY_SMARTBOND_PIXEL_SIZE(inst)	\
	(DISPLAY_BITS_PER_PIXEL(DT_INST_PROP(inst, pixel_format)) / 8)

#if CONFIG_DISPLAY_RENESAS_LCDC_BUFFER_PSRAM
#define DISPLAY_BUFFER_LINKER_SECTION \
	Z_GENERIC_SECTION(LINKER_DT_NODE_REGION_NAME(DT_NODELABEL(psram)))
#else
#define DISPLAY_BUFFER_LINKER_SECTION
#endif

struct display_smartbond_data {
	/* Provide mutual exclusion when a display operation is requested. */
	struct k_sem device_sem;
	/* Frame update synchronization token */
	struct k_sem sync_sem;
	/* Flag indicating whether or not an underflow took place */
	volatile bool underflow_flag;
	/* Layer settings */
	lcdc_smartbond_layer_cfg layer;
	/* Frame buffer */
	uint8_t *buffer;
	/* DMA device */
	const struct device *dma;
	/* DMA configuration structures */
	struct dma_config dma_cfg;
	struct dma_block_config dma_block_cfg;
	/* DMA memory transfer synchronization token */
	struct k_sem dma_sync_sem;
	/* Granted DMA channel used for memory transfers */
	int dma_channel;
};

struct display_smartbond_config {
	/* Reference to device instance's pinctrl configurations */
	const struct pinctrl_dev_config *pcfg;
	/* Display ON/OFF GPIO */
	const struct gpio_dt_spec disp;
	/* Host controller's timing settings */
	lcdc_smartbond_timing_cfg timing_cfg;
	/* Parallel interface settings */
	lcdc_smartbond_mode_cfg mode;
	/* Background default color configuration */
	lcdc_smartbond_bgcolor_cfg bgcolor_cfg;
	/* Display dimensions */
	const uint16_t x_res;
	const uint16_t y_res;
	/* Pixel size in bytes */
	uint8_t pixel_size;
	enum display_pixel_format pixel_format;
};

/* Display pixel to layer color format translation */
static uint8_t lcdc_smartbond_pixel_to_lcm(enum display_pixel_format pixel_format)
{
	switch (pixel_format) {
	case PIXEL_FORMAT_RGB_565:
		return (uint8_t)LCDC_SMARTBOND_L0_RGB565;
	case PIXEL_FORMAT_ARGB_8888:
		return (uint8_t)LCDC_SMARTBOND_L0_ARGB8888;
	default:
		LOG_ERR("Unsupported pixel format");
		return 0;
	};
}

static int display_smartbond_configure(const struct device *dev)
{
	uint8_t clk_div =
		LCDC_SMARTBOND_CLK_DIV(DT_PROP(DT_INST_CHILD(0, display_timings), clock_frequency));

	const struct display_smartbond_config *config = dev->config;
	struct display_smartbond_data *data = dev->data;

	int ret = 0;

	/* First enable the controller so registers can be written. */
	da1469x_lcdc_set_status(true, LCDC_SMARTBOND_IS_PLL_REQUIRED, clk_div);

	if (!da1469x_lcdc_check_id()) {
		LOG_ERR("Invalid LCDC ID");
		da1469x_lcdc_set_status(false, false, 0);
		return -EINVAL;
	}

	da1469x_lcdc_parallel_interface_configure((lcdc_smartbond_mode_cfg *)&config->mode);
	da1469x_lcdc_bgcolor_configure((lcdc_smartbond_bgcolor_cfg *)&config->bgcolor_cfg);

	/*
	 * Partial update is not supported and so timing and layer settings can be configured
	 * once at initialization.
	 */
	ret = da1469x_lcdc_timings_configure(config->x_res, config->y_res,
						(lcdc_smartbond_timing_cfg *)&config->timing_cfg);
	if (ret < 0) {
		LOG_ERR("Unable to configure timing settings");
		da1469x_lcdc_set_status(false, false, 0);
		return ret;
	}

	/*
	 * Stride should be updated at the end of a frame update (typically in ISR context).
	 * It's OK to update stride here as continuous mode should not be enabled yet.
	 */
	data->layer.color_format =
		lcdc_smartbond_pixel_to_lcm(config->pixel_format);
	data->layer.stride =
		da1469x_lcdc_stride_calculation(data->layer.color_format, config->x_res);

	ret = da1469x_lcdc_layer_configure(&data->layer);
	if (ret < 0) {
		LOG_ERR("Unable to configure layer settings");
		da1469x_lcdc_set_status(false, false, 0);
	}

	LCDC_LAYER0_OFFSETX_REG_SET_FIELD(LCDC_L0_DMA_PREFETCH,
		LCDC->LCDC_LAYER0_OFFSETX_REG, DISPLAY_SMARTBOND_IS_DMA_PREFETCH_ENABLED);

	LCDC->LCDC_MODE_REG |= LCDC_LCDC_MODE_REG_LCDC_MODE_EN_Msk;

	return ret;
}

static void smartbond_display_isr(const void *arg)
{
	struct display_smartbond_data *data =  ((const struct device *)arg)->data;

	data->underflow_flag = LCDC_STATUS_REG_GET_FIELD(LCDC_STICKY_UNDERFLOW);

	/*
	 * Underflow sticky bit will remain high until cleared by writing
	 * any value to LCDC_INTERRUPT_REG.
	 */
	LCDC->LCDC_INTERRUPT_REG &= ~LCDC_LCDC_INTERRUPT_REG_LCDC_VSYNC_IRQ_EN_Msk;

	/* Notify that current frame update is completed */
	k_sem_give(&data->sync_sem);
}

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

	/* Select default state */
	ret = pinctrl_apply_state(config->pcfg, PINCTRL_STATE_DEFAULT);
	if (ret < 0) {
		LOG_ERR("Could not apply LCDC pins' default state (%d)", ret);
		return -EIO;
	}

#if LCDC_SMARTBOND_IS_PLL_REQUIRED
	const struct device *clock_dev = DEVICE_DT_GET(DT_NODELABEL(osc));

	if (!device_is_ready(clock_dev)) {
		LOG_WRN("Clock device is not ready");
		return -ENODEV;
	}

	ret = z_smartbond_select_sys_clk(SMARTBOND_CLK_PLL96M);
	if (ret < 0) {
		LOG_WRN("Could not switch to PLL");
		return -EIO;
	}
#endif

	return display_smartbond_configure(dev);
}

static void display_smartbond_dma_cb(const struct device *dma, void *arg,
				uint32_t id, int status)
{
	struct display_smartbond_data *data = arg;

	if (status < 0) {
		LOG_WRN("DMA transfer did not complete");
	}

	k_sem_give(&data->dma_sync_sem);
}

static int display_smartbond_dma_config(const struct device *dev)
{
	struct display_smartbond_data *data = dev->data;

	data->dma = DEVICE_DT_GET(DT_NODELABEL(dma));
	if (!device_is_ready(data->dma)) {
		LOG_ERR("DMA device is not ready");
		return -ENODEV;
	}

	data->dma_cfg.channel_direction = MEMORY_TO_MEMORY;
	data->dma_cfg.user_data = data;
	data->dma_cfg.dma_callback = display_smartbond_dma_cb;
	data->dma_cfg.block_count = 1;
	data->dma_cfg.head_block = &data->dma_block_cfg;

	/* Request an arbitrary DMA channel */
	data->dma_channel = dma_request_channel(data->dma, NULL);
	if (data->dma_channel < 0) {
		LOG_ERR("Could not acquire a DMA channel");
		return -EIO;
	}

	return 0;
}


static int display_smartbond_init(const struct device *dev)
{
	const struct display_smartbond_config *config = dev->config;
	struct display_smartbond_data *data = dev->data;
	int ret;

	/* Device should be ready to be acquired */
	k_sem_init(&data->device_sem, 1, 1);
	/* Event should be signaled by LCDC ISR */
	k_sem_init(&data->sync_sem, 0, 1);
	/* Event should be signaled by DMA ISR */
	k_sem_init(&data->dma_sync_sem, 0, 1);

	/* As per docs, display port should be enabled by default. */
	if (gpio_is_ready_dt(&config->disp)) {
		ret = gpio_pin_configure_dt(&config->disp, GPIO_OUTPUT_ACTIVE);
		if (ret < 0) {
			LOG_ERR("Could not activate display port");
			return -EIO;
		}
	}

	ret = display_smartbond_resume(dev);
	if (ret < 0) {
		return ret;
	}

	ret = display_smartbond_dma_config(dev);
	if (ret < 0) {
		return ret;
	}

	IRQ_CONNECT(SMARTBOND_IRQN, SMARTBOND_IRQ_PRIO, smartbond_display_isr,
								DEVICE_DT_INST_GET(0), 0);

#if CONFIG_PM
	/*
	 * When in continues mode, the display device should always be refreshed
	 * and so the controller is not allowed to be turned off. The latter, is
	 * powered by PD_SYS which is turned off when the SoC enters the extended
	 * sleep state. By acquiring PD_SYS, the deep sleep state is prevented
	 * and the system enters the low-power state (i.e. ARM WFI) when possible.
	 *
	 * XXX CONFIG_PM_DEVICE_RUNTIME is no supported yet!
	 */
	da1469x_pd_acquire_noconf(MCU_PD_DOMAIN_SYS);
#endif

	return 0;
}

static int display_smartbond_blanking_on(const struct device *dev)
{
	const struct display_smartbond_config *config = dev->config;
	struct display_smartbond_data *data = dev->data;
	int ret = 0;

	k_sem_take(&data->device_sem, K_FOREVER);

	/*
	 * This bit will force LCD controller's output to blank that is,
	 * the controller will keep operating without outputting any
	 * pixel data.
	 */
	LCDC->LCDC_MODE_REG |= LCDC_LCDC_MODE_REG_LCDC_FORCE_BLANK_Msk;

	/* If enabled, disable display port. */
	if (gpio_is_ready_dt(&config->disp)) {
		ret = gpio_pin_configure_dt(&config->disp, GPIO_OUTPUT_INACTIVE);
		if (ret < 0) {
			LOG_WRN("Display port could not be de-activated");
		}
	}

	k_sem_give(&data->device_sem);

	return ret;
}

static int display_smartbond_blanking_off(const struct device *dev)
{
	const struct display_smartbond_config *config = dev->config;
	struct display_smartbond_data *data = dev->data;
	int ret = 0;

	k_sem_take(&data->device_sem, K_FOREVER);

	/* If used, enable display port */
	if (gpio_is_ready_dt(&config->disp)) {
		ret = gpio_pin_configure_dt(&config->disp, GPIO_OUTPUT_ACTIVE);
		if (ret < 0) {
			LOG_WRN("Display port could not be activated");
		}
	}

	/*
	 * This bit will force LCD controller's output to blank that is,
	 * the controller will keep operating without outputting any
	 * pixel data.
	 */
	LCDC->LCDC_MODE_REG &= ~LCDC_LCDC_MODE_REG_LCDC_FORCE_BLANK_Msk;

	k_sem_give(&data->device_sem);

	return ret;
}

static void *display_smartbond_get_framebuffer(const struct device *dev)
{
	struct display_smartbond_data *data = dev->data;

	return ((void *)data->buffer);
}

static void display_smartbond_get_capabilities(const struct device *dev,
			struct display_capabilities *capabilities)
{
	memset(capabilities, 0, sizeof(*capabilities));

	/*
	 * Multiple color formats should be supported by LCDC. Currently, RGB56 and ARGB888
	 * exposed by display API are supported. In the future we should consider supporting
	 * more color formats which should require changes in LVGL porting.
	 * Here, only one color format should be supported as the frame buffer is accessed
	 * directly by LCDC and is allocated statically during device initialization. The color
	 * format is defined based on the pixel-format property dictated by lcd-controller
	 * bindings.
	 */
	capabilities->supported_pixel_formats = DT_INST_PROP(0, pixel_format);
	capabilities->current_orientation = DISPLAY_ORIENTATION_NORMAL;
	capabilities->current_pixel_format = DT_INST_PROP(0, pixel_format);
	capabilities->x_resolution = DT_INST_PROP(0, width);
	capabilities->y_resolution = DT_INST_PROP(0, height);
}

static int display_smartbond_read(const struct device *dev,
			const uint16_t x, const uint16_t y,
			const struct display_buffer_descriptor *desc,
			void *buf)
{
	struct display_smartbond_data *data = dev->data;
	const struct display_smartbond_config *config = dev->config;
	uint8_t *dst = buf;
	const uint8_t *src = data->buffer;

	k_sem_take(&data->device_sem, K_FOREVER);

	/* pointer to upper left pixel of the rectangle */
	src += (x * config->pixel_size);
	src += (y * data->layer.stride);

	data->dma_block_cfg.block_size = desc->width * config->pixel_size;
	/*
	 * Source and destination base address is word aligned.
	 * Data size should be selected based on color depth as
	 * cursor is shifted multiple of pixel color depth.
	 */
	data->dma_cfg.source_data_size = data->dma_cfg.dest_data_size =
			!(config->pixel_size & 3) ? 4 :
			!(config->pixel_size & 1) ? 2 : 1;

	data->dma_cfg.dest_burst_length = data->dma_cfg.source_burst_length =
		!((data->dma_block_cfg.block_size / data->dma_cfg.source_data_size) & 7) ? 8 :
		!((data->dma_block_cfg.block_size / data->dma_cfg.source_data_size) & 3) ? 4 : 1;

	for (int row = 0; row < desc->height; row++) {

		data->dma_block_cfg.dest_address = (uint32_t)dst;
		data->dma_block_cfg.source_address = (uint32_t)src;

		if (dma_config(data->dma, data->dma_channel, &data->dma_cfg)) {
			LOG_ERR("Could not configure DMA");
			k_sem_give(&data->device_sem);
			return -EIO;
		}

		if (dma_start(data->dma, data->dma_channel)) {
			LOG_ERR("Could not start DMA");
			k_sem_give(&data->device_sem);
			return -EIO;
		}

		k_sem_take(&data->dma_sync_sem, K_FOREVER);

		src += data->layer.stride;
		dst += (desc->pitch * config->pixel_size);
	}

	if (dma_stop(data->dma, data->dma_channel)) {
		LOG_WRN("Could not stop DMA");
	}

	k_sem_give(&data->device_sem);

	return 0;
}

static int display_smartbond_write(const struct device *dev,
				const uint16_t x, const uint16_t y,
				const struct display_buffer_descriptor *desc,
				const void *buf)
{
	struct display_smartbond_data *data = dev->data;
	const struct display_smartbond_config *config = dev->config;
	uint8_t *dst = data->buffer;
	const uint8_t *src = buf;

	k_sem_take(&data->device_sem, K_FOREVER);

	/* pointer to upper left pixel of the rectangle */
	dst += (x * config->pixel_size);
	dst += (y * data->layer.stride);

	/*
	 * Wait for the current frame to finish. Do not disable continuous mode as this
	 * will have visual artifacts.
	 */
	LCDC->LCDC_INTERRUPT_REG |= LCDC_LCDC_INTERRUPT_REG_LCDC_VSYNC_IRQ_EN_Msk;
	k_sem_take(&data->sync_sem, K_FOREVER);

	data->dma_block_cfg.block_size = desc->width * config->pixel_size;
	/*
	 * Source and destination base address is word aligned.
	 * Data size should be selected based on color depth as
	 * cursor is shifted multiple of pixel color depth.
	 */
	data->dma_cfg.source_data_size = data->dma_cfg.dest_data_size =
			!(config->pixel_size & 3) ? 4 :
			!(config->pixel_size & 1) ? 2 : 1;

	data->dma_cfg.dest_burst_length = data->dma_cfg.source_burst_length =
		!((data->dma_block_cfg.block_size / data->dma_cfg.source_data_size) & 7) ? 8 :
		!((data->dma_block_cfg.block_size / data->dma_cfg.source_data_size) & 3) ? 4 : 1;

	for (int row = 0; row < desc->height; row++) {

		data->dma_block_cfg.dest_address = (uint32_t)dst;
		data->dma_block_cfg.source_address = (uint32_t)src;

		if (dma_config(data->dma, data->dma_channel, &data->dma_cfg)) {
			LOG_ERR("Could not configure DMA");
			k_sem_give(&data->device_sem);
			return -EIO;
		}

		if (dma_start(data->dma, data->dma_channel)) {
			LOG_ERR("Could not start DMA");
			k_sem_give(&data->device_sem);
			return -EIO;
		}

		k_sem_take(&data->dma_sync_sem, K_FOREVER);

		dst += data->layer.stride;
		src += (desc->pitch * config->pixel_size);
	}

	if (dma_stop(data->dma, data->dma_channel)) {
		LOG_WRN("Could not stop DMA");
	}

	k_sem_give(&data->device_sem);

	return 0;
}


static struct display_driver_api display_smartbond_driver_api = {
	.write =  display_smartbond_write,
	.read = display_smartbond_read,
	.get_framebuffer = display_smartbond_get_framebuffer,
	.get_capabilities = display_smartbond_get_capabilities,
	.blanking_off = display_smartbond_blanking_off,
	.blanking_on = display_smartbond_blanking_on
};

#define SMARTBOND_DISPLAY_INIT(inst)	\
	PINCTRL_DT_INST_DEFINE(inst);	\
									\
	__aligned(4) static uint8_t buffer_ ## inst[(((DT_INST_PROP(inst, width) * \
	DISPLAY_SMARTBOND_PIXEL_SIZE(inst)) + 0x3) & ~0x3) *	\
	DT_INST_PROP(inst, height)] DISPLAY_BUFFER_LINKER_SECTION;	\
                                            \
	static const struct display_smartbond_config display_smartbond_config_## inst = {  \
		.pcfg = PINCTRL_DT_INST_DEV_CONFIG_GET(inst),	\
		.disp = GPIO_DT_SPEC_INST_GET_OR(inst, disp_gpios, {}), \
		.timing_cfg.vsync_len =	\
			DT_PROP(DT_INST_CHILD(inst, display_timings), vsync_len),	\
		.timing_cfg.hsync_len =	\
			DT_PROP(DT_INST_CHILD(inst, display_timings), hsync_len),	\
		.timing_cfg.hfront_porch =	\
			DT_PROP(DT_INST_CHILD(inst, display_timings), hfront_porch),	\
		.timing_cfg.vfront_porch =	\
			DT_PROP(DT_INST_CHILD(inst, display_timings), vfront_porch),	\
		.timing_cfg.hback_porch =	\
			DT_PROP(DT_INST_CHILD(inst, display_timings), hback_porch),	\
		.timing_cfg.vback_porch =	\
			DT_PROP(DT_INST_CHILD(inst, display_timings), vback_porch),	\
		.bgcolor_cfg = {0xFF, 0xFF, 0xFF, 0}, \
		.x_res = DT_INST_PROP(inst, width), \
		.y_res = DT_INST_PROP(inst, height),	\
		.pixel_size = DISPLAY_SMARTBOND_PIXEL_SIZE(inst),	\
		.pixel_format = DT_INST_PROP(0, pixel_format),	\
		.mode.vsync_pol =	\
			DT_PROP(DT_INST_CHILD(inst, display_timings), vsync_active) ? 0 : 1,	\
		.mode.hsync_pol =	\
			DT_PROP(DT_INST_CHILD(inst, display_timings), vsync_active) ? 0 : 1,	\
		.mode.de_pol =	\
			DT_PROP(DT_INST_CHILD(inst, display_timings), de_active) ? 0 : 1,	\
		.mode.pixelclk_pol =	\
			DT_PROP(DT_INST_CHILD(inst, display_timings), pixelclk_active) ? 0 : 1,	\
	};							\
								\
	static struct display_smartbond_data display_smartbond_data_## inst = {	\
		.buffer = buffer_ ##inst,	\
		.layer.start_x = 0, \
		.layer.start_y = 0, \
		.layer.size_x = DT_INST_PROP(inst, width),	\
		.layer.size_y = DT_INST_PROP(inst, height),	\
		.layer.frame_buf = (uint32_t)buffer_ ## inst, \
	}; \
							\
							\
	DEVICE_DT_INST_DEFINE(inst, display_smartbond_init, NULL, \
				&display_smartbond_data_## inst,	\
				&display_smartbond_config_## inst,	\
				POST_KERNEL,	\
				CONFIG_DISPLAY_INIT_PRIORITY,	\
				&display_smartbond_driver_api);

SMARTBOND_DISPLAY_INIT(0);
