/*
 * Copyright (c) 2025 Arduino SA
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#define DT_DRV_COMPAT sitronix_st7701

#include <zephyr/kernel.h>
#include <zephyr/drivers/display.h>
#include <zephyr/drivers/mipi_dsi.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/pm/device.h>
#include <zephyr/sys/byteorder.h>
#include <zephyr/logging/log.h>

LOG_MODULE_REGISTER(st7701, CONFIG_DISPLAY_LOG_LEVEL);

/* Command2 BKx selection command */
#define DSI_CMD2BKX_SEL      0xFF
#define DSI_CMD2BK1_SEL      0x11
#define DSI_CMD2BK0_SEL      0x10
#define DSI_CMD2BKX_SEL_NONE 0x00

/* Command2, BK0 commands */
#define DSI_CMD2_BK0_PVGAMCTRL 0xB0 /* Positive Voltage Gamma Control */
#define DSI_CMD2_BK0_NVGAMCTRL 0xB1 /* Negative Voltage Gamma Control */
#define DSI_CMD2_BK0_LNESET    0xC0 /* Display Line setting */
#define DSI_CMD2_BK0_PORCTRL   0xC1 /* Porch control */
#define DSI_CMD2_BK0_INVSEL    0xC2 /* Inversion selection, Frame Rate Control */

/* Command2, BK1 commands */
#define DSI_CMD2_BK1_VRHS     0xB0 /* Vop amplitude setting */
#define DSI_CMD2_BK1_VCOM     0xB1 /* VCOM amplitude setting */
#define DSI_CMD2_BK1_VGHSS    0xB2 /* VGH Voltage setting */
#define DSI_CMD2_BK1_TESTCMD  0xB3 /* TEST Command Setting */
#define DSI_CMD2_BK1_VGLS     0xB5 /* VGL Voltage setting */
#define DSI_CMD2_BK1_PWCTLR1  0xB7 /* Power Control 1 */
#define DSI_CMD2_BK1_PWCTLR2  0xB8 /* Power Control 2 */
#define DSI_CMD2_BK1_SPD1     0xC1 /* Source pre_drive timing set1 */
#define DSI_CMD2_BK1_SPD2     0xC2 /* Source EQ2 Setting */
#define DSI_CMD2_BK1_MIPISET1 0xD0 /* MIPI Setting 1 */

#define ST7701_CMD_ID1 0xDA
#define ST7701_ID      0xFF

/* Write Control Display: brightness control. */
#define ST7701_WRCTRLD_BCTRL BIT(5)
/* Write Control Display: display dimming. */
#define ST7701_WRCTRLD_DD    BIT(3)
/* Write Control Display: backlight. */
#define ST7701_WRCTRLD_BL    BIT(2)

/* Adaptive Brightness Control: off. */
#define ST7701_WRCABC_OFF 0x00U
/* Adaptive Brightness Control: user interface. */
#define ST7701_WRCABC_UI  0x01U
/* Adaptive Brightness Control: still picture. */
#define ST7701_WRCABC_ST  0x02U
/* Adaptive Brightness Control: moving image. */
#define ST7701_WRCABC_MV  0x03U

struct st7701_config {
	const struct device *mipi_dsi;
	const struct gpio_dt_spec reset;
	const struct gpio_dt_spec backlight;
	uint8_t data_lanes;
	uint16_t width;
	uint16_t height;
	uint8_t channel;
	uint16_t rotation;
	uint32_t hbp;
	uint32_t hsync;
	uint32_t hfp;
	uint32_t vbp;
	uint32_t vsync;
	uint32_t vfp;
	uint8_t gip_e0[4];
	uint8_t gip_e1[12];
	uint8_t gip_e2[14];
	uint8_t gip_e3[5];
	uint8_t gip_e4[3];
	uint8_t gip_e5[17];
	uint8_t gip_e6[5];
	uint8_t gip_e7[3];
	uint8_t gip_e8[17];
	uint8_t gip_eb[8];
	uint8_t gip_ec[3];
	uint8_t gip_ed[17];
	uint8_t pvgamctrl[17];
	uint8_t nvgamctrl[17];
};

struct st7701_data {
	uint16_t xres;
	uint16_t yres;
	uint8_t dsi_pixel_format;
	enum display_pixel_format pixel_format;
	enum display_orientation orientation;
};

