// SPDX-License-Identifier: GPL-2.0-only
/*
 * Ilitek ILI9806E TFT LCD drm_panel driver.
 *
 * Copyright (C) 2022 Raspberry Pi Ltd
 *
 * Derived from drivers/drm/gpu/panel/panel-sitronix-st7789v.c
 * Copyright (C) 2017 Free Electrons
 */

#include <drm/drm_modes.h>
#include <drm/drm_panel.h>

#include <linux/bitops.h>
#include <linux/gpio/consumer.h>
#include <linux/media-bus-format.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/regmap.h>
#include <linux/regulator/consumer.h>
#include <linux/spi/spi.h>

#include <video/mipi_display.h>
#include <video/of_videomode.h>
#include <video/videomode.h>

struct ili9806 {
	struct drm_panel panel;
	struct spi_device *spi;
	struct gpio_desc *reset;
	struct regulator *power;
	u32 bus_format;
};

#define ILI9806_DATA		BIT(8)

#define ILI9806_MAX_MSG_LEN	6

struct ili9806e_msg {
	unsigned int len;
	u16 msg[ILI9806_MAX_MSG_LEN];
};

#define ILI9806_SET_PAGE(page)	\
	{				\
		.len = 6,		\
		.msg = {		\
			0xFF,			\
			ILI9806_DATA | 0xFF,	\
			ILI9806_DATA | 0x98,	\
			ILI9806_DATA | 0x06,	\
			ILI9806_DATA | 0x04,	\
			ILI9806_DATA | (page)	\
		},				\
	}

#define ILI9806_SET_REG_PARAM(reg, data)	\
	{					\
		.len = 2,			\
		.msg = {			\
			(reg),			\
			ILI9806_DATA | (data),	\
		},				\
	}

#define ILI9806_SET_REG(reg)	\
	{				\
		.len = 1,		\
		.msg = { (reg) },		\
	}

