/*
   forward-v4l2-fd720.h - v4l2 driver for SoftLab-NSK FD720 video board

   Copyright (C) 2017 - 2024 SoftLab-NSK <forward@softlab.tv>

   This program is free software; you can redistribute it and/or
   modify it under the terms of the GNU General Public License
   as published by the Free Software Foundation; either version 2
   of the License, or (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
*/

#include "forward-v4l2.h"
#include "forward-v4l2-io.h"

#include "fd720_reg.h"

#define FD720_IO_IRQ_NUM(io) ((io->index) * 3)
#define FD720_BUFFER_NEXT(buf, offset, count) ((((unsigned int)(buf)) + (offset)) % (count))
#define FD720_NUMBER_IN_BUFFERS (4)

static forward_irq_t fd720_irq_mask(const struct forward_v4l2_io *io)
{
	forward_irq_t result = { 0 };
	forward_irq_flags_set_one(result, FD720_IO_IRQ_NUM(io));
	return result;
}

static bool fd720_irq_info(struct forward_v4l2_io *io, const forward_irq_t *irq, int *cur_buf,
			   int *prev_buf, int *next_buf, int *vbi_size, int *video_size)
{
	if (!forward_irq_has_irq(*irq, FD720_IO_IRQ_NUM(io)))
		return false;

	*cur_buf = forward_irq_extract_data(*irq, FD720_IO_IRQ_NUM(io) + 1, 2);
	*prev_buf = FD720_BUFFER_NEXT(*cur_buf, -1, FD720_NUMBER_IN_BUFFERS);
	*next_buf = FD720_BUFFER_NEXT(*cur_buf, 1, FD720_NUMBER_IN_BUFFERS);
	if (video_size != NULL)
		*video_size = 0;
	if (vbi_size != NULL)
		*vbi_size = 0;

	return true;
}

static void fd720_region_info(const struct forward_v4l2_io *io, u32 *addr, size_t *size)
{
	*addr = io->index * 0x20000000;
	*size = 0x10000000;
}

static void fd720_buffer_info(const struct forward_v4l2_io *io, int buffer, u32 *address,
			      size_t *size)
{
	*size = 0x04000000UL;
	*address = io->index * 0x20000000UL + buffer * *size;
}

static bool fd720_timings_valid(const struct forward_v4l2_io *io,
				const struct forward_v4l2_timings *t)
{
	if (t->type != FORWARD_TIMING_HDMI)
		return false;

	if (t->hdmi.bt.pixelclock > 594000000)
		return false;

	return true;
}

static int fd720_timings_set(const struct forward_v4l2_io *io, const struct forward_v4l2_timings *t)
{
	return 0;
}

static int fd720_timings_query(const struct forward_v4l2_io *io, struct forward_v4l2_timings *t)
{
	struct forward_dev *dev = io->v4l2->dev;
	FD720_VideoInCS ics = FD720_VideoInCS_R(dev->csr, io->index);
	FD720_VideoInInfoFrame avi;
	FD720_VideoInLine line;
	FD720_VideoInPixel pixel;
	u64 pixelclk;

	if (!ics.hpd)
		return -ENOLINK;

	if (!ics.formatLocked || !ics.rateLocked)
		return -ENOLCK;

	if (!t)
		return 0;

	avi = FD720_VideoInInfoFrame_R(dev->csr, io->index);

	if (!avi.aviValid)
		avi.aviVIC = 0;
	line = FD720_VideoInLine_R(dev->csr, io->index);
	pixel = FD720_VideoInPixel_R(dev->csr, io->index);
	pixelclk = ics.bitrate * 25600;

	if (avi.aviValid && (avi.aviY == 3))
		pixelclk *= 2;

	if (avi.aviValid && (avi.aviPR != 0)) {
		pixelclk /= (avi.aviPR + 1);
		pixel.totalPixels /= (avi.aviPR + 1);
		pixel.activePixels /= (avi.aviPR + 1);
	}

	return forward_v4l2_timing_guess_hdmi(!ics.progressive, pixel.totalPixels, line.totalLines,
					      pixel.activePixels, line.activeLines, avi.aviVIC,
					      pixelclk, t) ?
		       0 :
		       -ERANGE;
}

static bool fd720_timings_changed(const struct forward_v4l2_io *io,
				  const struct forward_v4l2_timings *old,
				  struct forward_v4l2_timings *new)
{
	if (fd720_timings_query(io, new))
		return false;

	return !forward_v4l2_timings_equal(new, old, true);
}

