/*
 * Copyright (c) 2022 Jimmy Ou <yanagiis@gmail.com>
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#define DT_DRV_COMPAT maxim_max7219

/**
 * @file
 * @brief MAX7219 LED display driver
 *
 * This driver map the segment as x, digit as y.
 *
 * A MAX7219 has 8x8 pixels.
 * Two MAX7219s (with cascading) have 8x16 pixels.
 * So on and so forth.
 *
 * Datasheet: https://datasheets.maximintegrated.com/en/ds/MAX7219-MAX7221.pdf
 *
 * Limitations:
 *  1. This driver only implements no-decode mode.
 */

#include <stddef.h>

#include <zephyr/drivers/display.h>
#include <zephyr/drivers/spi.h>
#include <zephyr/logging/log.h>
#include <zephyr/sys/byteorder.h>
#include <zephyr/sys/util.h>
#include <zephyr/kernel.h>
LOG_MODULE_REGISTER(max7219, CONFIG_DISPLAY_LOG_LEVEL);

#define MAX7219_SEGMENTS_PER_DIGIT 8
#define MAX7219_DIGITS_PER_DEVICE  8

/* clang-format off */

/* MAX7219 registers and fields */
#define MAX7219_REG_NOOP		0x00
#define MAX7219_NOOP			0x00

#define MAX7219_REG_DECODE_MODE		0x09
#define MAX7219_NO_DECODE		0x00

#define MAX7219_REG_INTENSITY		0x0A

#define MAX7219_REG_SCAN_LIMIT		0x0B

#define MAX7219_REG_SHUTDOWN		0x0C
#define MAX7219_SHUTDOWN_MODE		0x00
#define MAX7219_LEAVE_SHUTDOWN_MODE	0x01

#define MAX7219_REG_DISPLAY_TEST	0x0F
#define MAX7219_LEAVE_DISPLAY_TEST_MODE	0x00
#define MAX7219_DISPLAY_TEST_MODE	0x01

/* clang-format on */

struct max7219_config {
	struct spi_dt_spec spi;
	uint32_t num_cascading;
	uint8_t intensity;
	uint8_t scan_limit;
};

struct max7219_data {
	/* Keep all digit_buf for all cascading MAX7219 */
	uint8_t *digit_buf;
	uint8_t *tx_buf;
};

static int max7219_transmit_all(const struct device *dev, const uint8_t addr, const uint8_t value)
{
	const struct max7219_config *dev_config = dev->config;
	struct max7219_data *dev_data = dev->data;

	const struct spi_buf tx_buf = {
		.buf = dev_data->tx_buf,
		.len = dev_config->num_cascading * 2,
	};
	const struct spi_buf_set tx_bufs = {
		.buffers = &tx_buf,
		.count = 1U,
	};

	for (int i = 0; i < dev_config->num_cascading; i++) {
		dev_data->tx_buf[i * 2] = addr;
		dev_data->tx_buf[i * 2 + 1] = value;
	}

	return spi_write_dt(&dev_config->spi, &tx_bufs);
}

static int max7219_transmit_one(const struct device *dev, const uint8_t max7219_idx,
				const uint8_t addr, const uint8_t value)
{
	const struct max7219_config *dev_config = dev->config;
	struct max7219_data *dev_data = dev->data;

	const struct spi_buf tx_buf = {
		.buf = dev_data->tx_buf,
		.len = dev_config->num_cascading * 2,
	};
	const struct spi_buf_set tx_bufs = {
		.buffers = &tx_buf,
		.count = 1U,
	};

	for (int i = 0; i < dev_config->num_cascading; i++) {
		if (i != (dev_config->num_cascading - 1 - max7219_idx)) {
			dev_data->tx_buf[i * 2] = MAX7219_REG_NOOP;
			dev_data->tx_buf[i * 2 + 1] = MAX7219_NOOP;
			continue;
		}

		dev_data->tx_buf[i * 2] = addr;
		dev_data->tx_buf[i * 2 + 1] = value;
	}

	return spi_write_dt(&dev_config->spi, &tx_bufs);
}

