/*
 * Copyright (c) 2022 Andreas Sandberg
 * Copyright (c) 2018-2020 PHYTEC Messtechnik GmbH
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#define LOG_LEVEL CONFIG_DISPLAY_LOG_LEVEL
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(ssd16xx);

#include <string.h>
#include <zephyr/device.h>
#include <zephyr/drivers/display.h>
#include <zephyr/init.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/spi.h>
#include <zephyr/sys/byteorder.h>

#include <zephyr/display/ssd16xx.h>
#include "ssd16xx_regs.h"

/**
 * SSD16xx compatible EPD controller driver.
 */

#define EPD_PANEL_NUMOF_ROWS_PER_PAGE	8
#define SSD16XX_PANEL_FIRST_PAGE	0
#define SSD16XX_PANEL_FIRST_GATE	0
#define SSD16XX_PIXELS_PER_BYTE		8
#define SSD16XX_DEFAULT_TR_VALUE	25U
#define SSD16XX_TR_SCALE_FACTOR		256U


enum ssd16xx_profile_type {
	SSD16XX_PROFILE_FULL = 0,
	SSD16XX_PROFILE_PARTIAL,
	SSD16XX_NUM_PROFILES,
	SSD16XX_PROFILE_INVALID = SSD16XX_NUM_PROFILES,
};

struct ssd16xx_quirks {
	/* Gates */
	uint16_t max_width;
	/* Sources */
	uint16_t max_height;
	/* Width (bits) of integer type representing an x coordinate */
	uint8_t pp_width_bits;
	/* Width (bits) of integer type representing a y coordinate */
	uint8_t pp_height_bits;

	/*
	 * Device specific flags to be included in
	 * SSD16XX_CMD_UPDATE_CTRL2 for a full refresh.
	 */
	uint8_t ctrl2_full;
	/*
	 * Device specific flags to be included in
	 * SSD16XX_CMD_UPDATE_CTRL2 for a partial refresh.
	 */
	uint8_t ctrl2_partial;
};

struct ssd16xx_data {
	bool read_supported;
	uint8_t scan_mode;
	bool blanking_on;
	enum ssd16xx_profile_type profile;
};

struct ssd16xx_dt_array {
	uint8_t *data;
	uint8_t len;
};

struct ssd16xx_profile {
	struct ssd16xx_dt_array lut;
	struct ssd16xx_dt_array gdv;
	struct ssd16xx_dt_array sdv;
	uint8_t vcom;
	uint8_t bwf;
	uint8_t dummy_line;
	uint8_t gate_line_width;

	bool override_vcom;
	bool override_bwf;
	bool override_dummy_line;
	bool override_gate_line_width;
};

struct ssd16xx_config {
	struct spi_dt_spec bus;
	struct gpio_dt_spec dc_gpio;
	struct gpio_dt_spec busy_gpio;
	struct gpio_dt_spec reset_gpio;

	const struct ssd16xx_quirks *quirks;

	struct ssd16xx_dt_array softstart;

	const struct ssd16xx_profile *profiles[SSD16XX_NUM_PROFILES];

	bool orientation;
	uint16_t height;
	uint16_t width;
	uint8_t tssv;
};

static int ssd16xx_set_profile(const struct device *dev,
			       enum ssd16xx_profile_type type);

static inline void ssd16xx_busy_wait(const struct device *dev)
{
	const struct ssd16xx_config *config = dev->config;
	int pin = gpio_pin_get_dt(&config->busy_gpio);

	while (pin > 0) {
		__ASSERT(pin >= 0, "Failed to get pin level");
		k_msleep(SSD16XX_BUSY_DELAY);
		pin = gpio_pin_get_dt(&config->busy_gpio);
	}
}

static inline int ssd16xx_write_cmd(const struct device *dev, uint8_t cmd,
				    const uint8_t *data, size_t len)
{
	const struct ssd16xx_config *config = dev->config;
	struct spi_buf buf = {.buf = &cmd, .len = sizeof(cmd)};
	struct spi_buf_set buf_set = {.buffers = &buf, .count = 1};
	int err = 0;

	ssd16xx_busy_wait(dev);

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

	err = spi_write_dt(&config->bus, &buf_set);
	if (err < 0) {
		goto spi_out;
	}

	if (data != NULL) {
		buf.buf = (void *)data;
		buf.len = len;

		err = gpio_pin_set_dt(&config->dc_gpio, 0);
		if (err < 0) {
			goto spi_out;
		}

		err = spi_write_dt(&config->bus, &buf_set);
		if (err < 0) {
			goto spi_out;
		}
	}

spi_out:
	spi_release_dt(&config->bus);
	return err;
}