static void fd720_check_fmt(const struct forward_v4l2_io *io, struct v4l2_pix_format *f)
{
	struct forward_dev *dev = io->v4l2->dev;
	FD720_VideoInInfoFrame avi = FD720_VideoInInfoFrame_R(io->v4l2->dev->csr, io->index);

	if (avi.aviValid) {
		if ((f->pixelformat == V4L2_PIX_FMT_YUV420) &&
		    ((avi.aviY != 3) || ((dev->fw_version & 0xFFFF) < 864)))
			f->pixelformat = V4L2_PIX_FMT_UYVY;
	}

	if (f->pixelformat == V4L2_PIX_FMT_BGR24)
		f->bytesperline = f->width * 3;
	else if ((f->pixelformat == V4L2_PIX_FMT_UYVY) || (f->pixelformat == V4L2_PIX_FMT_YUYV))
		f->bytesperline = f->width * 2;
	else if (f->pixelformat == V4L2_PIX_FMT_YUV420)
		f->bytesperline = f->width;

	f->sizeimage = f->height * f->bytesperline;

	if (f->pixelformat == V4L2_PIX_FMT_YUV420)
		f->sizeimage = f->sizeimage * 3 / 2;
}

static int fd720_enum_fmt(const struct forward_v4l2_io *io, int index, u32 *pix_fmt, char *desc,
			  size_t desc_len)
{
	(void)io;
	if ((index < 0) || (index > 3))
		return -EINVAL;

	switch (index) {
	case 0:
		*pix_fmt = V4L2_PIX_FMT_UYVY;
		snprintf(desc, desc_len, "UYVY 4:2:2");
		break;
	case 1:
		*pix_fmt = V4L2_PIX_FMT_YUYV;
		snprintf(desc, desc_len, "YUYV 4:2:2");
		break;
	case 2:
		*pix_fmt = V4L2_PIX_FMT_BGR24;
		snprintf(desc, desc_len, "RGB");
		break;
	case 3:
		*pix_fmt = V4L2_PIX_FMT_YUV420;
		snprintf(desc, desc_len, "YUV 4:2:0");
		break;
	default:
		return -EINVAL;
	}
	return 0;
}

static void fd720_set_fmt(struct forward_v4l2_io *io, struct v4l2_pix_format *f)
{
	io->format_conv = FORWARD_V4L2_CONV_NONE;

	if (f->pixelformat == V4L2_PIX_FMT_YUYV)
		io->format_conv = FORWARD_V4L2_CONV_UYVY_YUYV;
	else if (f->pixelformat == V4L2_PIX_FMT_YUV420)
		io->format_conv = FORWARD_V4L2_CONV_HDMI420_YUV420;
}

static void fd720_setup_color_converter(struct forward_v4l2_io *io)
{
	struct forward_dev *dev = io->v4l2->dev;
	int16_t ino[3] = { 0, 0, 0 };
	int16_t outo[3] = { 0, 0, 0 };
	int16_t m[9] = { 4096, 0, 0, 0, 4096, 0, 0, 0, 4096 };
	int i;
	FD720_VideoInColorConversion cc = { .clamp = 0, .clampRGB = 0, .write = 1 };
	FD720_VideoInInfoFrame avi = FD720_VideoInInfoFrame_R(dev->csr, io->index);
	bool out_yuv = (io->format.pixelformat == V4L2_PIX_FMT_UYVY) ||
		       (io->format.pixelformat == V4L2_PIX_FMT_YUYV);
	// TODO: get that from V4L2 controls
	bool out_limited = io->format.pixelformat != V4L2_PIX_FMT_BGR24;

	if (avi.aviValid) {
		bool in_yuv = (avi.aviY == 1) || (avi.aviY == 2) || (avi.aviY == 3);
		bool in_limited = (avi.aviYQ == 0);
		bool ident = ((out_limited && in_limited) || (!out_limited && !in_limited)) &&
			     ((out_yuv && in_yuv) || (!out_yuv && !in_yuv));
		bool is420 = (avi.aviY == 3) && (io->format.pixelformat == V4L2_PIX_FMT_YUV420);

		if ((dev->fw_version & 0xFFFF) < 860) {
			forward_warn(
				dev,
				"this board firmware has limited color conversion support, consider firmware upgrade.");
			in_limited = true;
		}
		if ((dev->fw_version & 0xFFFF) < 864) {
			forward_warn(
				dev,
				"this board firmware has limited support for YCbCr 4:2:0, consider firmware upgrade.");
			is420 = false;
		}

		if (is420)
			ident = true;

		if (!ident && in_yuv && !out_yuv) {
			// YUV -> RGB
			m[0] = 7600;
			m[1] = 4096;
			m[2] = 0;
			m[3] = -767;
			m[4] = 4096;
			m[5] = -1917;
			m[6] = 0;
			m[7] = 4096;
			m[8] = 6450;
			ino[0] = -2048;
			ino[1] = 0;
			ino[2] = -2048;
		} else if (!ident && !in_yuv && out_yuv) {
			// RGB -> YUV
			m[0] = 2048;
			m[1] = -1579;
			m[2] = -469;
			m[3] = 296;
			m[4] = 2929;
			m[5] = 871;
			m[6] = -188;
			m[7] = -1860;
			m[8] = 2048;
			outo[0] = 2048;
			outo[1] = 0;
			outo[2] = 2048;
		}

		if (in_limited && !ident) {
			for (i = 0; i < 9; i++) {
				if (in_yuv && ((i % 3) != 1))
					m[i] = (int)m[i] * 255 / 224;
				else
					m[i] = (int)m[i] * 255 / 219;
			}
			ino[0] += in_yuv ? 0 : -256;
			ino[1] += -256;
			ino[2] += in_yuv ? 0 : -256;
		}

		if (out_limited && !ident) {
			for (i = 0; i < 9; i++) {
				if (out_yuv && ((i / 3) != 1))
					m[i] = (int)m[i] * 224 / 255;
				else
					m[i] = (int)m[i] * 219 / 255;
			}
			outo[0] += 256;
			outo[1] += 256;
			outo[2] += 256;
		}
		if (!ident && out_limited) {
			cc.clamp = 1;
			cc.clampRGB = out_yuv ? 0 : 1;
		}
	}

	for (i = 0; i < 3; i++) {
		cc.data = ino[i];
		cc.address = i + 0;
		FD720_VideoInColorConversion_W(dev->csr, io->index, cc);
	}
	for (i = 0; i < 3; i++) {
		cc.data = outo[i];
		cc.address = i + 3;
		FD720_VideoInColorConversion_W(dev->csr, io->index, cc);
	}
	for (i = 0; i < 9; i++) {
		cc.data = m[i];
		cc.address = i + 6;
		FD720_VideoInColorConversion_W(dev->csr, io->index, cc);
	}
}

