/*
 * Copyright (c) 2024 Bootlin
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#define DT_DRV_COMPAT st_stm32_fmc_mipi_dbi

#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/mipi_dbi.h>
#include <zephyr/drivers/clock_control/stm32_clock_control.h>
#include <zephyr/sys/barrier.h>
#include <zephyr/sys/sys_io.h>
#include <zephyr/sys/byteorder.h>

#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(mipi_dbi_stm32_fmc, CONFIG_MIPI_DBI_LOG_LEVEL);

struct mipi_dbi_stm32_fmc_config {
	/* Reset GPIO */
	const struct gpio_dt_spec reset;
	/* Power GPIO */
	const struct gpio_dt_spec power;
	mem_addr_t register_addr;
	mem_addr_t data_addr;
	uint32_t fmc_address_setup_time;
	uint32_t fmc_data_setup_time;
	uint32_t fmc_memory_width;
};

struct mipi_dbi_stm32_fmc_data {
	const struct mipi_dbi_config *dbi_config;
};

int mipi_dbi_stm32_fmc_check_config(const struct device *dev,
				    const struct mipi_dbi_config *dbi_config)
{
	const struct mipi_dbi_stm32_fmc_config *config = dev->config;
	struct mipi_dbi_stm32_fmc_data *data = dev->data;
	uint32_t fmc_write_cycles;

	if (data->dbi_config == dbi_config) {
		return 0;
	}

	if (dbi_config->mode != MIPI_DBI_MODE_8080_BUS_16_BIT) {
		LOG_ERR("Only support Intel 8080 16-bits");
		return -ENOTSUP;
	}

	if (config->fmc_memory_width != FMC_NORSRAM_MEM_BUS_WIDTH_16) {
		LOG_ERR("Only supports 16-bit bus width");
		return -EINVAL;
	}

	uint32_t hclk_freq =
		STM32_AHB_PRESCALER * DT_PROP(STM32_CLOCK_CONTROL_NODE, clock_frequency);

	/* According to the FMC documentation*/
	fmc_write_cycles =
		((config->fmc_address_setup_time + 1) + (config->fmc_data_setup_time + 1)) * 1;

	if (hclk_freq / fmc_write_cycles > dbi_config->config.frequency) {
		LOG_ERR("Frequency is too high for the display controller");
		return -EINVAL;
	}

	data->dbi_config = dbi_config;
	return 0;
}

int mipi_dbi_stm32_fmc_command_write(const struct device *dev,
				     const struct mipi_dbi_config *dbi_config, uint8_t cmd,
				     const uint8_t *data_buf, size_t len)
{
	const struct mipi_dbi_stm32_fmc_config *config = dev->config;
	int ret;
	size_t i;

	ret = mipi_dbi_stm32_fmc_check_config(dev, dbi_config);
	if (ret < 0) {
		return ret;
	}

	sys_write16(cmd, config->register_addr);
	if (IS_ENABLED(CONFIG_MIPI_DBI_STM32_FMC_MEM_BARRIER)) {
		barrier_dsync_fence_full();
	}

	for (i = 0U; i < len; i++) {
		sys_write16((uint16_t)data_buf[i], config->data_addr);
		if (IS_ENABLED(CONFIG_MIPI_DBI_STM32_FMC_MEM_BARRIER)) {
			barrier_dsync_fence_full();
		}
	}

	return 0;
}

static int mipi_dbi_stm32_fmc_write_display(const struct device *dev,
					    const struct mipi_dbi_config *dbi_config,
					    const uint8_t *framebuf,
					    struct display_buffer_descriptor *desc,
					    enum display_pixel_format pixfmt)
{
	const struct mipi_dbi_stm32_fmc_config *config = dev->config;
	size_t i;
	int ret;

	ret = mipi_dbi_stm32_fmc_check_config(dev, dbi_config);
	if (ret < 0) {
		return ret;
	}