static inline int ssd16xx_write_uint8(const struct device *dev, uint8_t cmd,
				      uint8_t data)
{
	return ssd16xx_write_cmd(dev, cmd, &data, 1);
}

static inline int ssd16xx_read_cmd(const struct device *dev, uint8_t cmd,
				    uint8_t *data, size_t len)
{
	const struct ssd16xx_config *config = dev->config;
	const struct ssd16xx_data *dev_data = dev->data;
	struct spi_buf buf = {.buf = &cmd, .len = sizeof(cmd)};
	struct spi_buf_set buf_set = {.buffers = &buf, .count = 1};
	int err = 0;

	if (!dev_data->read_supported) {
		return -ENOTSUP;
	}

	ssd16xx_busy_wait(dev);

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

	err = spi_write_dt(&config->bus, &buf_set);
	if (err < 0) {
		goto spi_out;
	}

	if (data != NULL) {
		buf.buf = data;
		buf.len = len;

		err = gpio_pin_set_dt(&config->dc_gpio, 0);
		if (err < 0) {
			goto spi_out;
		}

		err = spi_read_dt(&config->bus, &buf_set);
		if (err < 0) {
			goto spi_out;
		}
	}

spi_out:
	spi_release_dt(&config->bus);
	return err;
}

static inline size_t push_x_param(const struct device *dev,
				  uint8_t *data, uint16_t x)
{
	const struct ssd16xx_config *config = dev->config;

	if (config->quirks->pp_width_bits == 8) {
		data[0] = (uint8_t)x;
		return 1;
	}

	if (config->quirks->pp_width_bits == 16) {
		sys_put_le16(sys_cpu_to_le16(x), data);
		return 2;
	}

	LOG_ERR("Unsupported pp_width_bits %u",
		config->quirks->pp_width_bits);
	return 0;
}

static inline size_t push_y_param(const struct device *dev,
				  uint8_t *data, uint16_t y)
{
	const struct ssd16xx_config *config = dev->config;

	if (config->quirks->pp_height_bits == 8) {
		data[0] = (uint8_t)y;
		return 1;
	}

	if (config->quirks->pp_height_bits == 16) {
		sys_put_le16(sys_cpu_to_le16(y), data);
		return 2;
	}

	LOG_ERR("Unsupported pp_height_bitsa %u",
		config->quirks->pp_height_bits);
	return 0;
}



static inline int ssd16xx_set_ram_param(const struct device *dev,
					uint16_t sx, uint16_t ex,
					uint16_t sy, uint16_t ey)
{
	int err;
	uint8_t tmp[4];
	size_t len;

	len  = push_x_param(dev, tmp, sx);
	len += push_x_param(dev, tmp + len, ex);
	err = ssd16xx_write_cmd(dev, SSD16XX_CMD_RAM_XPOS_CTRL, tmp, len);
	if (err < 0) {
		return err;
	}

	len  = push_y_param(dev, tmp, sy);
	len += push_y_param(dev, tmp + len, ey);
	err = ssd16xx_write_cmd(dev, SSD16XX_CMD_RAM_YPOS_CTRL, tmp,	len);
	if (err < 0) {
		return err;
	}

	return 0;
}

static inline int ssd16xx_set_ram_ptr(const struct device *dev, uint16_t x,
				      uint16_t y)
{
	int err;
	uint8_t tmp[2];
	size_t len;

	len = push_x_param(dev, tmp, x);
	err = ssd16xx_write_cmd(dev, SSD16XX_CMD_RAM_XPOS_CNTR, tmp, len);
	if (err < 0) {
		return err;
	}

	len = push_y_param(dev, tmp, y);
	return ssd16xx_write_cmd(dev, SSD16XX_CMD_RAM_YPOS_CNTR, tmp, len);
}

static int ssd16xx_activate(const struct device *dev, uint8_t ctrl2)
{
	int err;

	err = ssd16xx_write_uint8(dev, SSD16XX_CMD_UPDATE_CTRL2, ctrl2);
	if (err < 0) {
		return err;
	}

	return ssd16xx_write_cmd(dev, SSD16XX_CMD_MASTER_ACTIVATION, NULL, 0);
}

