/*
 * Copyright (c) 2024 tinyVision.ai Inc.
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#define DT_DRV_COMPAT zephyr_video_emul_imager

#include <string.h>

#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/sys/util.h>
#include <zephyr/sys/byteorder.h>
#include <zephyr/drivers/video.h>
#include <zephyr/drivers/video-controls.h>
#include <zephyr/drivers/i2c.h>
#include <zephyr/logging/log.h>

LOG_MODULE_REGISTER(video_emul_imager, CONFIG_VIDEO_LOG_LEVEL);

#define EMUL_IMAGER_REG_SENSOR_ID 0x0000
#define EMUL_IMAGER_SENSOR_ID     0x99
#define EMUL_IMAGER_REG_CTRL      0x0001
#define EMUL_IMAGER_REG_INIT1     0x0002
#define EMUL_IMAGER_REG_INIT2     0x0003
#define EMUL_IMAGER_REG_TIMING1   0x0004
#define EMUL_IMAGER_REG_TIMING2   0x0005
#define EMUL_IMAGER_REG_TIMING3   0x0006
#define EMUL_IMAGER_REG_EXPOSURE  0x0007
#define EMUL_IMAGER_REG_GAIN      0x0008
#define EMUL_IMAGER_REG_PATTERN   0x0009
#define EMUL_IMAGER_PATTERN_OFF   0x00
#define EMUL_IMAGER_PATTERN_BARS1 0x01
#define EMUL_IMAGER_PATTERN_BARS2 0x02

/* Emulated register bank */
uint8_t emul_imager_fake_regs[10];

enum emul_imager_fmt_id {
	RGB565_64x20,
	YUYV_64x20,
};

struct emul_imager_reg {
	uint16_t addr;
	uint8_t value;
};

struct emul_imager_mode {
	uint8_t fps;
	/* List of registers lists to configure the various properties of the sensor.
	 * This permits to deduplicate the list of registers in case some lare sections
	 * are repeated across modes, such as the resolution for different FPS.
	 */
	const struct emul_imager_reg *regs[2];
	/* More fields can be added according to the needs of the sensor driver */
};

struct emul_imager_config {
	struct i2c_dt_spec i2c;
};

struct emul_imager_data {
	/* First field is a framebuffer for I/O emulation purpose */
	uint8_t framebuffer[CONFIG_VIDEO_EMUL_IMAGER_FRAMEBUFFER_SIZE];
	/* Other fields are shared with real hardware drivers */
	const struct emul_imager_mode *mode;
	enum emul_imager_fmt_id fmt_id;
	struct video_format fmt;
};

/* Initial parameters of the sensors common to all modes. */
static const struct emul_imager_reg emul_imager_init_regs[] = {
	{EMUL_IMAGER_REG_CTRL, 0x00},
	/* Example comment about REG_INIT1 */
	{EMUL_IMAGER_REG_INIT1, 0x10},
	{EMUL_IMAGER_REG_INIT2, 0x00},
	{0},
};

/* List of registers aggregated together in "modes" that can be applied
 * to set the timing parameters and other mode-dependent configuration.
 */

static const struct emul_imager_reg emul_imager_rgb565_64x20[] = {
	{EMUL_IMAGER_REG_TIMING1, 0x64},
	{EMUL_IMAGER_REG_TIMING2, 0x20},
	{0},
};
static const struct emul_imager_reg emul_imager_rgb565_64x20_15fps[] = {
	{EMUL_IMAGER_REG_TIMING3, 15},
	{0},
};
static const struct emul_imager_reg emul_imager_rgb565_64x20_30fps[] = {
	{EMUL_IMAGER_REG_TIMING3, 30},
	{0},
};
static const struct emul_imager_reg emul_imager_rgb565_64x20_60fps[] = {
	{EMUL_IMAGER_REG_TIMING3, 60},
	{0},
};
struct emul_imager_mode emul_imager_rgb565_64x20_modes[] = {
	{.fps = 15, .regs = {emul_imager_rgb565_64x20, emul_imager_rgb565_64x20_15fps}},
	{.fps = 30, .regs = {emul_imager_rgb565_64x20, emul_imager_rgb565_64x20_30fps}},
	{.fps = 60, .regs = {emul_imager_rgb565_64x20, emul_imager_rgb565_64x20_60fps}},
	{0},
};