static inline uint8_t next_pixel(uint8_t *mask, uint8_t *data, const uint8_t **buf)
{
	*mask <<= 1;
	if (!*mask) {
		*mask = 0x01;
		*data = *(*buf)++;
	}
	return *data & *mask;
}

static inline void skip_pixel(uint8_t *mask, uint8_t *data, const uint8_t **buf, uint16_t count)
{
	while (count--) {
		next_pixel(mask, data, buf);
	}
}

static int max7219_write(const struct device *dev, const uint16_t x, const uint16_t y,
			 const struct display_buffer_descriptor *desc, const void *buf)
{
	const struct max7219_config *dev_config = dev->config;
	struct max7219_data *dev_data = dev->data;

	const uint16_t max_width = MAX7219_SEGMENTS_PER_DIGIT;
	const uint16_t max_height = dev_config->num_cascading * MAX7219_DIGITS_PER_DEVICE;

	/*
	 * MAX7219 only supports PIXEL_FORMAT_MONO01. 1 bit stands for 1 pixel.
	 */
	__ASSERT((desc->pitch * desc->height) <= (desc->buf_size * 8U), "Input buffer too small");
	__ASSERT(desc->width <= desc->pitch, "Pitch is smaller than width");
	__ASSERT(desc->pitch <= max_width, "Pitch in descriptor is larger than screen size");
	__ASSERT(desc->height <= max_height, "Height in descriptor is larger than screen size");
	__ASSERT(x + desc->pitch <= max_width,
		 "Writing outside screen boundaries in horizontal direction");
	__ASSERT(y + desc->height <= max_height,
		 "Writing outside screen boundaries in vertical direction");

	if (desc->width > desc->pitch || (desc->pitch * desc->height) > (desc->buf_size * 8U)) {
		return -EINVAL;
	}

	if ((x + desc->pitch) > max_width || (y + desc->height) > max_height) {
		return -EINVAL;
	}

	const uint16_t end_x = x + desc->width;
	const uint16_t end_y = y + desc->height;
	const uint8_t *byte_buf = buf;
	const uint16_t to_skip = desc->pitch - desc->width;
	uint8_t mask = 0;
	uint8_t data = 0;

	for (uint16_t py = y; py < end_y; ++py) {
		const uint8_t max7219_idx = py / MAX7219_DIGITS_PER_DEVICE;
		const uint8_t digit_idx = py % MAX7219_DIGITS_PER_DEVICE;
		uint8_t segment = dev_data->digit_buf[py];
		int ret;

		for (uint16_t px = x; px < end_x; ++px) {
			WRITE_BIT(segment, px, next_pixel(&mask, &data, &byte_buf));
		}

		skip_pixel(&mask, &data, &byte_buf, to_skip);

		/* led register address begins from 1 */
		ret = max7219_transmit_one(dev, max7219_idx, digit_idx + 1, segment);
		if (ret < 0) {
			return ret;
		}

		dev_data->digit_buf[y] = segment;
	}

	return 0;
}

static int max7219_set_brightness(const struct device *dev, const uint8_t brightness)
{
	int ret;

	/*
	 * max7219 supports intensity value from 0x0 to 0xF.
	 * map the brightness from [0, 255] to [0, 15]
	 */
	ret = max7219_transmit_all(dev, MAX7219_REG_INTENSITY, brightness >> 4);
	if (ret < 0) {
		LOG_ERR("Failed to set brightness");
		return ret;
	}

	return 0;
}

static int max7219_set_pixel_format(const struct device *dev,
				    const enum display_pixel_format format)
{
	ARG_UNUSED(dev);

	switch (format) {
	case PIXEL_FORMAT_MONO01:
		return 0;
	default:
		return -ENOTSUP;
	}
}

static int max7219_set_orientation(const struct device *dev,
				   const enum display_orientation orientation)
{
	ARG_UNUSED(dev);

	switch (orientation) {
	case DISPLAY_ORIENTATION_NORMAL:
		return 0;
	default:
		return -ENOTSUP;
	}
}