static int ssd16xx_update_display(const struct device *dev)
{
	const struct ssd16xx_config *config = dev->config;
	const struct ssd16xx_data *data = dev->data;
	const struct ssd16xx_profile *p = config->profiles[data->profile];
	const struct ssd16xx_quirks *quirks = config->quirks;
	const bool load_lut = !p || p->lut.len == 0;
	const bool load_temp = load_lut && config->tssv;
	const bool partial = data->profile == SSD16XX_PROFILE_PARTIAL;
	const uint8_t update_cmd =
		SSD16XX_CTRL2_ENABLE_CLK |
		SSD16XX_CTRL2_ENABLE_ANALOG |
		(load_lut ? SSD16XX_CTRL2_LOAD_LUT : 0) |
		(load_temp ? SSD16XX_CTRL2_LOAD_TEMPERATURE : 0) |
		(partial ? quirks->ctrl2_partial : quirks->ctrl2_full) |
		SSD16XX_CTRL2_DISABLE_ANALOG |
		SSD16XX_CTRL2_DISABLE_CLK;

	return ssd16xx_activate(dev, update_cmd);
}

static int ssd16xx_blanking_off(const struct device *dev)
{
	struct ssd16xx_data *data = dev->data;

	if (data->blanking_on) {
		data->blanking_on = false;
		return ssd16xx_update_display(dev);
	}

	return 0;
}

static int ssd16xx_blanking_on(const struct device *dev)
{
	struct ssd16xx_data *data = dev->data;

	if (!data->blanking_on) {
		if (ssd16xx_set_profile(dev, SSD16XX_PROFILE_FULL)) {
			return -EIO;
		}
	}

	data->blanking_on = true;

	return 0;
}

static int ssd16xx_set_window(const struct device *dev,
			      const uint16_t x, const uint16_t y,
			      const struct display_buffer_descriptor *desc)
{
	const struct ssd16xx_config *config = dev->config;
	const struct ssd16xx_data *data = dev->data;
	int err;
	uint16_t x_start;
	uint16_t x_end;
	uint16_t y_start;
	uint16_t y_end;
	uint16_t panel_h = config->height -
			   config->height % EPD_PANEL_NUMOF_ROWS_PER_PAGE;

	if (desc->pitch < desc->width) {
		LOG_ERR("Pitch is smaller than width");
		return -EINVAL;
	}

	if (desc->pitch > desc->width) {
		LOG_ERR("Unsupported mode");
		return -ENOTSUP;
	}

	if ((y + desc->height) > panel_h) {
		LOG_ERR("Buffer out of bounds (height)");
		return -EINVAL;
	}

	if ((x + desc->width) > config->width) {
		LOG_ERR("Buffer out of bounds (width)");
		return -EINVAL;
	}

	if ((desc->height % EPD_PANEL_NUMOF_ROWS_PER_PAGE) != 0U) {
		LOG_ERR("Buffer height not multiple of %d",
				EPD_PANEL_NUMOF_ROWS_PER_PAGE);
		return -EINVAL;
	}

	if ((y % EPD_PANEL_NUMOF_ROWS_PER_PAGE) != 0U) {
		LOG_ERR("Y coordinate not multiple of %d",
				EPD_PANEL_NUMOF_ROWS_PER_PAGE);
		return -EINVAL;
	}

	switch (data->scan_mode) {
	case SSD16XX_DATA_ENTRY_XIYDY:
		x_start = y / SSD16XX_PIXELS_PER_BYTE;
		x_end = (y + desc->height - 1) / SSD16XX_PIXELS_PER_BYTE;
		y_start = (x + desc->width - 1);
		y_end = x;
		break;

	case SSD16XX_DATA_ENTRY_XDYIY:
		x_start = (panel_h - 1 - y) / SSD16XX_PIXELS_PER_BYTE;
		x_end = (panel_h - 1 - (y + desc->height - 1)) /
			SSD16XX_PIXELS_PER_BYTE;
		y_start = x;
		y_end = (x + desc->width - 1);
		break;
	default:
		return -EINVAL;
	}

	err = ssd16xx_write_cmd(dev, SSD16XX_CMD_ENTRY_MODE,
				&data->scan_mode, sizeof(data->scan_mode));
	if (err < 0) {
		return err;
	}

	err = ssd16xx_set_ram_param(dev, x_start, x_end, y_start, y_end);
	if (err < 0) {
		return err;
	}

	err = ssd16xx_set_ram_ptr(dev, x_start, y_start);
	if (err < 0) {
		return err;
	}

	return 0;
}

