/*
 * Copyright (c) 2017 Intel Corporation
 * Copyright (c) 2021, Nordic Semiconductor ASA
 *
 * SPDX-License-Identifier: Apache-2.0
 */

/*
 * This tool uses display controller driver API and requires
 * a suitable LED matrix controller driver.
 */

#include <zephyr/kernel.h>
#include <zephyr/init.h>
#include <string.h>
#include <zephyr/sys/printk.h>

#include <zephyr/display/mb_display.h>
#include <zephyr/drivers/display.h>

#include "mb_font.h"

#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(mb_disp, CONFIG_DISPLAY_LOG_LEVEL);

#define MODE_MASK    BIT_MASK(16)

#define SCROLL_OFF   0
#define SCROLL_START 1
#define SCROLL_DEFAULT_DURATION_MS 80

#define MB_DISP_XRES 5
#define MB_DISP_YRES 5

struct mb_display {
	const struct device *lm_dev;   /* LED matrix display device */

	struct k_work_delayable dwork; /* Delayable work item */

	uint8_t         img_count;     /* Image count */

	uint8_t         cur_img;       /* Current image or character to show */

	uint8_t         scroll:3,      /* Scroll shift */
			first:1,       /* First frame of a scroll sequence */
			loop:1,        /* Loop to beginning */
			text:1,        /* We're showing a string (not image) */
			img_sep:1,     /* One column image separation */
			msb:1;         /* MSB represents the first pixel */

	int32_t         duration;      /* Duration for each shown image */

	union {
		const struct mb_image *img; /* Array of images to show */
		const char            *str; /* String to be shown */
	};

	/* Buffer for printed strings */
	char            str_buf[CONFIG_MICROBIT_DISPLAY_STR_MAX];
};

static inline const struct mb_image *get_font(char ch)
{
	if (ch < MB_FONT_START || ch > MB_FONT_END) {
		return &mb_font[' ' - MB_FONT_START];
	}

	return &mb_font[ch - MB_FONT_START];
}

static ALWAYS_INLINE uint8_t flip_pixels(uint8_t b)
{
	b = (b & 0xf0) >> 4 | (b & 0x0f) << 4;
	b = (b & 0xcc) >> 2 | (b & 0x33) << 2;
	b = (b & 0xaa) >> 1 | (b & 0x55) << 1;

	return b;
}

static int update_content(struct mb_display *disp, const struct mb_image *img)
{
	const struct display_buffer_descriptor buf_desc = {
		.buf_size = sizeof(struct mb_image),
		.width    = MB_DISP_XRES,
		.height   = MB_DISP_YRES,
		.pitch    = 8,
	};
	struct mb_image tmp_img;
	int ret;

	if (disp->msb) {
		for (int i = 0; i < sizeof(struct mb_image); i++) {
			tmp_img.row[i] = flip_pixels(img->row[i]);
		}

		ret = display_write(disp->lm_dev, 0, 0, &buf_desc, &tmp_img);
	} else {
		ret = display_write(disp->lm_dev, 0, 0, &buf_desc, img);
	}

	if (ret < 0) {
		LOG_ERR("Write to display controller failed");
		return ret;
	}

	LOG_DBG("Image duration %d", disp->duration);
	if (disp->duration != SYS_FOREVER_MS) {
		k_work_reschedule(&disp->dwork, K_MSEC(disp->duration));
	}

	return ret;
}

static int start_image(struct mb_display *disp, const struct mb_image *img)
{
	int ret;

	ret = display_blanking_off(disp->lm_dev);
	if (ret < 0) {
		LOG_ERR("Set blanking off failed");
		return ret;
	}

	return update_content(disp, img);
}

static int reset_display(struct mb_display *disp)
{
	int ret;

	disp->str = NULL;
	disp->cur_img = 0U;
	disp->img = NULL;
	disp->img_count = 0U;
	disp->scroll = SCROLL_OFF;

	ret = display_blanking_on(disp->lm_dev);
	if (ret < 0) {
		LOG_ERR("Set blanking on failed");
	}

	return ret;
}

static const struct mb_image *current_img(struct mb_display *disp)
{
	if (disp->scroll && disp->first) {
		return get_font(' ');
	}

	if (disp->text) {
		return get_font(disp->str[disp->cur_img]);
	} else {
		return &disp->img[disp->cur_img];
	}
}

static const struct mb_image *next_img(struct mb_display *disp)
{
	if (disp->text) {
		if (disp->first) {
			return get_font(disp->str[0]);
		} else if (disp->str[disp->cur_img]) {
			return get_font(disp->str[disp->cur_img + 1]);
		} else {
			return get_font(' ');
		}
	} else {
		if (disp->first) {
			return &disp->img[0];
		} else if (disp->cur_img < (disp->img_count - 1)) {
			return &disp->img[disp->cur_img + 1];
		} else {
			return get_font(' ');
		}
	}
}

static inline bool last_frame(struct mb_display *disp)
{
	if (disp->text) {
		return (disp->str[disp->cur_img] == '\0');
	} else {
		return (disp->cur_img >= disp->img_count);
	}
}

static inline uint8_t scroll_steps(struct mb_display *disp)
{
	return MB_DISP_XRES + disp->img_sep;
}