static inline int st7701_dcs_write(const struct device *dev, uint8_t cmd, const void *buf,
				   size_t len)
{
	const struct st7701_config *cfg = dev->config;
	int ret;

	ret = mipi_dsi_dcs_write(cfg->mipi_dsi, cfg->channel, cmd, buf, len);
	if (ret < 0) {
		LOG_ERR("DCS 0x%x write failed! (%d)", cmd, ret);
		return ret;
	}

	return 0;
}

static int st7701_short_write_1p(const struct device *dev, uint8_t cmd, uint8_t val)
{
	const struct st7701_config *cfg = dev->config;
	int ret;
	uint8_t buf[] = {cmd, val};

	ret = mipi_dsi_generic_write(cfg->mipi_dsi, cfg->channel, buf, sizeof(val));
	if (ret < 0) {
		LOG_ERR("Short write failed! (%d)", ret);
		return ret;
	}

	return 0;
}

static int st7701_generic_write(const struct device *dev, const void *buf, size_t len)
{
	const struct st7701_config *cfg = dev->config;
	int ret;

	ret = mipi_dsi_generic_write(cfg->mipi_dsi, cfg->channel, buf, len);
	if (ret < 0) {
		LOG_ERR("Generic write failed! (%d)", ret);
		return ret;
	}

	return 0;
}

static int st7701_check_id(const struct device *dev)
{
	const struct st7701_config *cfg = dev->config;
	uint32_t id = 0;
	int ret;

	ret = mipi_dsi_dcs_read(cfg->mipi_dsi, cfg->channel, ST7701_CMD_ID1, &id, sizeof(id));
	if (ret != sizeof(id)) {
		LOG_ERR("Read panel ID failed! (%d)", ret);
		return -EIO;
	}

	if (id != ST7701_ID) {
		LOG_ERR("ID 0x%x (should 0x%x)", id, ST7701_ID);
		return -EINVAL;
	}

	return 0;
}