static int ssd16xx_write(const struct device *dev, const uint16_t x,
			 const uint16_t y,
			 const struct display_buffer_descriptor *desc,
			 const void *buf)
{
	const struct ssd16xx_config *config = dev->config;
	const struct ssd16xx_data *data = dev->data;
	const bool have_partial_refresh =
		config->profiles[SSD16XX_PROFILE_PARTIAL] != NULL;
	const bool partial_refresh = !data->blanking_on && have_partial_refresh;
	const size_t buf_len = MIN(desc->buf_size,
				   desc->height * desc->width / 8);
	int err;

	if (buf == NULL || buf_len == 0U) {
		LOG_ERR("Display buffer is not available");
		return -EINVAL;
	}

	if (partial_refresh) {
		/*
		 * Request the partial profile. This operation becomes
		 * a no-op if the profile is already active.
		 */
		err = ssd16xx_set_profile(dev, SSD16XX_PROFILE_PARTIAL);
		if (err < 0) {
			return -EIO;
		}
	}

	err = ssd16xx_set_window(dev, x, y, desc);
	if (err < 0) {
		return err;
	}

	err = ssd16xx_write_cmd(dev, SSD16XX_CMD_WRITE_RAM, (uint8_t *)buf,
				buf_len);
	if (err < 0) {
		return err;
	}

	if (!data->blanking_on) {
		err = ssd16xx_update_display(dev);
		if (err < 0) {
			return err;
		}
	}

	if (data->blanking_on && have_partial_refresh) {
		/*
		 * We will trigger a full refresh when blanking is
		 * turned off. The controller won't keep track of the
		 * old frame buffer, which is needed to perform a
		 * partial update, when this happens. Maintain the old
		 * frame buffer manually here to make sure future
		 * partial updates will work as expected.
		 */
		err = ssd16xx_write_cmd(dev, SSD16XX_CMD_WRITE_RED_RAM,
					(uint8_t *)buf, buf_len);
		if (err < 0) {
			return err;
		}
	} else if (partial_refresh) {
		/*
		 * We just performed a partial refresh. After the
		 * refresh, the controller swaps the black/red buffers
		 * containing the current and new image. We need to
		 * perform a second write here to ensure that future
		 * updates work on an up-to-date framebuffer.
		 */
		err = ssd16xx_write_cmd(dev, SSD16XX_CMD_WRITE_RAM,
					(uint8_t *)buf, buf_len);
		if (err < 0) {
			return err;
		}
	}

	return 0;
}

int ssd16xx_read_ram(const struct device *dev, enum ssd16xx_ram ram_type,
		     const uint16_t x, const uint16_t y,
		     const struct display_buffer_descriptor *desc,
		     void *buf)
{
	const struct ssd16xx_data *data = dev->data;
	const size_t buf_len = MIN(desc->buf_size,
				   desc->height * desc->width / 8);
	int err;
	uint8_t ram_ctrl;

	if (!data->read_supported) {
		return -ENOTSUP;
	}

	switch (ram_type) {
	case SSD16XX_RAM_BLACK:
		ram_ctrl = SSD16XX_RAM_READ_CTRL_BLACK;
		break;

	case SSD16XX_RAM_RED:
		ram_ctrl = SSD16XX_RAM_READ_CTRL_RED;
		break;

	default:
		return -EINVAL;
	}

	if (buf == NULL || buf_len == 0U) {
		LOG_ERR("Display buffer is not available");
		return -EINVAL;
	}

	err = ssd16xx_set_window(dev, x, y, desc);
	if (err < 0) {
		return err;
	}

	err = ssd16xx_write_cmd(dev, SSD16XX_CMD_RAM_READ_CTRL,
				&ram_ctrl, sizeof(ram_ctrl));
	if (err < 0) {
		return err;
	}

	err = ssd16xx_read_cmd(dev, SSD16XX_CMD_READ_RAM, (uint8_t *)buf,
			       buf_len);
	if (err < 0) {
		return err;
	}

	return 0;
}

static int ssd16xx_read(const struct device *dev,
			const uint16_t x, const uint16_t y,
			const struct display_buffer_descriptor *desc,
			void *buf)
{
	return ssd16xx_read_ram(dev, SSD16XX_RAM_BLACK, x, y, desc, buf);
}

static void *ssd16xx_get_framebuffer(const struct device *dev)
{
	LOG_ERR("not supported");
	return NULL;
}

static int ssd16xx_set_brightness(const struct device *dev,
				  const uint8_t brightness)
{
	LOG_WRN("not supported");
	return -ENOTSUP;
}

static int ssd16xx_set_contrast(const struct device *dev, uint8_t contrast)
{
	LOG_WRN("not supported");
	return -ENOTSUP;
}

static void ssd16xx_get_capabilities(const struct device *dev,
				     struct display_capabilities *caps)
{
	const struct ssd16xx_config *config = dev->config;

	memset(caps, 0, sizeof(struct display_capabilities));
	caps->x_resolution = config->width;
	caps->y_resolution = config->height -
			     config->height % EPD_PANEL_NUMOF_ROWS_PER_PAGE;
	caps->supported_pixel_formats = PIXEL_FORMAT_MONO10;
	caps->current_pixel_format = PIXEL_FORMAT_MONO10;
	caps->screen_info = SCREEN_INFO_MONO_VTILED |
			    SCREEN_INFO_MONO_MSB_FIRST |
			    SCREEN_INFO_EPD;
}