static void max7219_get_capabilities(const struct device *dev, struct display_capabilities *caps)
{
	const struct max7219_config *dev_config = dev->config;

	caps->x_resolution = MAX7219_SEGMENTS_PER_DIGIT;
	caps->y_resolution = MAX7219_DIGITS_PER_DEVICE * dev_config->num_cascading;
	caps->supported_pixel_formats = PIXEL_FORMAT_MONO01;
	caps->screen_info = 0;
	caps->current_pixel_format = PIXEL_FORMAT_MONO01;
	caps->current_orientation = DISPLAY_ORIENTATION_NORMAL;
}

static DEVICE_API(display, max7219_api) = {
	.write = max7219_write,
	.set_brightness = max7219_set_brightness,
	.get_capabilities = max7219_get_capabilities,
	.set_pixel_format = max7219_set_pixel_format,
	.set_orientation = max7219_set_orientation,
};

static int max7219_init(const struct device *dev)
{
	const struct max7219_config *dev_config = dev->config;
	struct max7219_data *dev_data = dev->data;
	int ret;

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

	/* turn off all leds */
	memset(dev_data->digit_buf, 0,
	       dev_config->num_cascading * MAX7219_DIGITS_PER_DEVICE * sizeof(uint8_t));

	ret = max7219_transmit_all(dev, MAX7219_REG_DISPLAY_TEST, MAX7219_LEAVE_DISPLAY_TEST_MODE);
	if (ret < 0) {
		LOG_ERR("Failed to disable display test");
		return ret;
	}

	ret = max7219_transmit_all(dev, MAX7219_REG_DECODE_MODE, MAX7219_NO_DECODE);
	if (ret < 0) {
		LOG_ERR("Failed to set decode mode");
		return ret;
	}

	ret = max7219_transmit_all(dev, MAX7219_REG_INTENSITY, dev_config->intensity);
	if (ret < 0) {
		LOG_ERR("Failed to set global brightness");
		return ret;
	}

	ret = max7219_transmit_all(dev, MAX7219_REG_SCAN_LIMIT, dev_config->scan_limit);
	if (ret < 0) {
		LOG_ERR("Failed to set scan limit");
		return ret;
	}

	ret = max7219_transmit_all(dev, MAX7219_REG_SHUTDOWN, MAX7219_LEAVE_SHUTDOWN_MODE);
	if (ret < 0) {
		LOG_ERR("Failed to leave shutdown state");
		return ret;
	}

	const struct display_buffer_descriptor desc = {
		.buf_size = dev_config->num_cascading * MAX7219_DIGITS_PER_DEVICE,
		.height = dev_config->num_cascading * MAX7219_DIGITS_PER_DEVICE,
		.width = MAX7219_DIGITS_PER_DEVICE,
		.pitch = MAX7219_DIGITS_PER_DEVICE,
	};

	ret = max7219_write(dev, 0, 0, &desc, dev_data->digit_buf);
	if (ret < 0) {
		return ret;
	}

	return 0;
}

#define DISPLAY_MAX7219_INIT(n)                                                \
	static uint8_t max7219_digit_data_##n[DT_INST_PROP(n, num_cascading) * \
					      MAX7219_DIGITS_PER_DEVICE];      \
	static uint8_t max7219_tx_buf##n[DT_INST_PROP(n, num_cascading) * 2];  \
	static struct max7219_data max7219_data_##n = {                        \
		.digit_buf = max7219_digit_data_##n,                           \
		.tx_buf = max7219_tx_buf##n,                                   \
	};                                                                     \
	static const struct max7219_config max7219_config_##n = {              \
		.spi = SPI_DT_SPEC_INST_GET(                                   \
			n, SPI_OP_MODE_MASTER | SPI_WORD_SET(8U)),             \
		.num_cascading = DT_INST_PROP(n, num_cascading),               \
		.intensity = DT_INST_PROP(n, intensity),                       \
		.scan_limit = DT_INST_PROP(n, scan_limit),                     \
	};                                                                     \
	DEVICE_DT_INST_DEFINE(n, max7219_init, NULL, &max7219_data_##n,        \
			      &max7219_config_##n, POST_KERNEL,                \
			      CONFIG_DISPLAY_INIT_PRIORITY, &max7219_api);

DT_INST_FOREACH_STATUS_OKAY(DISPLAY_MAX7219_INIT)