static int st7701_configure(const struct device *dev)
{
	struct st7701_data *data = dev->data;
	const struct st7701_config *cfg = dev->config;
	uint8_t buf[4];
	int ret;

	const uint8_t ff1[] = {DSI_CMD2BKX_SEL, 0x77, 0x01, 0x00, 0x00, DSI_CMD2BK1_SEL};
	const uint8_t ff2[] = {DSI_CMD2BKX_SEL, 0x77, 0x01, 0x00, 0x00, DSI_CMD2BKX_SEL_NONE};

	const uint8_t control0[] = {DSI_CMD2BKX_SEL, 0x77, 0x01, 0x00, 0x00, DSI_CMD2BK0_SEL};
	const uint8_t control1[] = {0xC0, 0x63, 0x00};
	const uint8_t control2[] = {0xC1, 0x11, 0x02};
	const uint8_t control3[] = {0xC2, 0x01, 0x08};
	const uint8_t control4[] = {0xCC, 0x18};

	st7701_generic_write(dev, control0, sizeof(control0));
	st7701_generic_write(dev, control1, sizeof(control1));
	st7701_generic_write(dev, control2, sizeof(control2));
	st7701_generic_write(dev, control3, sizeof(control3));
	st7701_generic_write(dev, control4, sizeof(control4));

	/* Gamma Cluster Setting */
	st7701_generic_write(dev, cfg->pvgamctrl, sizeof(cfg->pvgamctrl));
	st7701_generic_write(dev, cfg->nvgamctrl, sizeof(cfg->nvgamctrl));

	/* Initial power control registers */
	st7701_generic_write(dev, ff1, sizeof(ff1));

	st7701_short_write_1p(dev, DSI_CMD2_BK1_VRHS, 0x65);
	st7701_short_write_1p(dev, DSI_CMD2_BK1_VCOM, 0x34);
	st7701_short_write_1p(dev, DSI_CMD2_BK1_VGHSS, 0x87);
	st7701_short_write_1p(dev, DSI_CMD2_BK1_TESTCMD, 0x80);

	st7701_short_write_1p(dev, DSI_CMD2_BK1_VGLS, 0x49);
	st7701_short_write_1p(dev, DSI_CMD2_BK1_PWCTLR1, 0x85);

	st7701_short_write_1p(dev, DSI_CMD2_BK1_PWCTLR2, 0x20);
	st7701_short_write_1p(dev, 0xB9, 0x10);
	st7701_short_write_1p(dev, DSI_CMD2_BK1_SPD1, 0x78);
	st7701_short_write_1p(dev, DSI_CMD2_BK1_SPD2, 0x78);
	st7701_short_write_1p(dev, DSI_CMD2_BK1_MIPISET1, 0x88);
	k_msleep(100);

	/* GIP Setting */
	st7701_generic_write(dev, cfg->gip_e0, sizeof(cfg->gip_e0));
	st7701_generic_write(dev, cfg->gip_e1, sizeof(cfg->gip_e1));
	st7701_generic_write(dev, cfg->gip_e2, sizeof(cfg->gip_e2));
	st7701_generic_write(dev, cfg->gip_e3, sizeof(cfg->gip_e3));
	st7701_generic_write(dev, cfg->gip_e4, sizeof(cfg->gip_e4));
	st7701_generic_write(dev, cfg->gip_e5, sizeof(cfg->gip_e5));
	st7701_generic_write(dev, cfg->gip_e6, sizeof(cfg->gip_e6));
	st7701_generic_write(dev, cfg->gip_e7, sizeof(cfg->gip_e7));
	st7701_generic_write(dev, cfg->gip_e8, sizeof(cfg->gip_e8));
	st7701_generic_write(dev, cfg->gip_eb, sizeof(cfg->gip_eb));
	st7701_generic_write(dev, cfg->gip_ec, sizeof(cfg->gip_ec));
	st7701_generic_write(dev, cfg->gip_ed, sizeof(cfg->gip_ed));

	/* Bank1 setting */
	st7701_generic_write(dev, ff2, sizeof(ff2));

	/* Exit sleep mode */
	ret = st7701_dcs_write(dev, MIPI_DCS_EXIT_SLEEP_MODE, NULL, 0);
	if (ret < 0) {
		return ret;
	}

	k_msleep(50);

	/* Set pixel color format */
	switch (data->dsi_pixel_format) {
	case MIPI_DSI_PIXFMT_RGB565:
		buf[0] = MIPI_DCS_PIXEL_FORMAT_16BIT;
		break;
	case MIPI_DSI_PIXFMT_RGB888:
		buf[0] = MIPI_DCS_PIXEL_FORMAT_24BIT;
		break;
	default:
		LOG_ERR("Unsupported pixel format 0x%x!", data->dsi_pixel_format);
		return -ENOTSUP;
	}

	ret = st7701_dcs_write(dev, MIPI_DCS_SET_PIXEL_FORMAT, buf, 1);
	if (ret < 0) {
		return ret;
	}

	buf[0] = 0x00;
	buf[1] = 0x00;
	sys_put_be16(data->xres, (uint8_t *)&buf[2]);
	ret = st7701_dcs_write(dev, MIPI_DCS_SET_COLUMN_ADDRESS, buf, 4);
	if (ret < 0) {
		return ret;
	}

	buf[0] = 0x00;
	buf[1] = 0x00;
	sys_put_be16(data->yres, (uint8_t *)&buf[2]);
	ret = st7701_dcs_write(dev, MIPI_DCS_SET_PAGE_ADDRESS, buf, 4);
	if (ret < 0) {
		return ret;
	}

	/* Backlight control */
	buf[0] = ST7701_WRCTRLD_BCTRL | ST7701_WRCTRLD_DD | ST7701_WRCTRLD_BL;
	ret = st7701_dcs_write(dev, MIPI_DCS_WRITE_CONTROL_DISPLAY, buf, 1);
	if (ret < 0) {
		return ret;
	}

	/* Adaptive brightness control */
	buf[0] = ST7701_WRCABC_UI;
	ret = st7701_dcs_write(dev, MIPI_DCS_WRITE_POWER_SAVE, buf, 1);
	if (ret < 0) {
		return ret;
	}

	/* Adaptive brightness control minimum brightness */
	buf[0] = 0xFF;
	ret = st7701_dcs_write(dev, MIPI_DCS_SET_CABC_MIN_BRIGHTNESS, buf, 1);
	if (ret < 0) {
		return ret;
	}

	/* Brightness */
	buf[0] = 0xFF;
	ret = st7701_dcs_write(dev, MIPI_DCS_SET_DISPLAY_BRIGHTNESS, buf, 1);
	if (ret < 0) {
		return ret;
	}

	/* Display On */
	ret = st7701_dcs_write(dev, MIPI_DCS_SET_DISPLAY_ON, NULL, 0);
	if (ret < 0) {
		return ret;
	}

	return 0;
}