static const struct ili9806e_msg panel_init[] = {
	ILI9806_SET_PAGE(1),

	/* interface mode
	 *   SEPT_SDIO = 0 (spi interface transfer through SDA pin)
	 *   SDO_STATUS = 1 (always output, but without output tri-state)
	 */
	ILI9806_SET_REG_PARAM(0x08, 0x10),
	/* display control
	 * VSPL = 1 (vertical sync polarity)
	 * HSPL = 0 (horizontal sync polarity)
	 * DPL = 0 (PCLK polarity)
	 * EPL = 1 (data enable polarity)
	 */
	ILI9806_SET_REG_PARAM(0x21, 0x0d),
	/* resolution control (0x02 = 480x800) */
	ILI9806_SET_REG_PARAM(0x30, 0x02),
	/* display inversion control (0x00 = column inversion) */
	ILI9806_SET_REG_PARAM(0x31, 0x00),
	/* power control
	 *  EXB1T = 0 (internal charge pump)
	 *  EXT_CPCK_SEL = 1 (pump clock control signal = output 2 x waveform)
	 *  BT = 0 (DDVDH / DDVDL voltage = VCI x 2 / VCI x -2)
	 */
	ILI9806_SET_REG_PARAM(0x40, 0x10),
	/* power control
	 *  DDVDH_CLP = 5.6 (DDVDH clamp leve)
	 *  DDVDL_CLP = -5.6 (DDVDL clamp leve)
	 */
	ILI9806_SET_REG_PARAM(0x41, 0x55),
	/* power control
	 *  VGH_CP = 2DDVDH - DDVDL (step up factor for VGH)
	 *  VGL_CP = DDVDL + VCL - VCIP (step up factor for VGL)
	 */
	ILI9806_SET_REG_PARAM(0x42, 0x02),
	/* power control
	 *  VGH_CLPEN = 0 (disable VGH clamp level)
	 *  VGH_CLP = 9 (15.0 VGH clamp level - but this is disabled so not used?)
	 */
	ILI9806_SET_REG_PARAM(0x43, 0x84),
	/* power control
	 *  VGL_CLPEN = 0 (disable VGL clamp level)
	 *  VGL_CLP = 9 (-11.0 VGL clamp level - but this is disabled so not used?)
	 */
	ILI9806_SET_REG_PARAM(0x44, 0x84),

	/* power control
	 *  VREG1OUT voltage for positive gamma?
	 */
	ILI9806_SET_REG_PARAM(0x50, 0x78),
	/* power control
	 *  VREG2OUT voltage for negative gamma?
	 */
	ILI9806_SET_REG_PARAM(0x51, 0x78),

	ILI9806_SET_REG_PARAM(0x52, 0x00),
	ILI9806_SET_REG_PARAM(0x53, 0x77),
	ILI9806_SET_REG_PARAM(0x57, 0x60),
	ILI9806_SET_REG_PARAM(0x60, 0x07),
	ILI9806_SET_REG_PARAM(0x61, 0x00),
	ILI9806_SET_REG_PARAM(0x62, 0x08),
	ILI9806_SET_REG_PARAM(0x63, 0x00),
	ILI9806_SET_REG_PARAM(0xA0, 0x00),
	ILI9806_SET_REG_PARAM(0xA1, 0x07),
	ILI9806_SET_REG_PARAM(0xA2, 0x0C),
	ILI9806_SET_REG_PARAM(0xA3, 0x0B),
	ILI9806_SET_REG_PARAM(0xA4, 0x03),
	ILI9806_SET_REG_PARAM(0xA5, 0x07),
	ILI9806_SET_REG_PARAM(0xA6, 0x06),
	ILI9806_SET_REG_PARAM(0xA7, 0x04),
	ILI9806_SET_REG_PARAM(0xA8, 0x08),
	ILI9806_SET_REG_PARAM(0xA9, 0x0C),
	ILI9806_SET_REG_PARAM(0xAA, 0x13),
	ILI9806_SET_REG_PARAM(0xAB, 0x06),
	ILI9806_SET_REG_PARAM(0xAC, 0x0D),
	ILI9806_SET_REG_PARAM(0xAD, 0x19),
	ILI9806_SET_REG_PARAM(0xAE, 0x10),
	ILI9806_SET_REG_PARAM(0xAF, 0x00),
	/* negative gamma control
	 * set the gray scale voltage to adjust the gamma characteristics of the panel
	 */
	ILI9806_SET_REG_PARAM(0xC0, 0x00),
	ILI9806_SET_REG_PARAM(0xC1, 0x07),
	ILI9806_SET_REG_PARAM(0xC2, 0x0C),
	ILI9806_SET_REG_PARAM(0xC3, 0x0B),
	ILI9806_SET_REG_PARAM(0xC4, 0x03),
	ILI9806_SET_REG_PARAM(0xC5, 0x07),
	ILI9806_SET_REG_PARAM(0xC6, 0x07),
	ILI9806_SET_REG_PARAM(0xC7, 0x04),
	ILI9806_SET_REG_PARAM(0xC8, 0x08),
	ILI9806_SET_REG_PARAM(0xC9, 0x0C),
	ILI9806_SET_REG_PARAM(0xCA, 0x13),
	ILI9806_SET_REG_PARAM(0xCB, 0x06),
	ILI9806_SET_REG_PARAM(0xCC, 0x0D),
	ILI9806_SET_REG_PARAM(0xCD, 0x18),
	ILI9806_SET_REG_PARAM(0xCE, 0x10),
	ILI9806_SET_REG_PARAM(0xCF, 0x00),

	ILI9806_SET_PAGE(6),

	ILI9806_SET_REG_PARAM(0x00, 0x20),
	ILI9806_SET_REG_PARAM(0x01, 0x0A),
	ILI9806_SET_REG_PARAM(0x02, 0x00),
	ILI9806_SET_REG_PARAM(0x03, 0x00),
	ILI9806_SET_REG_PARAM(0x04, 0x01),
	ILI9806_SET_REG_PARAM(0x05, 0x01),
	ILI9806_SET_REG_PARAM(0x06, 0x98),
	ILI9806_SET_REG_PARAM(0x07, 0x06),
	ILI9806_SET_REG_PARAM(0x08, 0x01),
	ILI9806_SET_REG_PARAM(0x09, 0x80),
	ILI9806_SET_REG_PARAM(0x0A, 0x00),
	ILI9806_SET_REG_PARAM(0x0B, 0x00),
	ILI9806_SET_REG_PARAM(0x0C, 0x01),
	ILI9806_SET_REG_PARAM(0x0D, 0x01),
	ILI9806_SET_REG_PARAM(0x0E, 0x00),
	ILI9806_SET_REG_PARAM(0x0F, 0x00),
	ILI9806_SET_REG_PARAM(0x10, 0xF0),
	ILI9806_SET_REG_PARAM(0x11, 0xF4),
	ILI9806_SET_REG_PARAM(0x12, 0x01),
	ILI9806_SET_REG_PARAM(0x13, 0x00),
	ILI9806_SET_REG_PARAM(0x14, 0x00),
	ILI9806_SET_REG_PARAM(0x15, 0xC0),
	ILI9806_SET_REG_PARAM(0x16, 0x08),
	ILI9806_SET_REG_PARAM(0x17, 0x00),
	ILI9806_SET_REG_PARAM(0x18, 0x00),
	ILI9806_SET_REG_PARAM(0x19, 0x00),
	ILI9806_SET_REG_PARAM(0x1A, 0x00),
	ILI9806_SET_REG_PARAM(0x1B, 0x00),
	ILI9806_SET_REG_PARAM(0x1C, 0x00),
	ILI9806_SET_REG_PARAM(0x1D, 0x00),
	ILI9806_SET_REG_PARAM(0x20, 0x01),
	ILI9806_SET_REG_PARAM(0x21, 0x23),
	ILI9806_SET_REG_PARAM(0x22, 0x45),
	ILI9806_SET_REG_PARAM(0x23, 0x67),
	ILI9806_SET_REG_PARAM(0x24, 0x01),
	ILI9806_SET_REG_PARAM(0x25, 0x23),
	ILI9806_SET_REG_PARAM(0x26, 0x45),
	ILI9806_SET_REG_PARAM(0x27, 0x67),
	ILI9806_SET_REG_PARAM(0x30, 0x11),
	ILI9806_SET_REG_PARAM(0x31, 0x11),
	ILI9806_SET_REG_PARAM(0x32, 0x00),
	ILI9806_SET_REG_PARAM(0x33, 0xEE),
	ILI9806_SET_REG_PARAM(0x34, 0xFF),
	ILI9806_SET_REG_PARAM(0x35, 0xBB),
	ILI9806_SET_REG_PARAM(0x36, 0xAA),
	ILI9806_SET_REG_PARAM(0x37, 0xDD),
	ILI9806_SET_REG_PARAM(0x38, 0xCC),
	ILI9806_SET_REG_PARAM(0x39, 0x66),
	ILI9806_SET_REG_PARAM(0x3A, 0x77),
	ILI9806_SET_REG_PARAM(0x3B, 0x22),
	ILI9806_SET_REG_PARAM(0x3C, 0x22),
	ILI9806_SET_REG_PARAM(0x3D, 0x22),
	ILI9806_SET_REG_PARAM(0x3E, 0x22),
	ILI9806_SET_REG_PARAM(0x3F, 0x22),
	ILI9806_SET_REG_PARAM(0x40, 0x22),
	/* register doesn't exist on page 6? */
	ILI9806_SET_REG_PARAM(0x52, 0x10),
	/* doesn't make sense, not valid according to datasheet */
	ILI9806_SET_REG_PARAM(0x53, 0x10),
	/* doesn't make sense, not valid according to datasheet */
	ILI9806_SET_REG_PARAM(0x54, 0x13),

	ILI9806_SET_PAGE(7),

	/* enable VREG */
	ILI9806_SET_REG_PARAM(0x18, 0x1D),
	/* enable VGL_REG */
	ILI9806_SET_REG_PARAM(0x17, 0x22),
	/* register doesn't exist on page 7? */
	ILI9806_SET_REG_PARAM(0x02, 0x77),
	/* register doesn't exist on page 7? */
	ILI9806_SET_REG_PARAM(0x26, 0xB2),
	/* register doesn't exist on page 7? */
	ILI9806_SET_REG_PARAM(0xE1, 0x79),

	ILI9806_SET_PAGE(0),

	ILI9806_SET_REG_PARAM(MIPI_DCS_SET_PIXEL_FORMAT,
			      MIPI_DCS_PIXEL_FMT_18BIT << 4),
	ILI9806_SET_REG_PARAM(MIPI_DCS_SET_TEAR_ON, 0x00),
	ILI9806_SET_REG(MIPI_DCS_EXIT_SLEEP_MODE),
};

