/*
 * Copyright (c) 2025 Siratul Islam <email@sirat.me>
 * SPDX-License-Identifier: Apache-2.0
 *
 * Driver for 32x16 monochrome P10 LED panels with HUB12 interface.
 */

#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/display.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/spi.h>
#include <zephyr/logging/log.h>
#include <zephyr/sys/util.h>
#include <string.h>

LOG_MODULE_REGISTER(hub12, CONFIG_DISPLAY_LOG_LEVEL);

#define DT_DRV_COMPAT zephyr_hub12

/* Single panel constants */
#define HUB12_PANEL_WIDTH     32
#define HUB12_PANEL_HEIGHT    16
#define HUB12_ROWS            4
#define HUB12_BYTES_PER_PANEL 16
#define HUB12_GROUP_SIZE      4
#define HUB12_NUM_GROUPS      4
#define HUB12_PIXELS_PER_BYTE 8

/* Macros for cache and row calculations */
#define HUB12_BYTES_PER_ROW(width) (((width) / HUB12_PANEL_WIDTH) * HUB12_BYTES_PER_PANEL)
#define HUB12_CACHE_SIZE(width)    (HUB12_ROWS * HUB12_BYTES_PER_ROW(width))

/* Brightness control parameters */
#define HUB12_DEFAULT_BRIGHTNESS 5
#define HUB12_MIN_BRIGHTNESS     1
#define HUB12_MAX_BRIGHTNESS     50

struct hub12_config {
	struct gpio_dt_spec pa;
	struct gpio_dt_spec pb;
	struct gpio_dt_spec pe;
	struct gpio_dt_spec plat;
	struct spi_dt_spec spi;
	uint16_t width;
	uint16_t height;
	uint16_t bytes_per_row;
	uint8_t num_panels;
};

struct hub12_data {
	uint8_t *framebuffer;
	uint8_t *cache;
	uint8_t current_row;
	struct k_timer scan_timer;
	struct k_work scan_work;
	struct k_sem lock;
	const struct device *dev;
	uint8_t brightness_us;
};

static void hub12_update_cache(struct hub12_data *data, const struct hub12_config *config,
			       uint8_t row)
{
	const uint8_t *fb = data->framebuffer;
	uint8_t *cache_row = data->cache + (row * config->bytes_per_row);

	for (uint8_t panel = 0; panel < config->num_panels; panel++) {
		for (int i = 0; i < HUB12_BYTES_PER_PANEL; i++) {
			/* Determine byte position within 4-byte groups */
			int group = i / HUB12_GROUP_SIZE;
			int offset = i % HUB12_GROUP_SIZE;
			int reverse_offset = (HUB12_GROUP_SIZE - 1) - offset;

			/* Calculate which framebuffer byte maps to this cache position */
			int bytes_per_group_row = config->num_panels * HUB12_NUM_GROUPS;
			int offset_base = reverse_offset * bytes_per_group_row * HUB12_ROWS;
			int row_offset = row * bytes_per_group_row;
			int panel_offset = panel * HUB12_NUM_GROUPS;
			int fb_idx = offset_base + row_offset + panel_offset + group;

			cache_row[panel * HUB12_BYTES_PER_PANEL + i] = ~fb[fb_idx];
		}
	}
}

static void hub12_scan_row(struct hub12_data *data, const struct hub12_config *config)
{
	uint8_t row = data->current_row;
	uint8_t *cache_row = data->cache + (row * config->bytes_per_row);
	int ret;

	struct spi_buf tx_buf = {.buf = cache_row, .len = config->bytes_per_row};
	struct spi_buf_set tx = {.buffers = &tx_buf, .count = 1};

	ret = spi_write_dt(&config->spi, &tx);
	if (ret < 0) {
		LOG_ERR("SPI write failed: %d", ret);
		return;
	}

	gpio_pin_set_dt(&config->pe, 0);

	gpio_pin_set_dt(&config->plat, 1);
	k_busy_wait(1);
	gpio_pin_set_dt(&config->plat, 0);

	gpio_pin_set_dt(&config->pa, (row & BIT(0)) ? 1 : 0);
	gpio_pin_set_dt(&config->pb, (row & BIT(1)) ? 1 : 0);

	if (data->brightness_us > 0) {
		gpio_pin_set_dt(&config->pe, 1);
		k_busy_wait(data->brightness_us);
		gpio_pin_set_dt(&config->pe, 0);
	}

	data->current_row = (data->current_row + 1) % HUB12_ROWS;
}

static void hub12_scan_work_handler(struct k_work *work)
{
	struct hub12_data *data = CONTAINER_OF(work, struct hub12_data, scan_work);
	const struct hub12_config *config = data->dev->config;

	/* K_NO_WAIT to avoid blocking work queue */
	if (k_sem_take(&data->lock, K_NO_WAIT) == 0) {
		hub12_scan_row(data, config);

		uint8_t next_row = data->current_row;

		hub12_update_cache(data, config, next_row);

		k_sem_give(&data->lock);
	}
}