static int ssd16xx_set_orientation(const struct device *dev,
				   const enum display_orientation
				   orientation)
{
	LOG_ERR("Unsupported");
	return -ENOTSUP;
}

static int ssd16xx_set_pixel_format(const struct device *dev,
				    const enum display_pixel_format pf)
{
	if (pf == PIXEL_FORMAT_MONO10) {
		return 0;
	}

	LOG_ERR("not supported");
	return -ENOTSUP;
}

static int ssd16xx_clear_cntlr_mem(const struct device *dev, uint8_t ram_cmd)
{
	const struct ssd16xx_config *config = dev->config;
	uint16_t panel_h = config->height / EPD_PANEL_NUMOF_ROWS_PER_PAGE;
	uint16_t last_gate = config->width - 1;
	uint8_t clear_page[64];
	int err;

	/*
	 * Clear unusable memory area when the resolution of the panel is not
	 * multiple of an octet.
	 */
	if (config->height % EPD_PANEL_NUMOF_ROWS_PER_PAGE) {
		panel_h += 1;
	}

	err = ssd16xx_write_uint8(dev, SSD16XX_CMD_ENTRY_MODE,
				  SSD16XX_DATA_ENTRY_XIYDY);
	if (err < 0) {
		return err;
	}

	err = ssd16xx_set_ram_param(dev, SSD16XX_PANEL_FIRST_PAGE,
				    panel_h - 1, last_gate,
				    SSD16XX_PANEL_FIRST_GATE);
	if (err < 0) {
		return err;
	}

	err = ssd16xx_set_ram_ptr(dev, SSD16XX_PANEL_FIRST_PAGE, last_gate);
	if (err < 0) {
		return err;
	}

	memset(clear_page, 0xff, sizeof(clear_page));
	for (int h = 0; h < panel_h; h++) {
		size_t x = config->width;

		while (x) {
			size_t l = MIN(x, sizeof(clear_page));

			x -= l;
			err = ssd16xx_write_cmd(dev, ram_cmd, clear_page, l);
			if (err < 0) {
				return err;
			}
		}
	}

	return 0;
}

static inline int ssd16xx_load_ws_from_otp_tssv(const struct device *dev)
{
	const struct ssd16xx_config *config = dev->config;

	/*
	 * Controller has an integrated temperature sensor or external
	 * temperature sensor is connected to the controller.
	 */
	LOG_INF("Select and load WS from OTP");
	return ssd16xx_write_uint8(dev, SSD16XX_CMD_TSENSOR_SELECTION,
				   config->tssv);
}

static inline int ssd16xx_load_ws_from_otp(const struct device *dev)
{
	int16_t t = (SSD16XX_DEFAULT_TR_VALUE * SSD16XX_TR_SCALE_FACTOR);
	uint8_t tmp[2];
	int err;

	LOG_INF("Load default WS (25 degrees Celsius) from OTP");

	err = ssd16xx_activate(dev, SSD16XX_CTRL2_ENABLE_CLK);
	if (err < 0) {
		return err;
	}

	/* Load temperature value */
	sys_put_be16(t, tmp);
	err = ssd16xx_write_cmd(dev, SSD16XX_CMD_TSENS_CTRL, tmp, 2);
	if (err < 0) {
		return err;
	}

	err = ssd16xx_activate(dev, SSD16XX_CTRL2_DISABLE_CLK);
	if (err < 0) {
		return err;
	}

	return 0;
}


static int ssd16xx_load_lut(const struct device *dev,
			    const struct ssd16xx_dt_array *lut)
{
	const struct ssd16xx_config *config = dev->config;

	if (lut && lut->len) {
		LOG_DBG("Using user-provided LUT");
		return ssd16xx_write_cmd(dev, SSD16XX_CMD_UPDATE_LUT,
					 lut->data, lut->len);
	} else {
		if (config->tssv) {
			return ssd16xx_load_ws_from_otp_tssv(dev);
		} else {
			return ssd16xx_load_ws_from_otp(dev);
		}
	}
}

static int ssd16xx_set_profile(const struct device *dev,
			       enum ssd16xx_profile_type type)
{
	const struct ssd16xx_config *config = dev->config;
	struct ssd16xx_data *data = dev->data;
	const struct ssd16xx_profile *p;
	const uint16_t last_gate = config->width - 1;
	uint8_t gdo[3];
	size_t gdo_len;
	int err = 0;

	if (type >= SSD16XX_NUM_PROFILES) {
		return -EINVAL;
	}