static const struct emul_imager_reg emul_imager_yuyv_64x20[] = {
	{EMUL_IMAGER_REG_TIMING1, 0x64},
	{EMUL_IMAGER_REG_TIMING2, 0x20},
	{0},
};
static const struct emul_imager_reg emul_imager_yuyv_64x20_15fps[] = {
	{EMUL_IMAGER_REG_TIMING3, 15},
	{0},
};
static const struct emul_imager_reg emul_imager_yuyv_64x20_30fps[] = {
	{EMUL_IMAGER_REG_TIMING3, 30},
	{0},
};
struct emul_imager_mode emul_imager_yuyv_64x20_modes[] = {
	{.fps = 15, .regs = {emul_imager_yuyv_64x20, emul_imager_yuyv_64x20_15fps}},
	{.fps = 30, .regs = {emul_imager_yuyv_64x20, emul_imager_yuyv_64x20_30fps}},
	{0},
};

/* Summary of all the modes of all the frame formats, with the format ID as
 * index, matching fmts[].
 */
static const struct emul_imager_mode *emul_imager_modes[] = {
	[RGB565_64x20] = emul_imager_rgb565_64x20_modes,
	[YUYV_64x20] = emul_imager_yuyv_64x20_modes,
};

/* Video device capabilities where the supported resolutions and pixel formats are listed.
 * The format ID is used as index to fetch the matching mode from the list above.
 */
#define EMUL_IMAGER_VIDEO_FORMAT_CAP(width, height, format)                                        \
	{                                                                                          \
		.pixelformat = (format),                                                           \
		.width_min = (width),                                                              \
		.width_max = (width),                                                              \
		.height_min = (height),                                                            \
		.height_max = (height),                                                            \
		.width_step = 0,                                                                   \
		.height_step = 0,                                                                  \
	}
static const struct video_format_cap fmts[] = {
	[RGB565_64x20] = EMUL_IMAGER_VIDEO_FORMAT_CAP(64, 20, VIDEO_PIX_FMT_RGB565),
	[YUYV_64x20] = EMUL_IMAGER_VIDEO_FORMAT_CAP(64, 20, VIDEO_PIX_FMT_YUYV),
	{0},
};

/* Emulated I2C register interface, to replace with actual I2C calls for real hardware */
static int emul_imager_read_reg(const struct device *const dev, uint8_t reg_addr, uint8_t *value)
{
	LOG_DBG("%s placeholder for I2C read from 0x%02x", dev->name, reg_addr);
	switch (reg_addr) {
	case EMUL_IMAGER_REG_SENSOR_ID:
		*value = EMUL_IMAGER_SENSOR_ID;
		break;
	default:
		*value = emul_imager_fake_regs[reg_addr];
	}
	return 0;
}

/* Helper to read a full integer directly from a register */
static int emul_imager_read_int(const struct device *const dev, uint8_t reg_addr, int *value)
{
	uint8_t val8;
	int ret;

	ret = emul_imager_read_reg(dev, reg_addr, &val8);
	*value = val8;
	return ret;
}

/* Some sensors will need reg8 or reg16 variants. */
static int emul_imager_write_reg(const struct device *const dev, uint8_t reg_addr, uint8_t value)
{
	LOG_DBG("%s placeholder for I2C write 0x%08x to 0x%02x", dev->name, value, reg_addr);
	emul_imager_fake_regs[reg_addr] = value;
	return 0;
}

static int emul_imager_write_multi(const struct device *const dev,
				   const struct emul_imager_reg *regs)
{
	int ret;

	for (int i = 0; regs[i].addr != 0; i++) {
		ret = emul_imager_write_reg(dev, regs[i].addr, regs[i].value);
		if (ret < 0) {
			return ret;
		}
	}
	return 0;
}