static void hub12_scan_timer_handler(struct k_timer *timer)
{
	struct hub12_data *data = CONTAINER_OF(timer, struct hub12_data, scan_timer);

	k_work_submit(&data->scan_work);
}

static int hub12_write(const struct device *dev, const uint16_t x, const uint16_t y,
		       const struct display_buffer_descriptor *desc, const void *buf)
{
	struct hub12_data *data = dev->data;
	const struct hub12_config *config = dev->config;
	const uint8_t *src = buf;
	size_t fb_size = config->width * config->height / HUB12_PIXELS_PER_BYTE;

	if (x >= config->width || y >= config->height) {
		return -EINVAL;
	}

	if ((x + desc->width) > config->width || (y + desc->height) > config->height) {
		return -EINVAL;
	}

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

	if (desc->buf_size < (desc->width * desc->height / HUB12_PIXELS_PER_BYTE)) {
		LOG_ERR("Buffer too small");
		return -EINVAL;
	}

	k_sem_take(&data->lock, K_FOREVER);

	if (x == 0 && y == 0 && desc->width == config->width && desc->height == config->height) {
		memcpy(data->framebuffer, src, fb_size);
	} else {
		/* Partial update */
		size_t src_pitch_bytes = desc->pitch / HUB12_PIXELS_PER_BYTE;
		size_t dest_pitch_bytes = config->width / HUB12_PIXELS_PER_BYTE;

		for (uint16_t j = 0; j < desc->height; j++) {
			uint16_t dest_y = y + j;

			for (uint16_t i = 0; i < desc->width; i++) {
				uint16_t dest_x = x + i;
				size_t src_byte_idx =
					(j * src_pitch_bytes) + (i / HUB12_PIXELS_PER_BYTE);
				uint8_t src_bit_mask = BIT(7 - (i % HUB12_PIXELS_PER_BYTE));
				bool bit_is_set = (src[src_byte_idx] & src_bit_mask);

				size_t dest_byte_idx = (dest_y * dest_pitch_bytes) +
						       (dest_x / HUB12_PIXELS_PER_BYTE);
				uint8_t dest_bit_mask = BIT(7 - (dest_x % HUB12_PIXELS_PER_BYTE));

				if (bit_is_set) {
					data->framebuffer[dest_byte_idx] |= dest_bit_mask;
				} else {
					data->framebuffer[dest_byte_idx] &= ~dest_bit_mask;
				}
			}
		}
	}

	for (int i = 0; i < HUB12_ROWS; i++) {
		hub12_update_cache(data, config, i);
	}

	k_sem_give(&data->lock);

	return 0;
}

static int hub12_read(const struct device *dev, const uint16_t x, const uint16_t y,
		      const struct display_buffer_descriptor *desc, void *buf)
{
	return -ENOTSUP;
}

static void *hub12_get_framebuffer(const struct device *dev)
{
	struct hub12_data *data = dev->data;

	return data->framebuffer;
}

static int hub12_blanking_off(const struct device *dev)
{
	return 0;
}

static int hub12_blanking_on(const struct device *dev)
{
	return 0;
}

static int hub12_set_brightness(const struct device *dev, const uint8_t brightness)
{
	struct hub12_data *data = dev->data;

	if (brightness == 0) {
		data->brightness_us = 0;
	} else {
		uint32_t range = HUB12_MAX_BRIGHTNESS - HUB12_MIN_BRIGHTNESS;

		data->brightness_us = HUB12_MIN_BRIGHTNESS + (uint8_t)((brightness * range) / 255U);
	}

	LOG_INF("Brightness set to %u us", data->brightness_us);

	return 0;
}

static int hub12_set_contrast(const struct device *dev, const uint8_t contrast)
{
	return -ENOTSUP;
}

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

	memset(caps, 0, sizeof(*caps));
	caps->x_resolution = config->width;
	caps->y_resolution = config->height;
	caps->supported_pixel_formats = PIXEL_FORMAT_MONO01;
	caps->current_pixel_format = PIXEL_FORMAT_MONO01;
	caps->screen_info = SCREEN_INFO_MONO_MSB_FIRST;
}

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

	return -ENOTSUP;
}

static int hub12_set_orientation(const struct device *dev,
				 const enum display_orientation orientation)
{
	if (orientation == DISPLAY_ORIENTATION_NORMAL) {
		return 0;
	}

	return -ENOTSUP;
}