	p = config->profiles[type];

	/*
	 * The full profile is the only one that always exists. If it
	 * hasn't been specified, we use the defaults.
	 */
	if (!p && type != SSD16XX_PROFILE_FULL) {
		return -ENOENT;
	}

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

	/*
	 * Perform a soft reset to make sure registers are reset. This
	 * will leave the RAM contents intact.
	 */
	err = ssd16xx_write_cmd(dev, SSD16XX_CMD_SW_RESET, NULL, 0);
	if (err < 0) {
		return err;
	}

	gdo_len = push_y_param(dev, gdo, last_gate);
	gdo[gdo_len++] = 0U;
	err = ssd16xx_write_cmd(dev, SSD16XX_CMD_GDO_CTRL, gdo, gdo_len);
	if (err < 0) {
		return err;
	}

	if (config->softstart.len) {
		err = ssd16xx_write_cmd(dev, SSD16XX_CMD_SOFTSTART,
					config->softstart.data,
					config->softstart.len);
		if (err < 0) {
			return err;
		}
	}

	err = ssd16xx_load_lut(dev, p ? &p->lut : NULL);
	if (err < 0) {
		return err;
	}

	if (p && p->override_dummy_line) {
		err = ssd16xx_write_uint8(dev, SSD16XX_CMD_DUMMY_LINE,
					  p->dummy_line);
		if (err < 0) {
			return err;
		}
	}

	if (p && p->override_gate_line_width) {
		err = ssd16xx_write_uint8(dev, SSD16XX_CMD_GATE_LINE_WIDTH,
					  p->override_gate_line_width);
		if (err < 0) {
			return err;
		}
	}

	if (p && p->gdv.len) {
		LOG_DBG("Setting GDV");
		err = ssd16xx_write_cmd(dev, SSD16XX_CMD_GDV_CTRL,
					p->gdv.data, p->gdv.len);
		if (err < 0) {
			return err;
		}
	}

	if (p && p->sdv.len) {
		LOG_DBG("Setting SDV");
		err = ssd16xx_write_cmd(dev, SSD16XX_CMD_SDV_CTRL,
					p->sdv.data, p->sdv.len);
		if (err < 0) {
			return err;
		}
	}

	if (p && p->override_vcom) {
		LOG_DBG("Setting VCOM");
		err = ssd16xx_write_cmd(dev, SSD16XX_CMD_VCOM_VOLTAGE,
					&p->vcom, 1);
		if (err < 0) {
			return err;
		}
	}

	if (p && p->override_bwf) {
		LOG_DBG("Setting BWF");
		err = ssd16xx_write_cmd(dev, SSD16XX_CMD_BWF_CTRL,
					&p->bwf, 1);
		if (err < 0) {
			return err;
		}
	}

	data->profile = type;

	return 0;
}

static int ssd16xx_controller_init(const struct device *dev)
{
	const struct ssd16xx_config *config = dev->config;
	struct ssd16xx_data *data = dev->data;
	int err;

	LOG_DBG("");

	data->blanking_on = false;
	data->profile = SSD16XX_PROFILE_INVALID;

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

	k_msleep(SSD16XX_RESET_DELAY);
	err = gpio_pin_set_dt(&config->reset_gpio, 0);
	if (err < 0) {
		return err;
	}

	k_msleep(SSD16XX_RESET_DELAY);

	if (config->orientation == 1) {
		data->scan_mode = SSD16XX_DATA_ENTRY_XIYDY;
	} else {
		data->scan_mode = SSD16XX_DATA_ENTRY_XDYIY;
	}

	err = ssd16xx_set_profile(dev, SSD16XX_PROFILE_FULL);
	if (err < 0) {
		return err;
	}

	err = ssd16xx_clear_cntlr_mem(dev, SSD16XX_CMD_WRITE_RAM);
	if (err < 0) {
		return err;
	}

	err = ssd16xx_clear_cntlr_mem(dev, SSD16XX_CMD_WRITE_RED_RAM);
	if (err < 0) {
		return err;
	}

	err = ssd16xx_update_display(dev);
	if (err < 0) {
		return err;
	}

	return 0;
}