static void fd720_prepare(struct forward_v4l2_io *io)
{
	struct forward_dev *dev = io->v4l2->dev;
	FD720_VideoInCS ics = FD720_VideoInCS_R(dev->csr, io->index);

	ics.test = 0;
	ics.edidOverride = 0;
	ics.pixrep = 0;
	if ((io->format.pixelformat == V4L2_PIX_FMT_UYVY) ||
	    (io->format.pixelformat == V4L2_PIX_FMT_YUYV))
		ics.captureColor = 2;
	else if (io->format.pixelformat == V4L2_PIX_FMT_YUV420)
		ics.captureColor = 3;
	else
		ics.captureColor = 0;
	ics.captureDepth = 0;

	fd720_setup_color_converter(io);
	FD720_VideoInCS_W(dev->csr, io->index, ics);
}

static void fd720_start(struct forward_v4l2_io *io)
{
	struct forward_dev *dev = io->v4l2->dev;
	dev->cfg.toggle_streaming(dev, io->index, true);
}

static void fd720_stop(struct forward_v4l2_io *io)
{
	struct forward_dev *dev = io->v4l2->dev;
	dev->cfg.toggle_streaming(dev, io->index, false);
}

static bool fd720_signal_present(const struct forward_v4l2_io *io)
{
	struct forward_dev *dev = io->v4l2->dev;
	FD720_VideoInCS ics = FD720_VideoInCS_R(dev->csr, io->index);
	return ics.rateLocked ? true : false;
}

static u32 fd720_hw_timestamp(struct forward_v4l2 *v4l2)
{
	struct forward_dev *dev = v4l2->dev;
	return FD720_VideoClockCounter_R(dev->csr).counter;
}

static u32 fd720_io_timestamp(const struct forward_v4l2_io *io)
{
	struct forward_dev *dev = io->v4l2->dev;
	return FD720_VideoInIRQRefTime_R(dev->csr, io->index).time;
}

struct forward_v4l2_dev_ops fd720_ops = {
	.irq_mask = fd720_irq_mask,
	.irq_info = fd720_irq_info,
	.region_info = fd720_region_info,
	.buffer_info = fd720_buffer_info,
	.timings_valid = fd720_timings_valid,
	.timings_set = fd720_timings_set,
	.timings_query = fd720_timings_query,
	.timings_changed = fd720_timings_changed,
	.check_fmt = fd720_check_fmt,
	.enum_fmt = fd720_enum_fmt,
	.set_fmt = fd720_set_fmt,
	.prepare = fd720_prepare,
	.start = fd720_start,
	.stop = fd720_stop,
	.signal_present = fd720_signal_present,
	.hw_buffers = FD720_NUMBER_IN_BUFFERS,
	.vbi_support = false,

	.io_timestamp = fd720_io_timestamp,
	.hw_timestamp = fd720_hw_timestamp,
	.clock_deviation = NULL,
	.set_clock_deviation = NULL,
};
