/*
 * Copyright (c) 2015 Intel Corporation
 * Copyright (c) 2022 Nordic Semiconductor ASA
 * Copyright (c) 2022-2023 Jamie McCrae
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#define DT_DRV_COMPAT jhd_jhd1313

#include <string.h>
#include <zephyr/device.h>
#include <zephyr/devicetree.h>
#include <zephyr/drivers/i2c.h>
#include <zephyr/drivers/auxdisplay.h>
#include <zephyr/kernel.h>
#include <zephyr/sys/util.h>
#include <zephyr/logging/log.h>

LOG_MODULE_REGISTER(auxdisplay_jhd1313, CONFIG_AUXDISPLAY_LOG_LEVEL);

#define JHD1313_BACKLIGHT_ADDR		(0x62)

/* Defines for the JHD1313_CMD_CURSOR_SHIFT */
#define JHD1313_CS_DISPLAY_SHIFT	(1 << 3)
#define JHD1313_CS_RIGHT_SHIFT		(1 << 2)

/* Defines for the JHD1313_CMD_INPUT_SET to change text direction */
#define JHD1313_IS_INCREMENT		(1 << 1)
#define JHD1313_IS_DECREMENT		(0 << 1)
#define JHD1313_IS_SHIFT		(1 << 0)

/* Defines for the JHD1313_CMD_FUNCTION_SET */
#define JHD1313_FS_8BIT_MODE		(1 << 4)
#define JHD1313_FS_ROWS_2		(1 << 3)
#define JHD1313_FS_ROWS_1		(0 << 3)
#define JHD1313_FS_DOT_SIZE_BIG		(1 << 2)
#define JHD1313_FS_DOT_SIZE_LITTLE	(0 << 2)

/* LCD Display Commands */
#define JHD1313_CMD_SCREEN_CLEAR	(1 << 0)
#define JHD1313_CMD_CURSOR_RETURN	(1 << 1)
#define JHD1313_CMD_INPUT_SET		(1 << 2)
#define JHD1313_CMD_DISPLAY_SWITCH	(1 << 3)
#define JHD1313_CMD_CURSOR_SHIFT	(1 << 4)
#define JHD1313_CMD_FUNCTION_SET	(1 << 5)
#define JHD1313_CMD_SET_CGRAM_ADDR	(1 << 6)
#define JHD1313_CMD_SET_DDRAM_ADDR	(1 << 7)

#define JHD1313_DS_DISPLAY_ON		(1 << 2)
#define JHD1313_DS_CURSOR_ON		(1 << 1)
#define JHD1313_DS_BLINK_ON		(1 << 0)

#define JHD1313_LED_REG_R		0x04
#define JHD1313_LED_REG_G		0x03
#define JHD1313_LED_REG_B		0x02

#define JHD1313_LINE_FIRST		0x80
#define JHD1313_LINE_SECOND		0xC0

#define CLEAR_DELAY_MS			20
#define UPDATE_DELAY_MS			5

struct auxdisplay_jhd1313_data {
	uint8_t input_set;
	bool power;
	bool cursor;
	bool blinking;
	uint8_t function;
	uint8_t backlight;
};

struct auxdisplay_jhd1313_config {
	struct auxdisplay_capabilities capabilities;
	struct i2c_dt_spec bus;
};

static const uint8_t colour_define[][4] = {
	{ 0,   0,   0   },      /* Off */
	{ 255, 255, 255 },	/* White */
	{ 255, 0,   0   },      /* Red */
	{ 0,   255, 0   },      /* Green */
	{ 0,   0,   255 },      /* Blue */
};

static void auxdisplay_jhd1313_reg_set(const struct device *i2c, uint8_t addr, uint8_t data)
{
	uint8_t command[2] = { addr, data };

	i2c_write(i2c, command, sizeof(command), JHD1313_BACKLIGHT_ADDR);
}

static int auxdisplay_jhd1313_print(const struct device *dev, const uint8_t *data, uint16_t size)
{
	const struct auxdisplay_jhd1313_config *config = dev->config;
	uint8_t buf[] = { JHD1313_CMD_SET_CGRAM_ADDR, 0 };
	int rc = 0;
	int16_t i;

	for (i = 0; i < size; i++) {
		buf[1] = data[i];
		rc = i2c_write_dt(&config->bus, buf, sizeof(buf));
	}

	return rc;
}

static int auxdisplay_jhd1313_cursor_position_set(const struct device *dev,
						  enum auxdisplay_position type, int16_t x,
						  int16_t y)
{
	const struct auxdisplay_jhd1313_config *config = dev->config;
	unsigned char data[2];

	if (type != AUXDISPLAY_POSITION_ABSOLUTE) {
		return -EINVAL;
	}

	if (y == 0U) {
		x |= JHD1313_LINE_FIRST;
	} else {
		x |= JHD1313_LINE_SECOND;
	}

	data[0] = JHD1313_CMD_SET_DDRAM_ADDR;
	data[1] = x;

	return i2c_write_dt(&config->bus, data, 2);
}