#define NUM_INIT_REGS ARRAY_SIZE(panel_init)

static inline struct ili9806 *panel_to_ili9806(struct drm_panel *panel)
{
	return container_of(panel, struct ili9806, panel);
}

static int ili9806_write_msg(struct ili9806 *ctx, const struct ili9806e_msg *msg)
{
	struct spi_transfer xfer = { };
	struct spi_message spi;
	//u16 txbuf[] = { msg->, ILI9806_DATA | data };

	spi_message_init(&spi);

	xfer.tx_buf = msg->msg;
	xfer.bits_per_word = 9;
	xfer.len = sizeof(u16) * msg->len;

	spi_message_add_tail(&xfer, &spi);
	return spi_sync(ctx->spi, &spi);
}

static int ili9806e_write_msg_list(struct ili9806 *ctx,
				   const struct ili9806e_msg msgs[],
				   unsigned int num_msgs)
{
	int ret, i;

	for (i = 0; i < num_msgs; i++) {
		ret = ili9806_write_msg(ctx, &msgs[i]);
		if (ret)
			break;
	}

	return ret;
}

static const struct drm_display_mode ili9806e_480x800_mode = {
	.clock = 32000,
	.hdisplay = 480,
	.hsync_start = 480 + 10,
	.hsync_end = 480 + 10 + 16,
	.htotal = 480 + 10 + 16 + 59,
	.vdisplay = 800,
	.vsync_start = 800 + 15,
	.vsync_end = 800 + 15 + 113,
	.vtotal = 800 + 15 + 113 + 15,
	.flags = DRM_MODE_FLAG_NHSYNC | DRM_MODE_FLAG_NVSYNC,
};