static int st7701_blanking_on(const struct device *dev)
{
	const struct st7701_config *cfg = dev->config;
	int ret;

	if (cfg->backlight.port != NULL) {
		ret = gpio_pin_set_dt(&cfg->backlight, 0);
		if (ret) {
			LOG_ERR("Disable backlight failed! (%d)", ret);
			return ret;
		}
	}

	return st7701_dcs_write(dev, MIPI_DCS_SET_DISPLAY_OFF, NULL, 0);
}

static int st7701_blanking_off(const struct device *dev)
{
	const struct st7701_config *cfg = dev->config;
	int ret;

	if (cfg->backlight.port != NULL) {
		ret = gpio_pin_set_dt(&cfg->backlight, 1);
		if (ret) {
			LOG_ERR("Enable backlight failed! (%d)", ret);
			return ret;
		}
	}

	return st7701_dcs_write(dev, MIPI_DCS_SET_DISPLAY_ON, NULL, 0);
}

static int st7701_set_brightness(const struct device *dev, uint8_t brightness)
{
	return st7701_dcs_write(dev, MIPI_DCS_SET_DISPLAY_BRIGHTNESS, &brightness, 1);
}

static void st7701_get_capabilities(const struct device *dev,
				    struct display_capabilities *capabilities)
{
	const struct st7701_config *cfg = dev->config;
	struct st7701_data *data = dev->data;

	if (!capabilities) {
		return;
	}

	memset(capabilities, 0, sizeof(struct display_capabilities));
	capabilities->x_resolution = cfg->width;
	capabilities->y_resolution = cfg->height;
	capabilities->supported_pixel_formats = data->pixel_format;
	capabilities->current_pixel_format = data->pixel_format;
	capabilities->current_orientation = data->orientation;
}

#ifdef CONFIG_PM_DEVICE
static int st7701_pm_action(const struct device *dev, enum pm_device_action action)
{
	int ret;

	switch (action) {
	case PM_DEVICE_ACTION_RESUME:
		ret = st7701_dcs_write(dev, MIPI_DCS_EXIT_SLEEP_MODE, NULL, 0);
		break;
	case PM_DEVICE_ACTION_SUSPEND:
		ret = st7701_dcs_write(dev, MIPI_DCS_ENTER_SLEEP_MODE, NULL, 0);
		break;
	default:
		return -ENOTSUP;
	}

	return ret;
}
#endif

static DEVICE_API(display, st7701_api) = {
	.blanking_on = st7701_blanking_on,
	.blanking_off = st7701_blanking_off,
	.set_brightness = st7701_set_brightness,
	.get_capabilities = st7701_get_capabilities,
};

static int st7701_init(const struct device *dev)
{
	const struct st7701_config *cfg = dev->config;
	struct st7701_data *data = dev->data;
	struct mipi_dsi_device mdev;
	int ret;

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

		ret = gpio_pin_configure_dt(&cfg->reset, GPIO_OUTPUT_INACTIVE);
		if (ret < 0) {
			LOG_ERR("Reset display failed! (%d)", ret);
			return ret;
		}

		k_msleep(10);
		ret = gpio_pin_set_dt(&cfg->reset, 1);
		if (ret < 0) {
			LOG_ERR("Enable display failed! (%d)", ret);
			return ret;
		}

		k_msleep(100);
	}

	/* store x/y resolution & rotation */
	if (cfg->rotation == 0) {
		data->xres = cfg->width;
		data->yres = cfg->height;
		data->orientation = DISPLAY_ORIENTATION_NORMAL;
	} else if (cfg->rotation == 90) {
		data->xres = cfg->height;
		data->yres = cfg->width;
		data->orientation = DISPLAY_ORIENTATION_ROTATED_90;
	} else if (cfg->rotation == 180) {
		data->xres = cfg->width;
		data->yres = cfg->height;
		data->orientation = DISPLAY_ORIENTATION_ROTATED_180;
	} else if (cfg->rotation == 270) {
		data->xres = cfg->height;
		data->yres = cfg->width;
		data->orientation = DISPLAY_ORIENTATION_ROTATED_270;
	}

	/* attach to MIPI-DSI host */
	mdev.data_lanes = cfg->data_lanes;
	mdev.pixfmt = data->dsi_pixel_format;
	mdev.mode_flags = MIPI_DSI_MODE_VIDEO | MIPI_DSI_MODE_VIDEO_BURST | MIPI_DSI_MODE_LPM;

	mdev.timings.hactive = cfg->width;
	mdev.timings.hbp = cfg->hbp;
	mdev.timings.hsync = cfg->hsync;
	mdev.timings.hfp = cfg->hfp;
	mdev.timings.vactive = cfg->height;
	mdev.timings.vbp = cfg->vbp;
	mdev.timings.vsync = cfg->vsync;
	mdev.timings.vfp = cfg->vfp;

	ret = mipi_dsi_attach(cfg->mipi_dsi, cfg->channel, &mdev);
	if (ret < 0) {
		LOG_ERR("MIPI-DSI attach failed! (%d)", ret);
		return ret;
	}

	ret = st7701_check_id(dev);
	if (ret) {
		LOG_ERR("Panel ID check failed! (%d)", ret);
		return ret;
	}

	ret = st7701_configure(dev);
	if (ret) {
		LOG_ERR("DSI init sequence failed! (%d)", ret);
		return ret;
	}

	ret = st7701_blanking_off(dev);
	if (ret) {
		LOG_ERR("Display blanking off failed! (%d)", ret);
		return ret;
	}

	return 0;
}