static int auxdisplay_jhd1313_clear(const struct device *dev)
{
	int rc;
	const struct auxdisplay_jhd1313_config *config = dev->config;
	uint8_t clear[] = { 0, JHD1313_CMD_SCREEN_CLEAR };

	rc = i2c_write_dt(&config->bus, clear, sizeof(clear));
	LOG_DBG("Clear, delay 20 ms");

	k_sleep(K_MSEC(CLEAR_DELAY_MS));

	return rc;
}

static int auxdisplay_jhd1313_update_display_state(
				const struct auxdisplay_jhd1313_config *config,
				struct auxdisplay_jhd1313_data *data)
{
	int rc;
	uint8_t buf[] = { 0, JHD1313_CMD_DISPLAY_SWITCH };

	if (data->power) {
		buf[1] |= JHD1313_DS_DISPLAY_ON;
	}

	if (data->cursor) {
		buf[1] |= JHD1313_DS_CURSOR_ON;
	}

	if (data->blinking) {
		buf[1] |= JHD1313_DS_BLINK_ON;
	}

	rc = i2c_write_dt(&config->bus, buf, sizeof(buf));

	LOG_DBG("Set display_state options, delay 5 ms");
	k_sleep(K_MSEC(UPDATE_DELAY_MS));

	return rc;
}

static int auxdisplay_jhd1313_cursor_set_enabled(const struct device *dev, bool enabled)
{
	const struct auxdisplay_jhd1313_config *config = dev->config;
	struct auxdisplay_jhd1313_data *data = dev->data;

	data->cursor = enabled;
	return auxdisplay_jhd1313_update_display_state(config, data);
}

static int auxdisplay_jhd1313_position_blinking_set_enabled(const struct device *dev, bool enabled)
{
	const struct auxdisplay_jhd1313_config *config = dev->config;
	struct auxdisplay_jhd1313_data *data = dev->data;

	data->blinking = enabled;
	return auxdisplay_jhd1313_update_display_state(config, data);
}

static void auxdisplay_jhd1313_input_state_set(const struct device *dev, uint8_t opt)
{
	const struct auxdisplay_jhd1313_config *config = dev->config;
	struct auxdisplay_jhd1313_data *data = dev->data;
	uint8_t buf[] = { 0, 0 };

	data->input_set = opt;
	buf[1] = (opt | JHD1313_CMD_INPUT_SET);

	i2c_write_dt(&config->bus, buf, sizeof(buf));
	LOG_DBG("Set the input_set, no delay");
}

static int auxdisplay_jhd1313_backlight_set(const struct device *dev, uint8_t colour)
{
	const struct auxdisplay_jhd1313_config *config = dev->config;
	struct auxdisplay_jhd1313_data *data = dev->data;

	if (colour > ARRAY_SIZE(colour_define)) {
		LOG_WRN("Selected colour is too high a value");
		return -EINVAL;
	}

	data->backlight = colour;

	auxdisplay_jhd1313_reg_set(config->bus.bus, JHD1313_LED_REG_R, colour_define[colour][0]);
	auxdisplay_jhd1313_reg_set(config->bus.bus, JHD1313_LED_REG_G, colour_define[colour][1]);
	auxdisplay_jhd1313_reg_set(config->bus.bus, JHD1313_LED_REG_B, colour_define[colour][2]);

	return 0;
}

static int auxdisplay_jhd1313_backlight_get(const struct device *dev, uint8_t *backlight)
{
	struct auxdisplay_jhd1313_data *data = dev->data;

	*backlight = data->backlight;

	return 0;
}

static void auxdisplay_jhd1313_function_set(const struct device *dev, uint8_t opt)
{
	const struct auxdisplay_jhd1313_config *config = dev->config;
	struct auxdisplay_jhd1313_data *data = dev->data;
	uint8_t buf[] = { 0, 0 };

	data->function = opt;
	buf[1] = (opt | JHD1313_CMD_FUNCTION_SET);

	i2c_write_dt(&config->bus, buf, sizeof(buf));

	LOG_DBG("Set function options, delay 5 ms");
	k_sleep(K_MSEC(5));
}