	for (i = 0U; i < desc->buf_size; i += 2) {
		sys_write16(sys_get_le16(&framebuf[i]), config->data_addr);
		if (IS_ENABLED(CONFIG_MIPI_DBI_STM32_FMC_MEM_BARRIER)) {
			barrier_dsync_fence_full();
		}
	}

	return 0;
}

static int mipi_dbi_stm32_fmc_reset(const struct device *dev, k_timeout_t delay)
{
	const struct mipi_dbi_stm32_fmc_config *config = dev->config;
	int ret;

	if (config->reset.port == NULL) {
		return -ENOTSUP;
	}

	ret = gpio_pin_set_dt(&config->reset, 1);
	if (ret < 0) {
		return ret;
	}

	k_sleep(delay);

	return gpio_pin_set_dt(&config->reset, 0);
}

static int mipi_dbi_stm32_fmc_init(const struct device *dev)
{
	const struct mipi_dbi_stm32_fmc_config *config = dev->config;

	if (config->reset.port) {
		if (!gpio_is_ready_dt(&config->reset)) {
			LOG_ERR("Reset GPIO device not ready");
			return -ENODEV;
		}

		if (gpio_pin_configure_dt(&config->reset, GPIO_OUTPUT_INACTIVE)) {
			LOG_ERR("Couldn't configure reset pin");
			return -EIO;
		}
	}

	if (config->power.port) {
		if (!gpio_is_ready_dt(&config->power)) {
			LOG_ERR("Power GPIO device not ready");
			return -ENODEV;
		}

		if (gpio_pin_configure_dt(&config->power, GPIO_OUTPUT)) {
			LOG_ERR("Couldn't configure power pin");
			return -EIO;
		}
	}

	return 0;
}

static DEVICE_API(mipi_dbi, mipi_dbi_stm32_fmc_driver_api) = {
	.reset = mipi_dbi_stm32_fmc_reset,
	.command_write = mipi_dbi_stm32_fmc_command_write,
	.write_display = mipi_dbi_stm32_fmc_write_display,
};

#define MIPI_DBI_FMC_GET_ADDRESS(n) _CONCAT(FMC_BANK1_,                                            \
					UTIL_INC(DT_REG_ADDR_RAW(DT_INST_PARENT(n))))

#define MIPI_DBI_FMC_GET_DATA_ADDRESS(n)                                                           \
	MIPI_DBI_FMC_GET_ADDRESS(n) + (1 << (DT_INST_PROP(n, register_select_pin) + 1))

#define MIPI_DBI_STM32_FMC_INIT(n)                                                                 \
	static const struct mipi_dbi_stm32_fmc_config mipi_dbi_stm32_fmc_config_##n = {            \
		.reset = GPIO_DT_SPEC_INST_GET_OR(n, reset_gpios, {}),                             \
		.power = GPIO_DT_SPEC_INST_GET_OR(n, power_gpios, {}),                             \
		.register_addr = MIPI_DBI_FMC_GET_ADDRESS(n),                                      \
		.data_addr = MIPI_DBI_FMC_GET_DATA_ADDRESS(n),                                     \
		.fmc_address_setup_time = DT_PROP_BY_IDX(DT_INST_PARENT(n), st_timing, 0),         \
		.fmc_data_setup_time = DT_PROP_BY_IDX(DT_INST_PARENT(n), st_timing, 2),            \
		.fmc_memory_width = DT_PROP_BY_IDX(DT_INST_PARENT(n), st_control, 2),              \
	};                                                                                         \
                                                                                                   \
	static struct mipi_dbi_stm32_fmc_data mipi_dbi_stm32_fmc_data_##n;                         \
                                                                                                   \
	DEVICE_DT_INST_DEFINE(n, mipi_dbi_stm32_fmc_init, NULL, &mipi_dbi_stm32_fmc_data_##n,      \
			      &mipi_dbi_stm32_fmc_config_##n, POST_KERNEL,                         \
			      CONFIG_MIPI_DBI_INIT_PRIORITY, &mipi_dbi_stm32_fmc_driver_api);

DT_INST_FOREACH_STATUS_OKAY(MIPI_DBI_STM32_FMC_INIT)