static int ssd16xx_init(const struct device *dev)
{
	const struct ssd16xx_config *config = dev->config;
	struct ssd16xx_data *data = dev->data;
	int err;

	LOG_DBG("");

	if (!spi_is_ready_dt(&config->bus)) {
		LOG_ERR("SPI bus %s not ready", config->bus.bus->name);
		return -ENODEV;
	}

	data->read_supported =
		(config->bus.config.operation & SPI_HALF_DUPLEX) != 0;

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

	err = gpio_pin_configure_dt(&config->reset_gpio, GPIO_OUTPUT_INACTIVE);
	if (err < 0) {
		LOG_ERR("Failed to configure reset GPIO");
		return err;
	}

	if (!gpio_is_ready_dt(&config->dc_gpio)) {
		LOG_ERR("DC GPIO device not ready");
		return -ENODEV;
	}

	err = gpio_pin_configure_dt(&config->dc_gpio, GPIO_OUTPUT_INACTIVE);
	if (err < 0) {
		LOG_ERR("Failed to configure DC GPIO");
		return err;
	}

	if (!gpio_is_ready_dt(&config->busy_gpio)) {
		LOG_ERR("Busy GPIO device not ready");
		return -ENODEV;
	}

	err = gpio_pin_configure_dt(&config->busy_gpio, GPIO_INPUT);
	if (err < 0) {
		LOG_ERR("Failed to configure busy GPIO");
		return err;
	}

	if (config->width > config->quirks->max_width ||
	    config->height > config->quirks->max_height) {
		LOG_ERR("Display size out of range.");
		return -EINVAL;
	}

	return ssd16xx_controller_init(dev);
}

static struct display_driver_api ssd16xx_driver_api = {
	.blanking_on = ssd16xx_blanking_on,
	.blanking_off = ssd16xx_blanking_off,
	.write = ssd16xx_write,
	.read = ssd16xx_read,
	.get_framebuffer = ssd16xx_get_framebuffer,
	.set_brightness = ssd16xx_set_brightness,
	.set_contrast = ssd16xx_set_contrast,
	.get_capabilities = ssd16xx_get_capabilities,
	.set_pixel_format = ssd16xx_set_pixel_format,
	.set_orientation = ssd16xx_set_orientation,
};

#if DT_HAS_COMPAT_STATUS_OKAY(solomon_ssd1608)
static struct ssd16xx_quirks quirks_solomon_ssd1608 = {
	.max_width = 320,
	.max_height = 240,
	.pp_width_bits = 16,
	.pp_height_bits = 16,
	.ctrl2_full = SSD16XX_GEN1_CTRL2_TO_PATTERN,
	.ctrl2_partial = SSD16XX_GEN1_CTRL2_TO_PATTERN,
};
#endif

#if DT_HAS_COMPAT_STATUS_OKAY(solomon_ssd1673)
static struct ssd16xx_quirks quirks_solomon_ssd1673 = {
	.max_width = 250,
	.max_height = 150,
	.pp_width_bits = 8,
	.pp_height_bits = 8,
	.ctrl2_full = SSD16XX_GEN1_CTRL2_TO_PATTERN,
	.ctrl2_partial = SSD16XX_GEN1_CTRL2_TO_PATTERN,
};
#endif

#if DT_HAS_COMPAT_STATUS_OKAY(solomon_ssd1675a)
static struct ssd16xx_quirks quirks_solomon_ssd1675a = {
	.max_width = 296,
	.max_height = 160,
	.pp_width_bits = 8,
	.pp_height_bits = 16,
	.ctrl2_full = SSD16XX_GEN1_CTRL2_TO_PATTERN,
	.ctrl2_partial = SSD16XX_GEN1_CTRL2_TO_PATTERN,
};
#endif

#if DT_HAS_COMPAT_STATUS_OKAY(solomon_ssd1680)
static const struct ssd16xx_quirks quirks_solomon_ssd1680 = {
	.max_width = 296,
	.max_height = 176,
	.pp_width_bits = 8,
	.pp_height_bits = 16,
	.ctrl2_full = SSD16XX_GEN2_CTRL2_DISPLAY,
	.ctrl2_partial = SSD16XX_GEN2_CTRL2_DISPLAY | SSD16XX_GEN2_CTRL2_MODE2,
};
#endif

#if DT_HAS_COMPAT_STATUS_OKAY(solomon_ssd1681)
static struct ssd16xx_quirks quirks_solomon_ssd1681 = {
	.max_width = 200,
	.max_height = 200,
	.pp_width_bits = 8,
	.pp_height_bits = 16,
	.ctrl2_full = SSD16XX_GEN2_CTRL2_DISPLAY,
	.ctrl2_partial = SSD16XX_GEN2_CTRL2_DISPLAY | SSD16XX_GEN2_CTRL2_MODE2,
};
#endif