static int auxdisplay_jhd1313_initialize(const struct device *dev)
{
	const struct auxdisplay_jhd1313_config *config = dev->config;
	struct auxdisplay_jhd1313_data *data = dev->data;
	uint8_t cmd;

	LOG_DBG("Initialize called");

	if (!device_is_ready(config->bus.bus)) {
		return -ENODEV;
	}

	/*
	 * Initialization sequence from the data sheet:
	 * 1 - Power on
	 *   - Wait for more than 30 ms AFTER VDD rises to 4.5v
	 * 2 - Send FUNCTION set
	 *   - Wait for 39 us
	 * 3 - Send DISPLAY Control
	 *   - wait for 39 us
	 * 4 - send DISPLAY Clear
	 *   - wait for 1.5 ms
	 * 5 - send ENTRY Mode
	 * 6 - Initialization is done
	 */

	/*
	 * We're here! Let's just make sure we've had enough time for the
	 * VDD to power on, so pause a little here, 30 ms min, so we go 50
	 */
	LOG_DBG("Delay 50 ms while the VDD powers on");
	k_sleep(K_MSEC(50));

	/* Configure everything for the display function first */
	cmd = JHD1313_CMD_FUNCTION_SET | JHD1313_FS_ROWS_2;
	auxdisplay_jhd1313_function_set(dev, cmd);

	/* Turn the display on - by default no cursor and no blinking */
	auxdisplay_jhd1313_update_display_state(config, data);

	/* Clear the screen */
	auxdisplay_jhd1313_clear(dev);

	/*
	 * Initialize to the default text direction for romance languages
	 * (increment, no shift)
	 */
	cmd = JHD1313_IS_INCREMENT;

	auxdisplay_jhd1313_input_state_set(dev, cmd);

	/* Now power on the background RGB control */
	LOG_INF("Configuring the RGB background");
	auxdisplay_jhd1313_reg_set(config->bus.bus, 0x00, 0x00);
	auxdisplay_jhd1313_reg_set(config->bus.bus, 0x01, 0x05);
	auxdisplay_jhd1313_reg_set(config->bus.bus, 0x08, 0xAA);

	/* Now set the background colour to black */
	LOG_DBG("Background set to off");
	auxdisplay_jhd1313_backlight_set(dev, 0);

	return 0;
}

static int auxdisplay_jhd1313_display_on(const struct device *dev)
{
	const struct auxdisplay_jhd1313_config *config = dev->config;
	struct auxdisplay_jhd1313_data *data = dev->data;

	data->power = true;
	return auxdisplay_jhd1313_update_display_state(config, data);
}

static int auxdisplay_jhd1313_display_off(const struct device *dev)
{
	const struct auxdisplay_jhd1313_config *config = dev->config;
	struct auxdisplay_jhd1313_data *data = dev->data;

	data->power = false;
	return auxdisplay_jhd1313_update_display_state(config, data);
}

static int auxdisplay_jhd1313_capabilities_get(const struct device *dev,
					       struct auxdisplay_capabilities *capabilities)
{
	const struct auxdisplay_jhd1313_config *config = dev->config;

	memcpy(capabilities, &config->capabilities, sizeof(struct auxdisplay_capabilities));

	return 0;
}

static const struct auxdisplay_driver_api auxdisplay_jhd1313_auxdisplay_api = {
	.display_on = auxdisplay_jhd1313_display_on,
	.display_off = auxdisplay_jhd1313_display_off,
	.cursor_set_enabled = auxdisplay_jhd1313_cursor_set_enabled,
	.position_blinking_set_enabled = auxdisplay_jhd1313_position_blinking_set_enabled,
	.cursor_position_set = auxdisplay_jhd1313_cursor_position_set,
	.capabilities_get = auxdisplay_jhd1313_capabilities_get,
	.clear = auxdisplay_jhd1313_clear,
	.backlight_get = auxdisplay_jhd1313_backlight_get,
	.backlight_set = auxdisplay_jhd1313_backlight_set,
	.write = auxdisplay_jhd1313_print,
};

#define AUXDISPLAY_JHD1313_DEVICE(inst)								\
	static const struct auxdisplay_jhd1313_config auxdisplay_jhd1313_config_##inst = {	\
		.capabilities = {								\
			.columns = 16,								\
			.rows = 2,								\
			.mode = 0,								\
			.brightness.minimum = AUXDISPLAY_LIGHT_NOT_SUPPORTED,			\
			.brightness.maximum = AUXDISPLAY_LIGHT_NOT_SUPPORTED,			\
			.backlight.minimum = 0,							\
			.backlight.maximum = ARRAY_SIZE(colour_define),				\
			.custom_characters = 0,							\
		},										\
		.bus = I2C_DT_SPEC_INST_GET(inst),						\
	};											\
	static struct auxdisplay_jhd1313_data auxdisplay_jhd1313_data_##inst = {		\
		.power = true,									\
		.cursor = false,								\
		.blinking = false,								\
	};											\
	DEVICE_DT_INST_DEFINE(inst,								\
			&auxdisplay_jhd1313_initialize,						\
			NULL,									\
			&auxdisplay_jhd1313_data_##inst,					\
			&auxdisplay_jhd1313_config_##inst,					\
			POST_KERNEL,								\
			CONFIG_AUXDISPLAY_INIT_PRIORITY,					\
			&auxdisplay_jhd1313_auxdisplay_api);

DT_INST_FOREACH_STATUS_OKAY(AUXDISPLAY_JHD1313_DEVICE)