static int ili9806_get_modes(struct drm_panel *panel,
			     struct drm_connector *connector)
{
	struct ili9806 *ctx = panel_to_ili9806(panel);
	struct drm_display_mode *mode;

	mode = drm_mode_duplicate(connector->dev, &ili9806e_480x800_mode);
	if (!mode) {
		dev_err(panel->dev, "failed to add mode %ux%ux@%u\n",
			ili9806e_480x800_mode.hdisplay,
			ili9806e_480x800_mode.vdisplay,
			drm_mode_vrefresh(&ili9806e_480x800_mode));
		return -ENOMEM;
	}

	drm_mode_set_name(mode);

	mode->type = DRM_MODE_TYPE_DRIVER | DRM_MODE_TYPE_PREFERRED;
	drm_mode_probed_add(connector, mode);

	connector->display_info.width_mm = 61;
	connector->display_info.height_mm = 103;
	drm_display_info_set_bus_formats(&connector->display_info,
					 &ctx->bus_format, 1);
	connector->display_info.bus_flags =
					DRM_BUS_FLAG_PIXDATA_DRIVE_NEGEDGE;

	return 1;
}

static int ili9806_prepare(struct drm_panel *panel)
{
	struct ili9806 *ctx = panel_to_ili9806(panel);
	int ret;

	ret = regulator_enable(ctx->power);
	if (ret)
		return ret;

	ret = ili9806e_write_msg_list(ctx, panel_init, NUM_INIT_REGS);

	return ret;
}

static int ili9806_enable(struct drm_panel *panel)
{
	struct ili9806 *ctx = panel_to_ili9806(panel);
	const struct ili9806e_msg msg = ILI9806_SET_REG(MIPI_DCS_SET_DISPLAY_ON);
	int ret;

	ret = ili9806_write_msg(ctx, &msg);

	return ret;
}