static int emul_imager_set_ctrl(const struct device *dev, unsigned int cid, void *value)
{
	switch (cid) {
	case VIDEO_CID_EXPOSURE:
		return emul_imager_write_reg(dev, EMUL_IMAGER_REG_EXPOSURE, (int)value);
	case VIDEO_CID_GAIN:
		return emul_imager_write_reg(dev, EMUL_IMAGER_REG_GAIN, (int)value);
	case VIDEO_CID_TEST_PATTERN:
		return emul_imager_write_reg(dev, EMUL_IMAGER_REG_PATTERN, (int)value);
	default:
		return -ENOTSUP;
	}
}

static int emul_imager_get_ctrl(const struct device *dev, unsigned int cid, void *value)
{
	struct emul_imager_data *data = dev->data;

	switch (cid) {
	case VIDEO_CID_EXPOSURE:
		return emul_imager_read_int(dev, EMUL_IMAGER_REG_EXPOSURE, value);
	case VIDEO_CID_GAIN:
		return emul_imager_read_int(dev, EMUL_IMAGER_REG_GAIN, value);
	case VIDEO_CID_TEST_PATTERN:
		return emul_imager_read_int(dev, EMUL_IMAGER_REG_PATTERN, value);
	case VIDEO_CID_PIXEL_RATE:
		*(int64_t *)value = (int64_t)data->fmt.width * data->fmt.pitch * data->mode->fps;
		return 0;
	default:
		return -ENOTSUP;
	}
}

/* Customize this function according to your "struct emul_imager_mode". */
static int emul_imager_set_mode(const struct device *dev, const struct emul_imager_mode *mode)
{
	struct emul_imager_data *data = dev->data;
	int ret;

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

	LOG_DBG("Applying mode %p at %d FPS", mode, mode->fps);

	/* Apply all the configuration registers for that mode */
	for (int i = 0; i < 2; i++) {
		ret = emul_imager_write_multi(dev, mode->regs[i]);
		if (ret < 0) {
			goto err;
		}
	}

	data->mode = mode;
	return 0;
err:
	LOG_ERR("Could not apply %s mode %p (%u FPS)", dev->name, mode, mode->fps);
	return ret;
}

static int emul_imager_set_frmival(const struct device *dev, enum video_endpoint_id ep,
				   struct video_frmival *frmival)
{
	struct emul_imager_data *data = dev->data;
	struct video_frmival_enum fie = {.format = &data->fmt, .discrete = *frmival};

	if (ep != VIDEO_EP_OUT && ep != VIDEO_EP_ALL) {
		return -EINVAL;
	}

	video_closest_frmival(dev, ep, &fie);
	LOG_DBG("Applying frame interval number %u", fie.index);
	return emul_imager_set_mode(dev, &emul_imager_modes[data->fmt_id][fie.index]);
}

static int emul_imager_get_frmival(const struct device *dev, enum video_endpoint_id ep,
				   struct video_frmival *frmival)
{
	struct emul_imager_data *data = dev->data;

	if (ep != VIDEO_EP_OUT && ep != VIDEO_EP_ALL) {
		return -EINVAL;
	}

	frmival->numerator = 1;
	frmival->denominator = data->mode->fps;
	return 0;
}

static int emul_imager_enum_frmival(const struct device *dev, enum video_endpoint_id ep,
				    struct video_frmival_enum *fie)
{
	const struct emul_imager_mode *mode;
	size_t fmt_id;
	int ret;

	if (ep != VIDEO_EP_OUT && ep != VIDEO_EP_ALL) {
		return -EINVAL;
	}

	ret = video_format_caps_index(fmts, fie->format, &fmt_id);
	if (ret < 0) {
		return ret;
	}

	mode = &emul_imager_modes[fmt_id][fie->index];

	fie->type = VIDEO_FRMIVAL_TYPE_DISCRETE;
	fie->discrete.numerator = 1;
	fie->discrete.denominator = mode->fps;
	fie->index++;