#define SOFTSTART_ASSIGN(n)						\
		.softstart = {						\
			.data = softstart_##n,				\
			.len = sizeof(softstart_##n),			\
		},

#define SSD16XX_MAKE_ARRAY_OPT(n, p)					\
	static uint8_t data_ ## n ## _ ## p[] = DT_PROP_OR(n, p, {})

#define SSD16XX_ASSIGN_ARRAY(n, p)					\
	{								\
		.data = data_ ## n ## _ ## p,				\
		.len = sizeof(data_ ## n ## _ ## p),			\
	}

#define SSD16XX_PROFILE(n)						\
	SSD16XX_MAKE_ARRAY_OPT(n, lut);					\
	SSD16XX_MAKE_ARRAY_OPT(n, gdv);					\
	SSD16XX_MAKE_ARRAY_OPT(n, sdv);					\
									\
	static const struct ssd16xx_profile ssd16xx_profile_ ## n = {	\
		.lut = SSD16XX_ASSIGN_ARRAY(n, lut),			\
		.gdv = SSD16XX_ASSIGN_ARRAY(n, gdv),			\
		.sdv = SSD16XX_ASSIGN_ARRAY(n, sdv),			\
		.vcom = DT_PROP_OR(n, vcom, 0),				\
		.override_vcom = DT_NODE_HAS_PROP(n, vcom),		\
		.bwf = DT_PROP_OR(n, border_waveform, 0),		\
		.override_bwf = DT_NODE_HAS_PROP(n, border_waveform),	\
		.dummy_line = DT_PROP_OR(n, dummy_line, 0),		\
		.override_dummy_line = DT_NODE_HAS_PROP(n, dummy_line),	\
		.gate_line_width = DT_PROP_OR(n, gate_line_width, 0),	\
		.override_gate_line_width = DT_NODE_HAS_PROP(		\
			n, gate_line_width),				\
	};


#define _SSD16XX_PROFILE_PTR(n) &ssd16xx_profile_ ## n

#define SSD16XX_PROFILE_PTR(n)						\
	COND_CODE_1(DT_NODE_EXISTS(n),					\
		    (_SSD16XX_PROFILE_PTR(n)),				\
		    NULL)

#define SSD16XX_DEFINE(n, quirks_ptr)					\
	SSD16XX_MAKE_ARRAY_OPT(n, softstart);				\
									\
	DT_FOREACH_CHILD(n, SSD16XX_PROFILE);				\
									\
	static const struct ssd16xx_config ssd16xx_cfg_ ## n = {	\
		.bus = SPI_DT_SPEC_GET(n,				\
			SPI_OP_MODE_MASTER | SPI_WORD_SET(8) |		\
			SPI_HOLD_ON_CS | SPI_LOCK_ON,			\
			0),						\
		.reset_gpio = GPIO_DT_SPEC_GET(n, reset_gpios),		\
		.dc_gpio = GPIO_DT_SPEC_GET(n, dc_gpios),		\
		.busy_gpio = GPIO_DT_SPEC_GET(n, busy_gpios),		\
		.quirks = quirks_ptr,					\
		.height = DT_PROP(n, height),				\
		.width = DT_PROP(n, width),				\
		.orientation = DT_PROP(n, orientation_flipped),		\
		.tssv = DT_PROP_OR(n, tssv, 0),				\
		.softstart = SSD16XX_ASSIGN_ARRAY(n, softstart),	\
		.profiles = {						\
			[SSD16XX_PROFILE_FULL] =			\
				SSD16XX_PROFILE_PTR(DT_CHILD(n, full)),	\
			[SSD16XX_PROFILE_PARTIAL] =			\
				SSD16XX_PROFILE_PTR(DT_CHILD(n, partial)),\
		},							\
	};								\
									\
	static struct ssd16xx_data ssd16xx_data_ ## n;			\
									\
	DEVICE_DT_DEFINE(n,						\
			 ssd16xx_init, NULL,				\
			 &ssd16xx_data_ ## n,				\
			 &ssd16xx_cfg_ ## n,				\
			 POST_KERNEL,					\
			 CONFIG_DISPLAY_INIT_PRIORITY,			\
			 &ssd16xx_driver_api)

DT_FOREACH_STATUS_OKAY_VARGS(solomon_ssd1608, SSD16XX_DEFINE,
			     &quirks_solomon_ssd1608);
DT_FOREACH_STATUS_OKAY_VARGS(solomon_ssd1673, SSD16XX_DEFINE,
			     &quirks_solomon_ssd1673);
DT_FOREACH_STATUS_OKAY_VARGS(solomon_ssd1675a, SSD16XX_DEFINE,
			     &quirks_solomon_ssd1675a);
DT_FOREACH_STATUS_OKAY_VARGS(solomon_ssd1680, SSD16XX_DEFINE,
			     &quirks_solomon_ssd1680);
DT_FOREACH_STATUS_OKAY_VARGS(solomon_ssd1681, SSD16XX_DEFINE,
			     &quirks_solomon_ssd1681);