static const struct display_driver_api hub12_api = {
	.blanking_on = hub12_blanking_on,
	.blanking_off = hub12_blanking_off,
	.write = hub12_write,
	.read = hub12_read,
	.get_framebuffer = hub12_get_framebuffer,
	.set_brightness = hub12_set_brightness,
	.set_contrast = hub12_set_contrast,
	.get_capabilities = hub12_get_capabilities,
	.set_pixel_format = hub12_set_pixel_format,
	.set_orientation = hub12_set_orientation,
};

static int hub12_init(const struct device *dev)
{
	struct hub12_data *data = dev->data;
	const struct hub12_config *config = dev->config;
	int ret;

	data->dev = dev;

	if (!gpio_is_ready_dt(&config->pa) || !gpio_is_ready_dt(&config->pb) ||
	    !gpio_is_ready_dt(&config->pe) || !gpio_is_ready_dt(&config->plat)) {
		LOG_ERR("GPIO devices not ready");
		return -ENODEV;
	}

	ret = gpio_pin_configure_dt(&config->pa, GPIO_OUTPUT_INACTIVE);
	if (ret < 0) {
		return ret;
	}

	ret = gpio_pin_configure_dt(&config->pb, GPIO_OUTPUT_INACTIVE);
	if (ret < 0) {
		return ret;
	}

	ret = gpio_pin_configure_dt(&config->pe, GPIO_OUTPUT_INACTIVE);
	if (ret < 0) {
		return ret;
	}

	ret = gpio_pin_configure_dt(&config->plat, GPIO_OUTPUT_INACTIVE);
	if (ret < 0) {
		return ret;
	}

	if (!spi_is_ready_dt(&config->spi)) {
		LOG_ERR("SPI device not ready");
		return -ENODEV;
	}

	memset(data->framebuffer, 0, (config->width * config->height) / HUB12_PIXELS_PER_BYTE);
	memset(data->cache, 0, HUB12_ROWS * config->bytes_per_row);
	data->current_row = 0;
	data->brightness_us = HUB12_DEFAULT_BRIGHTNESS;

	ret = k_sem_init(&data->lock, 1, 1);
	if (ret < 0) {
		LOG_ERR("Failed to initialize semaphore");
		return ret;
	}

	for (int i = 0; i < HUB12_ROWS; i++) {
		hub12_update_cache(data, config, i);
	}

	k_work_init(&data->scan_work, hub12_scan_work_handler);
	k_timer_init(&data->scan_timer, hub12_scan_timer_handler, NULL);
	k_timer_start(&data->scan_timer, K_MSEC(1), K_MSEC(1));

	LOG_INF("HUB12 display initialized: %dx%d (%d panels)", config->width, config->height,
		config->num_panels);

	return 0;
}

#define HUB12_INIT(inst)                                                                           \
	BUILD_ASSERT((DT_INST_PROP(inst, width) % HUB12_PANEL_WIDTH) == 0,                         \
		     "HUB12 width must be a multiple of " STRINGIFY(HUB12_PANEL_WIDTH));           \
                                                                                                   \
	static uint8_t hub12_framebuffer_##inst[(DT_INST_PROP(inst, width) *                       \
						 DT_INST_PROP(inst, height)) /                     \
						HUB12_PIXELS_PER_BYTE];                            \
	static uint8_t hub12_cache_##inst[HUB12_CACHE_SIZE(DT_INST_PROP(inst, width))];            \
	static struct hub12_data hub12_data_##inst = {                                             \
		.framebuffer = hub12_framebuffer_##inst,                                           \
		.cache = hub12_cache_##inst,                                                       \
	};                                                                                         \
                                                                                                   \
	static const struct hub12_config hub12_config_##inst = {                                   \
		.pa = GPIO_DT_SPEC_INST_GET(inst, pa_gpios),                                       \
		.pb = GPIO_DT_SPEC_INST_GET(inst, pb_gpios),                                       \
		.pe = GPIO_DT_SPEC_INST_GET(inst, pe_gpios),                                       \
		.plat = GPIO_DT_SPEC_INST_GET(inst, plat_gpios),                                   \
		.spi = SPI_DT_SPEC_INST_GET(inst, SPI_OP_MODE_MASTER | SPI_WORD_SET(8)),           \
		.width = DT_INST_PROP(inst, width),                                                \
		.height = DT_INST_PROP(inst, height),                                              \
		.num_panels = DT_INST_PROP(inst, width) / HUB12_PANEL_WIDTH,                       \
		.bytes_per_row = HUB12_BYTES_PER_ROW(DT_INST_PROP(inst, width)),                   \
	};                                                                                         \
                                                                                                   \
	DEVICE_DT_INST_DEFINE(inst, hub12_init, NULL, &hub12_data_##inst, &hub12_config_##inst,    \
			      POST_KERNEL, CONFIG_DISPLAY_INIT_PRIORITY, &hub12_api);

DT_INST_FOREACH_STATUS_OKAY(HUB12_INIT)