	return mode->fps == 0;
}

/* White, Yellow, Cyan, Green, Magenta, Red, Blue, Black */
static const uint16_t pattern_8bars_yuv[8][3] = {
	{0xFF, 0x7F, 0x7F}, {0xFF, 0x00, 0xFF}, {0xFF, 0xFF, 0x00}, {0x7F, 0x00, 0x00},
	{0x00, 0xFF, 0xFF}, {0x00, 0x00, 0xFF}, {0x00, 0xFF, 0x00}, {0x00, 0x7F, 0x7F}};
static const uint16_t pattern_8bars_rgb[8][3] = {
	{0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0x00}, {0x00, 0xFF, 0xFF}, {0x00, 0xFF, 0x00},
	{0xFF, 0x00, 0xFF}, {0xFF, 0x00, 0x00}, {0x00, 0x00, 0xFF}, {0x00, 0x00, 0x00}};
static void emul_imager_fill_framebuffer(const struct device *const dev, struct video_format *fmt)
{
	struct emul_imager_data *data = dev->data;
	uint16_t *fb16 = (uint16_t *)data->framebuffer;
	uint16_t r, g, b, y, uv;

	/* Fill the first row of the emulated framebuffer */
	switch (fmt->pixelformat) {
	case VIDEO_PIX_FMT_YUYV:
		for (size_t i = 0; i < fmt->width; i++) {
			y = pattern_8bars_yuv[i * 8 / fmt->width][0];
			uv = pattern_8bars_yuv[i * 8 / fmt->width][1 + i % 2];
			fb16[i] = sys_cpu_to_be16(y << 8 | uv << 0);
		}
		break;
	case VIDEO_PIX_FMT_RGB565:
		for (size_t i = 0; i < fmt->width; i++) {
			r = pattern_8bars_rgb[i * 8 / fmt->width][0] >> (8 - 5);
			g = pattern_8bars_rgb[i * 8 / fmt->width][1] >> (8 - 6);
			b = pattern_8bars_rgb[i * 8 / fmt->width][2] >> (8 - 5);
			fb16[i] = sys_cpu_to_le16((r << 11) | (g << 6) | (b << 0));
		}
		break;
	default:
		LOG_WRN("Unsupported pixel format %x, supported: %x, %x", fmt->pixelformat,
			VIDEO_PIX_FMT_YUYV, VIDEO_PIX_FMT_RGB565);
		memset(fb16, 0, fmt->pitch);
	}

	/* Duplicate the first row over the whole frame */
	for (size_t i = 1; i < fmt->height; i++) {
		memcpy(data->framebuffer + fmt->pitch * i, data->framebuffer, fmt->pitch);
	}
}

static int emul_imager_set_fmt(const struct device *const dev, enum video_endpoint_id ep,
			       struct video_format *fmt)
{
	struct emul_imager_data *data = dev->data;
	size_t fmt_id;
	int ret;

	if (ep != VIDEO_EP_OUT && ep != VIDEO_EP_ALL) {
		return -EINVAL;
	}

	if (fmt->pitch * fmt->height > CONFIG_VIDEO_EMUL_IMAGER_FRAMEBUFFER_SIZE) {
		LOG_ERR("%s has %u bytes of memory, unable to support %x %ux%u (%u bytes)",
			dev->name, CONFIG_VIDEO_EMUL_IMAGER_FRAMEBUFFER_SIZE, fmt->pixelformat,
			fmt->width, fmt->height, fmt->pitch * fmt->height);
		return -ENOMEM;
	}

	if (memcmp(&data->fmt, fmt, sizeof(data->fmt)) == 0) {
		return 0;
	}

	ret = video_format_caps_index(fmts, fmt, &fmt_id);
	if (ret < 0) {
		LOG_ERR("Format %x %ux%u not found for %s", fmt->pixelformat, fmt->width,
			fmt->height, dev->name);
		return ret;
	}

	ret = emul_imager_set_mode(dev, &emul_imager_modes[fmt_id][0]);
	if (ret < 0) {
		return ret;
	}