#define ST7701_DEVICE(inst)                                                                        \
	static const struct st7701_config st7701_config_##inst = {                                 \
		.mipi_dsi = DEVICE_DT_GET(DT_INST_BUS(inst)),                                      \
		.reset = GPIO_DT_SPEC_INST_GET_OR(inst, reset_gpios, {0}),                         \
		.backlight = GPIO_DT_SPEC_INST_GET_OR(inst, bl_gpios, {0}),                        \
		.data_lanes = DT_INST_PROP_BY_IDX(inst, data_lanes, 0),                            \
		.width = DT_INST_PROP(inst, width),                                                \
		.height = DT_INST_PROP(inst, height),                                              \
		.channel = DT_INST_REG_ADDR(inst),                                                 \
		.rotation = DT_INST_PROP(inst, rotation),                                          \
		.hbp = DT_PROP(DT_INST_CHILD(inst, display_timings), hback_porch),                 \
		.hsync = DT_PROP(DT_INST_CHILD(inst, display_timings), hsync_len),                 \
		.hfp = DT_PROP(DT_INST_CHILD(inst, display_timings), hfront_porch),                \
		.vbp = DT_PROP(DT_INST_CHILD(inst, display_timings), vback_porch),                 \
		.vsync = DT_PROP(DT_INST_CHILD(inst, display_timings), vsync_len),                 \
		.vfp = DT_PROP(DT_INST_CHILD(inst, display_timings), vfront_porch),                \
		.gip_e0 = DT_INST_PROP_OR(inst, gip_e0, {}),                                       \
		.gip_e1 = DT_INST_PROP_OR(inst, gip_e1, {}),                                       \
		.gip_e2 = DT_INST_PROP_OR(inst, gip_e2, {}),                                       \
		.gip_e3 = DT_INST_PROP_OR(inst, gip_e3, {}),                                       \
		.gip_e4 = DT_INST_PROP_OR(inst, gip_e4, {}),                                       \
		.gip_e5 = DT_INST_PROP_OR(inst, gip_e5, {}),                                       \
		.gip_e6 = DT_INST_PROP_OR(inst, gip_e6, {}),                                       \
		.gip_e7 = DT_INST_PROP_OR(inst, gip_e7, {}),                                       \
		.gip_e8 = DT_INST_PROP_OR(inst, gip_e8, {}),                                       \
		.gip_eb = DT_INST_PROP_OR(inst, gip_eb, {}),                                       \
		.gip_ec = DT_INST_PROP_OR(inst, gip_ec, {}),                                       \
		.gip_ed = DT_INST_PROP_OR(inst, gip_ed, {}),                                       \
		.pvgamctrl = DT_INST_PROP_OR(inst, pvgamctrl, {}),                                 \
		.nvgamctrl = DT_INST_PROP_OR(inst, nvgamctrl, {}),                                 \
	};                                                                                         \
	static struct st7701_data st7701_data_##inst = {                                           \
		.dsi_pixel_format = DT_INST_PROP(inst, pixel_format),                              \
	};                                                                                         \
	PM_DEVICE_DT_INST_DEFINE(inst, st7701_pm_action);                                          \
	DEVICE_DT_INST_DEFINE(inst, &st7701_init, PM_DEVICE_DT_INST_GET(inst), &st7701_data_##inst,\
			      &st7701_config_##inst, POST_KERNEL,                                  \
			      CONFIG_DISPLAY_INIT_PRIORITY, &st7701_api);

DT_INST_FOREACH_STATUS_OKAY(ST7701_DEVICE)