static int ili9806_disable(struct drm_panel *panel)
{
	struct ili9806 *ctx = panel_to_ili9806(panel);
	const struct ili9806e_msg msg = ILI9806_SET_REG(MIPI_DCS_SET_DISPLAY_OFF);
	int ret;

	ret = ili9806_write_msg(ctx, &msg);

	return ret;
}

static int ili9806_unprepare(struct drm_panel *panel)
{
	struct ili9806 *ctx = panel_to_ili9806(panel);
	const struct ili9806e_msg msg = ILI9806_SET_REG(MIPI_DCS_ENTER_SLEEP_MODE);
	int ret;

	ret = ili9806_write_msg(ctx, &msg);

	return ret;
}

static const struct drm_panel_funcs ili9806_drm_funcs = {
	.disable	= ili9806_disable,
	.enable		= ili9806_enable,
	.get_modes	= ili9806_get_modes,
	.prepare	= ili9806_prepare,
	.unprepare	= ili9806_unprepare,
};

static const struct of_device_id ili9806_of_match[] = {
	{	.compatible = "txw,txw397017s2",
		.data = (void *)MEDIA_BUS_FMT_RGB888_1X24,
	}, {
		.compatible = "pimoroni,hyperpixel4",
		.data = (void *)MEDIA_BUS_FMT_RGB666_1X24_CPADHI,
	}, {
		.compatible = "ilitek,ili9806e",
		.data = (void *)MEDIA_BUS_FMT_RGB888_1X24,
	}, {
		/* sentinel */
	}
};
MODULE_DEVICE_TABLE(of, ili9806_of_match);

static int ili9806_probe(struct spi_device *spi)
{
	const struct ili9806e_msg panel_reset[] = {
		ILI9806_SET_PAGE(0),
		ILI9806_SET_REG_PARAM(0x01, 0x00)
	};
	struct ili9806 *ctx;
	int ret;

	ctx = devm_kzalloc(&spi->dev, sizeof(*ctx), GFP_KERNEL);
	if (!ctx)
		return -ENOMEM;

	ctx->bus_format = (u32)(uintptr_t)of_device_get_match_data(&spi->dev);

	spi_set_drvdata(spi, ctx);
	ctx->spi = spi;

	drm_panel_init(&ctx->panel, &spi->dev, &ili9806_drm_funcs,
		       DRM_MODE_CONNECTOR_DPI);

	ctx->power = devm_regulator_get(&spi->dev, "power");
	if (IS_ERR(ctx->power))
		return PTR_ERR(ctx->power);

	ctx->reset = devm_gpiod_get_optional(&spi->dev, "reset", GPIOD_OUT_LOW);
	if (IS_ERR(ctx->reset)) {
		dev_err(&spi->dev, "Couldn't get our reset line\n");
		return PTR_ERR(ctx->reset);
	}

	/* Soft reset */
	ili9806e_write_msg_list(ctx, panel_reset, ARRAY_SIZE(panel_reset));
	msleep(200);

	ret = drm_panel_of_backlight(&ctx->panel);
	if (ret)
		return ret;

	drm_panel_add(&ctx->panel);

	return 0;
}

static void ili9806_remove(struct spi_device *spi)
{
	struct ili9806 *ctx = spi_get_drvdata(spi);

	drm_panel_remove(&ctx->panel);
}

static const struct spi_device_id ili9806_ids[] = {
	{ "txw397017s2", 0 },
	{ "ili9806e", 0 },
	{ "hyperpixel4", 0 },
	{ /* sentinel */ }
};

MODULE_DEVICE_TABLE(spi, ili9806_ids);

static struct spi_driver ili9806_driver = {
	.probe = ili9806_probe,
	.remove = ili9806_remove,
	.driver = {
		.name = "ili9806e",
		.of_match_table = ili9806_of_match,
	},
	.id_table = ili9806_ids,
};
module_spi_driver(ili9806_driver);

MODULE_AUTHOR("Dave Stevenson <dave.stevenson@raspberrypi.com>");
MODULE_DESCRIPTION("ili9806 LCD panel driver");
MODULE_LICENSE("GPL v2");