	/* Change the image pattern on the framebuffer */
	emul_imager_fill_framebuffer(dev, fmt);

	data->fmt_id = fmt_id;
	data->fmt = *fmt;
	return 0;
}

static int emul_imager_get_fmt(const struct device *dev, enum video_endpoint_id ep,
			       struct video_format *fmt)
{
	struct emul_imager_data *data = dev->data;

	if (ep != VIDEO_EP_OUT && ep != VIDEO_EP_ALL) {
		return -EINVAL;
	}

	*fmt = data->fmt;
	return 0;
}

static int emul_imager_get_caps(const struct device *dev, enum video_endpoint_id ep,
				struct video_caps *caps)
{
	if (ep != VIDEO_EP_OUT && ep != VIDEO_EP_ALL) {
		return -EINVAL;
	}

	caps->format_caps = fmts;
	return 0;
}

static int emul_imager_stream_start(const struct device *dev)
{
	return emul_imager_write_reg(dev, EMUL_IMAGER_REG_CTRL, 1);
}

static int emul_imager_stream_stop(const struct device *dev)
{
	return emul_imager_write_reg(dev, EMUL_IMAGER_REG_CTRL, 0);
}

static DEVICE_API(video, emul_imager_driver_api) = {
	.set_ctrl = emul_imager_set_ctrl,
	.get_ctrl = emul_imager_get_ctrl,
	.set_frmival = emul_imager_set_frmival,
	.get_frmival = emul_imager_get_frmival,
	.enum_frmival = emul_imager_enum_frmival,
	.set_format = emul_imager_set_fmt,
	.get_format = emul_imager_get_fmt,
	.get_caps = emul_imager_get_caps,
	.stream_start = emul_imager_stream_start,
	.stream_stop = emul_imager_stream_stop,
};

int emul_imager_init(const struct device *dev)
{
	struct video_format fmt;
	uint8_t sensor_id;
	int ret;

	if (/* !i2c_is_ready_dt(&cfg->i2c) */ false) {
		/* LOG_ERR("Bus %s is not ready", cfg->i2c.bus->name); */
		return -ENODEV;
	}

	ret = emul_imager_read_reg(dev, EMUL_IMAGER_REG_SENSOR_ID, &sensor_id);
	if (ret < 0 || sensor_id != EMUL_IMAGER_SENSOR_ID) {
		LOG_ERR("Failed to get %s correct sensor ID (0x%x", dev->name, sensor_id);
		return ret;
	}

	ret = emul_imager_write_multi(dev, emul_imager_init_regs);
	if (ret < 0) {
		LOG_ERR("Could not set %s initial registers", dev->name);
		return ret;
	}

	fmt.pixelformat = fmts[0].pixelformat;
	fmt.width = fmts[0].width_min;
	fmt.height = fmts[0].height_min;
	fmt.pitch = fmt.width * 2;

	ret = emul_imager_set_fmt(dev, VIDEO_EP_OUT, &fmt);
	if (ret < 0) {
		LOG_ERR("Failed to set %s to default format %x %ux%u", dev->name, fmt.pixelformat,
			fmt.width, fmt.height);
	}

	return 0;
}

#define EMUL_IMAGER_DEFINE(inst)                                                                   \
	static struct emul_imager_data emul_imager_data_##inst;                                    \
                                                                                                   \
	static const struct emul_imager_config emul_imager_cfg_##inst = {                          \
		.i2c = /* I2C_DT_SPEC_INST_GET(inst) */ {0},                                       \
	};                                                                                         \
                                                                                                   \
	DEVICE_DT_INST_DEFINE(inst, &emul_imager_init, NULL, &emul_imager_data_##inst,             \
			      &emul_imager_cfg_##inst, POST_KERNEL, CONFIG_VIDEO_INIT_PRIORITY,    \
			      &emul_imager_driver_api);

DT_INST_FOREACH_STATUS_OKAY(EMUL_IMAGER_DEFINE)