static int update_scroll(struct mb_display *disp)
{
	if (disp->scroll < scroll_steps(disp)) {
		struct mb_image img;

		for (int i = 0; i < MB_DISP_XRES; i++) {
			const struct mb_image *i1 = current_img(disp);
			const struct mb_image *i2 = next_img(disp);

			img.row[i] = ((i1->row[i] >> disp->scroll) |
				      (i2->row[i] << (scroll_steps(disp) -
						      disp->scroll)));
		}

		disp->scroll++;
		return update_content(disp, &img);
	} else {
		if (disp->first) {
			disp->first = 0U;
		} else {
			disp->cur_img++;
		}

		if (last_frame(disp)) {
			if (!disp->loop) {
				return reset_display(disp);
			}

			disp->cur_img = 0U;
			disp->first = 1U;
		}

		disp->scroll = SCROLL_START;
		return update_content(disp, current_img(disp));
	}
}

static int update_image(struct mb_display *disp)
{
	disp->cur_img++;

	if (last_frame(disp)) {
		if (!disp->loop) {
			return reset_display(disp);
		}

		disp->cur_img = 0U;
	}

	return update_content(disp, current_img(disp));
}

static void update_display_work(struct k_work *work)
{
	struct k_work_delayable *dwork = k_work_delayable_from_work(work);
	struct mb_display *disp = CONTAINER_OF(dwork, struct mb_display, dwork);
	int ret;

	if (disp->scroll) {
		ret = update_scroll(disp);
	} else {
		ret = update_image(disp);
	}

	__ASSERT(ret == 0, "Failed to update display");
}

static int start_scroll(struct mb_display *disp, int32_t duration)
{
	/* Divide total duration by number of scrolling steps */
	if (duration) {
		disp->duration = duration / scroll_steps(disp);
	} else {
		disp->duration = SCROLL_DEFAULT_DURATION_MS;
	}

	disp->scroll = SCROLL_START;
	disp->first = 1U;
	disp->cur_img = 0U;
	return start_image(disp, get_font(' '));
}

static int start_single(struct mb_display *disp, int32_t duration)
{
	disp->duration = duration;

	if (disp->text) {
		return start_image(disp, get_font(disp->str[0]));
	} else {
		return start_image(disp, disp->img);
	}
}

void mb_display_stop(struct mb_display *disp)
{
	struct k_work_sync sync;
	int ret;

	k_work_cancel_delayable_sync(&disp->dwork, &sync);
	LOG_DBG("delayable work stopped %p", disp);
	ret = reset_display(disp);
	__ASSERT(ret == 0, "Failed to reset display");
}

void mb_display_image(struct mb_display *disp, uint32_t mode, int32_t duration,
		      const struct mb_image *img, uint8_t img_count)
{
	int ret;

	mb_display_stop(disp);

	__ASSERT(img && img_count > 0, "Invalid parameters");

	disp->text = 0U;
	disp->img_count = img_count;
	disp->img = img;
	disp->img_sep = 0U;
	disp->cur_img = 0U;
	disp->loop = !!(mode & MB_DISPLAY_FLAG_LOOP);

	switch (mode & MODE_MASK) {
	case MB_DISPLAY_MODE_DEFAULT:
	case MB_DISPLAY_MODE_SINGLE:
		ret = start_single(disp, duration);
		__ASSERT(ret == 0, "Failed to start single mode");
		break;
	case MB_DISPLAY_MODE_SCROLL:
		ret = start_scroll(disp, duration);
		__ASSERT(ret == 0, "Failed to start scroll mode");
		break;
	default:
		__ASSERT(0, "Invalid display mode");
	}
}

void mb_display_print(struct mb_display *disp, uint32_t mode,
		      int32_t duration, const char *fmt, ...)
{
	va_list ap;
	int ret;

	mb_display_stop(disp);

	va_start(ap, fmt);
	vsnprintk(disp->str_buf, sizeof(disp->str_buf), fmt, ap);
	va_end(ap);

	if (disp->str_buf[0] == '\0') {
		return;
	}

	disp->str = disp->str_buf;
	disp->text = 1U;
	disp->img_sep = 1U;
	disp->cur_img = 0U;
	disp->loop = !!(mode & MB_DISPLAY_FLAG_LOOP);

	switch (mode & MODE_MASK) {
	case MB_DISPLAY_MODE_DEFAULT:
	case MB_DISPLAY_MODE_SCROLL:
		ret = start_scroll(disp, duration);
		__ASSERT(ret == 0, "Failed to start scroll mode");
		break;
	case MB_DISPLAY_MODE_SINGLE:
		ret = start_single(disp, duration);
		__ASSERT(ret == 0, "Failed to start single mode");
		break;
	default:
		__ASSERT(0, "Invalid display mode");
	}
}

static int mb_display_init(struct mb_display *disp)
{
	struct display_capabilities caps;
	int ret;

	display_get_capabilities(disp->lm_dev, &caps);
	if (caps.x_resolution != MB_DISP_XRES ||
	    caps.y_resolution != MB_DISP_YRES) {
		LOG_ERR("Not supported display resolution");
		return -ENOTSUP;
	}

	if (caps.screen_info & SCREEN_INFO_MONO_MSB_FIRST) {
		disp->msb = 1U;
	}

	ret = display_set_brightness(disp->lm_dev, 0xFF);
	if (ret < 0) {
		LOG_ERR("Failed to set brightness");
		return ret;
	}

	k_work_init_delayable(&disp->dwork, update_display_work);

	return 0;
}

static struct mb_display display;

struct mb_display *mb_display_get(void)
{
	return &display;
}

static int mb_display_init_on_boot(void)
{

	display.lm_dev = DEVICE_DT_GET_ONE(nordic_nrf_led_matrix);
	if (!device_is_ready(display.lm_dev)) {
		LOG_ERR("Display controller device not ready");
		return -ENODEV;
	}

	return mb_display_init(&display);
}

SYS_INIT(mb_display_init_on_boot, APPLICATION, CONFIG_DISPLAY_INIT_PRIORITY);
