/*
   forward-v4l2-fd2110.c - v4l2 driver for SoftLab-NSK FD2110 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 "forward-v4l2-ioctl.h"
#include "forward-v4l2-genlock.h"
#include "forward-pll.h"
#include "forward-fd2110.h"

#include <linux/inet.h>

#include "fd2110_reg.h"

#define FD2110_NUMBER_IN_BUFFERS (8)
#define FD2110_BUFFER_SIZE (8 * 1024 * 1024)
#define FD2110_BUFFER_NEXT(buf, offset, count) ((((unsigned int)(buf)) + (offset)) % (count))

#define FD2110_ASI_IN_BUFFER_SIZE (270000000ULL / 10 / 50)
#define FD2110_ASI_IN_BUFFER_SIZE_RAW (270000000ULL / 8 / 50)

#define FD2110_IO_IS_SDI(io) ((io)->index < 2)
#define FD2110_IO_VDMA_STREAM(io) (((io)->index - 2) % 16)
#define FD2110_IO_IS_TX(io) ((((io)->index - 2) % 32) >= 16)
#define FD2110_IO_ETHERNET(io) (((io)->index - 2) / 32)

#define FD2110_IO_IRQ_NUM(io)                                       \
	(FD2110_IO_IS_SDI(io) ? ((io)->index * 3) :                 \
				(32 + FD2110_IO_ETHERNET(io) * 32 + \
				 (FD2110_IO_IS_TX(io) ? 16 : 0) + FD2110_IO_VDMA_STREAM(io)))
#define FD2110_IO_VDMA_BASE(io)                                     \
	(FD2110_IO_IS_SDI(io) ?                                     \
		 (0xF0000000 + (io)->index * 0x08000000) :          \
		 (FD2110_IO_ETHERNET(io) * 0x40000000 +             \
		  (FD2110_IO_IS_TX(io) ? 0x20000000 : 0x00000000) + \
		  FD2110_IO_VDMA_STREAM(io) * FD2110_BUFFER_SIZE * FD2110_NUMBER_IN_BUFFERS))

#define FD2110_STREAM_ADDR_LENGTH (64)

struct fd2110_io {
	struct forward_v4l2_io *io;
	u8 stream_ip[16];
	u16 stream_port;
	bool stream_addr_valid;
	u16 stream_own_port;
	enum v4l2_forward_fd2110_stream_color stream_color;
	enum v4l2_forward_fd2110_stream_bpc stream_bpc;
	u8 stream_pt;
	u32 stream_ssrc;
};

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

inline bool fd2110_irq_info_in_sdi(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, FD2110_IO_IRQ_NUM(io)))
		return false;

	*cur_buf = forward_irq_extract_data(*irq, FD2110_IO_IRQ_NUM(io) + 1, 2);
	*prev_buf = FD2110_BUFFER_NEXT(*cur_buf, -1, 4);
	*next_buf = FD2110_BUFFER_NEXT(*cur_buf, 1, 4);
	if (video_size != NULL)
		*video_size = 0;
	if (vbi_size != NULL)
		*vbi_size = 0;

	return true;
}

inline bool fd2110_irq_info_in(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)
{
	FD2110_VDMARxVideoPosition vp;
	if (!forward_irq_has_irq(*irq, FD2110_IO_IRQ_NUM(io)))
		return false;

	vp = FD2110_VDMARxVideoPosition_R(io->v4l2->dev->csr, FD2110_IO_ETHERNET(io),
					  FD2110_IO_VDMA_STREAM(io));
	*cur_buf = vp.buffer * 2 + vp.field;

	if (io->timings.sdi.interlaced) {
		*prev_buf = FD2110_BUFFER_NEXT(*cur_buf, -1, FD2110_NUMBER_IN_BUFFERS);
		*next_buf = FD2110_BUFFER_NEXT(*cur_buf, 1, FD2110_NUMBER_IN_BUFFERS);
	} else {
		*prev_buf = FD2110_BUFFER_NEXT(*cur_buf, -2, FD2110_NUMBER_IN_BUFFERS);
		*next_buf = FD2110_BUFFER_NEXT(*cur_buf, 2, FD2110_NUMBER_IN_BUFFERS);
	}

	if (video_size != NULL)
		*video_size = 0;
	if (vbi_size != NULL)
		*vbi_size = 0;

	return true;
}

inline bool fd2110_irq_info_out_sdi(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, FD2110_IO_IRQ_NUM(io)))
		return false;

	*cur_buf = forward_irq_extract_data(*irq, FD2110_IO_IRQ_NUM(io) + 1, 2);
	*prev_buf = FD2110_BUFFER_NEXT(*cur_buf, -1, 4);
	*next_buf = FD2110_BUFFER_NEXT(*cur_buf, 1, 4);
	if (video_size != NULL)
		*video_size = 0;
	if (vbi_size != NULL)
		*vbi_size = 0;

	return true;
}

inline bool fd2110_irq_info_out(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)
{
	FD2110_VDMATxVideoPosition vp;
	if (!forward_irq_has_irq(*irq, FD2110_IO_IRQ_NUM(io)))
		return false;

	vp = FD2110_VDMATxVideoPosition_R(io->v4l2->dev->csr, FD2110_IO_ETHERNET(io),
					  FD2110_IO_VDMA_STREAM(io));
	*cur_buf = vp.buffer * 2 + vp.field;

	if (io->timings.sdi.interlaced) {
		*prev_buf = FD2110_BUFFER_NEXT(*cur_buf, -1, FD2110_NUMBER_IN_BUFFERS);
		*next_buf = FD2110_BUFFER_NEXT(*cur_buf, 1, FD2110_NUMBER_IN_BUFFERS);
	} else {
		*prev_buf = FD2110_BUFFER_NEXT(*cur_buf, -2, FD2110_NUMBER_IN_BUFFERS);
		*next_buf = FD2110_BUFFER_NEXT(*cur_buf, 2, FD2110_NUMBER_IN_BUFFERS);
	}

	return true;
}

inline bool fd2110_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 (FD2110_IO_IS_SDI(io)) {
		if (io->state == FORWARD_IO_TX)
			return fd2110_irq_info_out_sdi(io, irq, cur_buf, prev_buf, next_buf,
						       vbi_size, video_size);
		else
			return fd2110_irq_info_in_sdi(io, irq, cur_buf, prev_buf, next_buf,
						      vbi_size, video_size);
	} else {
		if (io->state == FORWARD_IO_TX)
			return fd2110_irq_info_out(io, irq, cur_buf, prev_buf, next_buf, vbi_size,
						   video_size);
		else
			return fd2110_irq_info_in(io, irq, cur_buf, prev_buf, next_buf, vbi_size,
						  video_size);
	}

	return true;
}

static void fd2110_region_info(const struct forward_v4l2_io *io, u32 *addr, size_t *size)
{
	*addr = FD2110_IO_VDMA_BASE(io);
	*size = FD2110_BUFFER_SIZE * FD2110_NUMBER_IN_BUFFERS;
}

static void fd2110_buffer_info(const struct forward_v4l2_io *io, int buffer, u32 *address,
			       size_t *size)
{
	*address = FD2110_IO_VDMA_BASE(io) + buffer * FD2110_BUFFER_SIZE;
	*size = FD2110_BUFFER_SIZE;
}

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

	if (FD2110_IO_IS_SDI(io)) {
		if ((t->sdi.mode != FORWARD_SDI_MODE_12G_SDI) &&
		    (t->sdi.mode != FORWARD_SDI_MODE_6G_SDI) &&
		    (t->sdi.mode != FORWARD_SDI_MODE_3G_SDI) &&
		    (t->sdi.mode != FORWARD_SDI_MODE_HD_SDI) &&
		    (t->sdi.mode != FORWARD_SDI_MODE_SD_SDI)) {
			return false;
		}
	}

	return true;
}

static int fd2110_timings_query(const struct forward_v4l2_io *io, struct forward_v4l2_timings *t)
{
	struct forward_dev *dev = io->v4l2->dev;
	enum forward_sdi_mode mode;
	FD2110_VideoInCS ics = FD2110_VideoInCS_R(dev->csr, io->index);
	FD2110_VideoInLine line;
	FD2110_VideoInPixel pixel;
	FD2110_VideoInVPID vpid = FD2110_VideoInVPID_R(dev->csr, io->index);
	FD2110_VideoInFrameSize framesize = FD2110_VideoInFrameSize_R(dev->csr, io->index);
	u32 pixel_clk;
	int width, height, active_width, active_height;
	bool result;

	if (!FD2110_IO_IS_SDI(io))
		return -EOPNOTSUPP;

	if (io->state != FORWARD_IO_RX)
		return -EOPNOTSUPP;

	if (!ics.cd)
		return -ENOLINK;

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

	if (!t)
		return 0;

	switch (ics.mode) {
	case 0:
		mode = FORWARD_SDI_MODE_HD_SDI;
		break;
	case 1:
		mode = FORWARD_SDI_MODE_3G_SDI;
		break;
	case 2:
		mode = FORWARD_SDI_MODE_6G_SDI;
		break;
	case 3:
		mode = FORWARD_SDI_MODE_12G_SDI;
		break;
	case 4:
		mode = FORWARD_SDI_MODE_SD_SDI;
		break;
	default:
		mode = FORWARD_SDI_MODE_INVALID;
		break;
	}

	if (ics.mode == 5) {
		int i = 0;
		for (i = 0; i < forward_v4l2_timings_count; i++) {
			if (fd2110_timings_valid(io, &forward_v4l2_timings[i])) {
				*t = forward_v4l2_timings[i];
				t->sdi.flags |= FORWARD_SDI_TIMINGS_F_ASI;
				break;
			}
		}
		return 0;
	}

	line = FD2110_VideoInLine_R(dev->csr, io->index);
	pixel = FD2110_VideoInPixel_R(dev->csr, io->index);
	vpid = FD2110_VideoInVPID_R(dev->csr, io->index);
	if (!framesize.size)
		framesize.size = 1;
	width = pixel.totalPixels;
	height = line.totalLines;
	active_width = pixel.activePixels;
	active_height = line.activeLines;

	if ((mode == FORWARD_SDI_MODE_6G_SDI) || (mode == FORWARD_SDI_MODE_12G_SDI)) {
		width *= 2;
		height *= 2;
		active_width *= 2;
		active_height *= 2;
	}

	pixel_clk = 48000000ULL * width * height / framesize.size;

	result = forward_v4l2_timing_guess_sdi(mode, !ics.progressive, width, height, active_width,
					       active_height, vpid.vpid, pixel_clk, t);

	return result ? 0 : -ERANGE;
}

inline int fd2110_timings_set_in_sdi(const struct forward_v4l2_io *io,
				     const struct forward_v4l2_timings *t)
{
	if (!fd2110_timings_valid(io, t))
		return -ERANGE;

	return 0;
}

inline int fd2110_timings_set_in(const struct forward_v4l2_io *io,
				 const struct forward_v4l2_timings *t)
{
	if (!fd2110_timings_valid(io, t))
		return -ERANGE;

	return 0;
}

inline int fd2110_timings_set_out_sdi(const struct forward_v4l2_io *io,
				      const struct forward_v4l2_timings *t)
{
	// TODO: ASI
	struct forward_dev *dev = io->v4l2->dev;
	int index = io->index;
	FD2110_VideoOutCS ocs = FD2110_VideoOutCS_R(dev->csr, index);
	FD2110_VideoOutLine line;
	FD2110_VideoOutPixel pixel;
	FD2110_VideoOutStart start;
	FD2110_VideoOutStop stop;
	FD2110_VideoOutField field;
	FD2110_VideoOutVPID vpid;
	bool dq;
	bool asi = t->sdi.flags & FORWARD_SDI_TIMINGS_F_ASI;
	int dq_div = 1;

	memset(&ocs, 0, sizeof(FD2110_VideoOutCS));

	ocs.reset = 1;
	FD2110_VideoOutCS_W(dev->csr, index, ocs);

	dq = !asi &&
	     ((t->sdi.mode == FORWARD_SDI_MODE_6G_SDI) | (t->sdi.mode == FORWARD_SDI_MODE_12G_SDI));

	ocs.levelB = 0;

	switch (t->sdi.mode) {
	case FORWARD_SDI_MODE_HD_SDI:
		ocs.mode = 0;
		break;
	case FORWARD_SDI_MODE_3G_SDI:
		ocs.mode = 1;
		break;
	case FORWARD_SDI_MODE_6G_SDI:
		ocs.mode = 2;
		break;
	case FORWARD_SDI_MODE_12G_SDI:
		ocs.mode = 3;
		break;
	case FORWARD_SDI_MODE_SD_SDI:
		ocs.mode = 4;
		break;
	default:
		ocs.mode = 0;
		break;
	}

	if (asi)
		ocs.mode = 5;

	if (t->sdi.m)
		ocs.clockM = 1;

	ocs.freeRunning = 1;
	ocs.progressive = (asi || t->sdi.interlaced) ? 0 : 1;

	if (dq)
		dq_div = 2;

	if (asi) {
		line.totalLines = 540;
		pixel.totalPixels = 1000 >> io->asi_period;
		pixel.activePixels = 720 >> io->asi_period;
		start.startOdd = 11;
		start.startEven = 281;
		stop.stopOdd = 267;
		stop.stopEven = 537;
		field.switchOdd = 539;
		field.switchEven = 269;
		vpid.vpid = 0x00000000;
	} else {
		line.totalLines = t->sdi.height / dq_div;
		pixel.totalPixels = t->sdi.width / dq_div;
		pixel.activePixels = t->sdi.active_width / dq_div;
		start.startOdd = t->sdi.active_starts[0] / dq_div;
		start.startEven = t->sdi.active_starts[1] / dq_div;
		stop.stopOdd = t->sdi.active_stops[0] / dq_div;
		stop.stopEven = t->sdi.active_stops[1] / dq_div;
		field.switchOdd = t->sdi.field_switch[0] / dq_div;
		field.switchEven = t->sdi.field_switch[1] / dq_div;
		vpid.vpid = forward_v4l2_timings_vpid(t, false, false, io->widescreen);
	}

	FD2110_VideoOutLine_W(dev->csr, index, line);
	FD2110_VideoOutPixel_W(dev->csr, index, pixel);
	FD2110_VideoOutStart_W(dev->csr, index, start);
	FD2110_VideoOutStop_W(dev->csr, index, stop);
	FD2110_VideoOutField_W(dev->csr, index, field);
	FD2110_VideoOutVPID_W(dev->csr, index, vpid);
	FD2110_VideoOutCS_W(dev->csr, index, ocs);

	if (asi)
		ocs.playbackRaw = 1;

	FD2110_VideoOutCS_W(dev->csr, index, ocs);

	ocs.reset = 0;

	FD2110_VideoOutCS_W(dev->csr, index, ocs);

	return 0;
}

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

static int fd2110_timings_set_sdi(const struct forward_v4l2_io *io,
				  const struct forward_v4l2_timings *t)
{
	if (io->state == FORWARD_IO_TX)
		return fd2110_timings_set_out_sdi(io, t);
	else
		return fd2110_timings_set_in_sdi(io, t);
}

static int fd2110_timings_set_net(const struct forward_v4l2_io *io,
				  const struct forward_v4l2_timings *t)
{
	if (io->state == FORWARD_IO_TX)
		return fd2110_timings_set_out(io, t);
	else
		return fd2110_timings_set_in(io, t);
}

static int fd2110_timings_set(const struct forward_v4l2_io *io,
			      const struct forward_v4l2_timings *t)
{
	if (FD2110_IO_IS_SDI(io))
		return fd2110_timings_set_sdi(io, t);
	else
		return fd2110_timings_set_net(io, t);
}

static bool fd2110_timings_changed(const struct forward_v4l2_io *io,
				   const struct forward_v4l2_timings *old,
				   struct forward_v4l2_timings *new)
{
	if (FD2110_IO_IS_SDI(io)) {
		if (fd2110_timings_query(io, new))
			return false;
	} else {
		*new = io->timings;
	}

	if ((old->sdi.flags & FORWARD_SDI_TIMINGS_F_ASI) ?
		    !(new->sdi.flags & FORWARD_SDI_TIMINGS_F_ASI) :
		    (new->sdi.flags & FORWARD_SDI_TIMINGS_F_ASI))
		return true;

	if (old->sdi.mode != new->sdi.mode)
		return true;

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

static void fd2110_check_fmt_sdi(const struct forward_v4l2_io *io, struct v4l2_pix_format *f)
{
	struct forward_dev *dev = io->v4l2->dev;
	FD2110_VideoInCS ics;

	if ((f->pixelformat != V4L2_PIX_FMT_MPEG) && (f->pixelformat != V4L2_PIX_FMT_UYVY) &&
	    (f->pixelformat != V4L2_PIX_FMT_YUYV) && (f->pixelformat != V4L2_PIX_FMT_V210) &&
	    (f->pixelformat != V4L2_PIX_FMT_SL_RAW) &&
	    (f->pixelformat != V4L2_PIX_FMT_SL_RAW_ASI)) {
		f->pixelformat = V4L2_PIX_FMT_UYVY;
		f->bytesperline = f->width * 2;
		f->sizeimage = f->height * f->bytesperline;
	}

	if (f->pixelformat == V4L2_PIX_FMT_MPEG) {
		f->bytesperline = 0;
		f->sizeimage = FD2110_ASI_IN_BUFFER_SIZE >> (io->asi_period);
	} else if (f->pixelformat == V4L2_PIX_FMT_SL_RAW_ASI) {
		f->bytesperline = 0;
		f->sizeimage = FD2110_ASI_IN_BUFFER_SIZE_RAW >> (io->asi_period);
	}

	if (io->state != FORWARD_IO_RX)
		return;

	ics = FD2110_VideoInCS_R(dev->csr, io->index);

	if (!ics.cd || !ics.formatLocked || !ics.modeLocked)
		return;

	if (ics.mode == 5) {
		if (f->pixelformat == V4L2_PIX_FMT_SL_RAW)
			f->pixelformat = V4L2_PIX_FMT_SL_RAW_ASI;
		else if (f->pixelformat != V4L2_PIX_FMT_SL_RAW_ASI)
			f->pixelformat = V4L2_PIX_FMT_MPEG;

		if (f->pixelformat == V4L2_PIX_FMT_MPEG) {
			f->bytesperline = 0;
			f->sizeimage = FD2110_ASI_IN_BUFFER_SIZE >> (io->asi_period);
		} else if (f->pixelformat == V4L2_PIX_FMT_SL_RAW) {
			f->bytesperline = 0;
			f->sizeimage = FD2110_ASI_IN_BUFFER_SIZE_RAW >> (io->asi_period);
		}
	} else {
		if (f->pixelformat == V4L2_PIX_FMT_SL_RAW_ASI)
			f->pixelformat = V4L2_PIX_FMT_SL_RAW;

		if (f->pixelformat == V4L2_PIX_FMT_MPEG)
			f->pixelformat = V4L2_PIX_FMT_UYVY;

		if (f->pixelformat == V4L2_PIX_FMT_V210) {
			f->bytesperline = f->width * 8 / 3;
			f->bytesperline = ((f->bytesperline - 1) / 128 + 1) * 128;
		} else
			f->bytesperline = f->width * 2;

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

static void fd2110_check_fmt_net(const struct forward_v4l2_io *io, struct v4l2_pix_format *f)
{
	if ((f->pixelformat != V4L2_PIX_FMT_UYVY) && (f->pixelformat != V4L2_PIX_FMT_V210)) {
		f->pixelformat = V4L2_PIX_FMT_UYVY;
		f->bytesperline = f->width * 2;
		f->sizeimage = f->height * f->bytesperline;
	}

	if (f->pixelformat == V4L2_PIX_FMT_V210) {
		f->bytesperline = f->width * 8 / 3;
		f->bytesperline = ((f->bytesperline - 1) / 128 + 1) * 128;
	} else
		f->bytesperline = f->width * 2;
}

static void fd2110_check_fmt(const struct forward_v4l2_io *io, struct v4l2_pix_format *f)
{
	if (FD2110_IO_IS_SDI(io))
		fd2110_check_fmt_sdi(io, f);
	else
		fd2110_check_fmt_net(io, f);
}

static int fd2110_enum_fmt(const struct forward_v4l2_io *io, int index, u32 *pix_fmt, char *desc,
			   size_t desc_len)
{
	(void)io;

	if (FD2110_IO_IS_SDI(io)) {
		if (io->state == FORWARD_IO_RX) {
			switch (index) {
			case 0:
				*pix_fmt = V4L2_PIX_FMT_UYVY;
				break;
			case 1:
				*pix_fmt = V4L2_PIX_FMT_YUYV;
				break;
			case 2:
				*pix_fmt = V4L2_PIX_FMT_MPEG;
				break;
			case 3:
				*pix_fmt = V4L2_PIX_FMT_V210;
				snprintf(desc, desc_len, "YUYV 4:2:2 v210");
				break;
			case 4:
				*pix_fmt = V4L2_PIX_FMT_SL_RAW;
				snprintf(desc, desc_len, "SoftLab-NSK Raw");
				break;
			case 5:
				*pix_fmt = V4L2_PIX_FMT_SL_RAW_ASI;
				snprintf(desc, desc_len, "SoftLab-NSK Raw ASI");
				break;
			default:
				return -EINVAL;
			}
		} else {
			switch (index) {
			case 0:
				*pix_fmt = V4L2_PIX_FMT_UYVY;
				break;
			case 1:
				*pix_fmt = V4L2_PIX_FMT_MPEG;
				break;
			case 2:
				*pix_fmt = V4L2_PIX_FMT_V210;
				break;
			case 3:
				*pix_fmt = V4L2_PIX_FMT_SL_RAW;
				snprintf(desc, desc_len, "SoftLab-NSK Raw");
				break;
			case 4:
				*pix_fmt = V4L2_PIX_FMT_SL_RAW_ASI;
				snprintf(desc, desc_len, "SoftLab-NSK Raw ASI");
				break;
			default:
				return -EINVAL;
			}
		}
	} else {
		switch (index) {
		case 0:
			*pix_fmt = V4L2_PIX_FMT_UYVY;
			break;
		case 1:
			*pix_fmt = V4L2_PIX_FMT_V210;
			snprintf(desc, desc_len, "YUYV 4:2:2 v210");
			break;
		default:
			return -EINVAL;
		}
	}

	return 0;
}

static void fd2110_set_fmt(struct forward_v4l2_io *io, struct v4l2_pix_format *f)
{
	if (FD2110_IO_IS_SDI(io)) {
	} else {
		struct fd2110_io *io2110 = io->private;

		io->format_conv = FORWARD_V4L2_CONV_NONE;

		if (FD2110_IO_IS_TX(io)) {
			switch (f->pixelformat) {
			case V4L2_PIX_FMT_V210:
				if (io2110->stream_bpc == V4L_FORWARD_FD2110_STREAM_BPC_10BIT)
					io->format_conv = FORWARD_V4L2_CONV_10BIT_V210;
				break;
			default:
				io->format_conv = FORWARD_V4L2_CONV_NONE;
				break;
			}
		} else {
			if (f->pixelformat == V4L2_PIX_FMT_V210)
				io->format_conv = FORWARD_V4L2_CONV_10BIT_V210;
		}
	}
}

static int fd2110_update_meta(struct forward_v4l2_io *io)
{
	struct forward_dev *dev = io->v4l2->dev;

	if (FD2110_IO_IS_SDI(io)) {
		if (io->state == FORWARD_IO_RX) {
			FD2110_VideoInVPID vpid = FD2110_VideoInVPID_R(dev->csr, io->index);

			if (((vpid.vpid >> 24) & 0xFF) == 0x81)
				io->widescreen = (vpid.vpid & 0x8000) ? true : false;
			else
				io->widescreen = true;
		} else {
			FD2110_VideoOutVPID vpid = FD2110_VideoOutVPID_R(dev->csr, io->index);

			if (((vpid.vpid >> 24) & 0xFF) == 0x81) {
				if (io->widescreen)
					vpid.vpid |= 0x8000;
				else
					vpid.vpid &= ~(0x8000);

				FD2110_VideoOutVPID_W(dev->csr, io->index, vpid);
			}
		}
	}

	return 0;
}

static void fd2110_update_timecode(struct forward_v4l2_io *io, u64 timecode, u16 ddb)
{
	if (FD2110_IO_IS_SDI(io)) {
		uint32_t *reg = io->v4l2->dev->csr + FD2110_VideoOutATC_A(io->index);
		iowrite32((0 << 30) | ((timecode >> 0) & 0x3FFFFFFF), reg);
		iowrite32((1 << 30) | ((timecode >> 30) & 0x3FFFFFFF), reg);
		iowrite32((2 << 30) | ((timecode >> 60) & 0xF) | (((u32)ddb << 4) & 0xFFF0), reg);
	}
}

static void fd2110_prepare_sdi_in(struct forward_v4l2_io *io)
{
	struct forward_dev *dev = io->v4l2->dev;

	FD2110_VideoInCS ics = FD2110_VideoInCS_R(dev->csr, io->index);

	ics.test = 0;
	ics.vbi10b = 1;
	ics.vis10b = (io->format.pixelformat == V4L2_PIX_FMT_V210) ? 1 : 0;
	ics.vbiTop = io->vbi_en ? 1 : 0;
	ics.vbiBottom = 0;
	ics.captureRaw = (io->asi || io->raw) ? 1 : 0;
	ics.asiPeriod = io->asi_period;
	ics.ycSwap = (io->format.pixelformat == V4L2_PIX_FMT_YUYV) ? 1 : 0;

	io->format_conv = FORWARD_V4L2_CONV_NONE;

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

static void fd2110_prepare_sdi_out(struct forward_v4l2_io *io)
{
	struct forward_dev *dev = io->v4l2->dev;

	FD2110_VideoOutCS ocs = FD2110_VideoOutCS_R(dev->csr, io->index);
	FD2110_VideoOutOddDataCount oddC;
	FD2110_VideoOutEvenDataCount evenC;
	struct forward_v4l2_timings *t = &io->timings;
	int dq_div;

	ocs.playbackRaw = (io->asi || io->raw) ? 1 : 0;
	ocs.vbiTop = io->vbi_en ? 1 : 0;
	ocs.vbiBottom = 0;
	ocs.vbi10b = 1;
	ocs.vis10b = (io->format.pixelformat == V4L2_PIX_FMT_V210) ? 1 : 0;
	ocs.dataPresent = 1;
	ocs.test = 0;
	ocs.clone = 0;
	ocs.atcEnable = (io->atc_type != V4L2_FORWARD_TIMECODE_DISABLED) ? 1 : 0;
	ocs.atcType = (io->atc_type == V4L2_FORWARD_TIMECODE_LTC) ? 0 : 1;

	dq_div = (!io->asi && ((t->sdi.mode == FORWARD_SDI_MODE_6G_SDI) |
			       (t->sdi.mode == FORWARD_SDI_MODE_12G_SDI))) ?
			 4 :
			 1;

	if (io->asi) {
		oddC.count = 675000;
		evenC.count = 675000;
	} else if (io->raw) {
		oddC.count = (t->sdi.field_switch[1] + 1) * t->sdi.width * 20 / 8;
		evenC.count =
			(t->sdi.field_switch[0] - t->sdi.field_switch[1]) * t->sdi.width * 20 / 8;
		oddC.count /= dq_div;
		evenC.count /= dq_div;
	} else {
		if (io->format.pixelformat != V4L2_PIX_FMT_V210) {
			oddC.count = t->sdi.active_height[0] * t->sdi.active_width * 2;
			evenC.count = t->sdi.active_height[1] * t->sdi.active_width * 2;
		} else {
			oddC.count = t->sdi.active_height[0] * t->sdi.active_width * 20 / 8;
			evenC.count = t->sdi.active_height[1] * t->sdi.active_width * 20 / 8;
		}

		if (io->vbi_en) {
			oddC.count += t->sdi.topVBIs[0] * t->sdi.active_width * 20 / 8;
			evenC.count += t->sdi.topVBIs[1] * t->sdi.active_width * 20 / 8;
		}
		oddC.count /= dq_div;
		evenC.count /= dq_div;
	}

	FD2110_VideoOutOddDataCount_W(dev->csr, io->index, oddC);
	FD2110_VideoOutEvenDataCount_W(dev->csr, io->index, evenC);

	FD2110_VideoOutCS_W(dev->csr, io->index, ocs);
}

static void fd2110_prepare_net_in(struct forward_v4l2_io *io)
{
	struct forward_dev *dev = io->v4l2->dev;
	struct fd2110_io *io2110 = io->private;
	FD2110_VDMARxVideoControl ctl;
	FD2110_VDMARxVideoFormat fmt = FD2110_VDMARxVideoFormat_R(dev->csr, FD2110_IO_ETHERNET(io),
								  FD2110_IO_VDMA_STREAM(io));

	ctl.address = FD2110_IO_VDMA_BASE(io) / 1024 / 1024;
	ctl.size = 1;
	// FIXME: hack to get IRQ
	ctl.enable = 1;

	fmt.inBPP = io2110->stream_bpc;
	fmt.inColor = io2110->stream_color;
	switch (io->format.pixelformat) {
	case V4L2_PIX_FMT_UYVY:
	case V4L2_PIX_FMT_YUYV:
		fmt.stride = io->format.bytesperline;
		fmt.outColor = 0;
		fmt.outBPP = 0;
		break;
	case V4L2_PIX_FMT_V210:
		fmt.stride = io->format.width * 20 / 8;
		fmt.outColor = 1;
		fmt.outBPP = 1;
		break;
	}
	fmt.interlace = io->timings.sdi.interlaced;

	FD2110_VDMARxVideoControl_W(dev->csr, FD2110_IO_ETHERNET(io), FD2110_IO_VDMA_STREAM(io),
				    ctl);
	FD2110_VDMARxVideoFormat_W(dev->csr, FD2110_IO_ETHERNET(io), FD2110_IO_VDMA_STREAM(io),
				   fmt);
}

static void fd2110_prepare_net_out(struct forward_v4l2_io *io)
{
	uint32_t *csr = io->v4l2->dev->csr;
	struct fd2110_io *io2110 = io->private;
	FD2110_VDMATxVideoControl ctl;
	FD2110_VDMATxVideoFormat fmt;
	FD2110_VDMATxPacketization pkt;
	FD2110_VDMATxST2110TimingsPacket pktt;
	FD2110_VDMATxST2110TimingsField ft;
	FD2110_VDMATxST2110Timings90Field ft90;
	FD2110_VDMATxSync sync;
	FD2110_VDMATxRTPTimestamp rtpts;
	FD2110_VDMATxSchedulerCS schctl;
	FD2110_VDMATxRTPSSRC ssrc;
	int packets_per_line;
	u64 field_period, frame_period_int;
	const struct forward_v4l2_timings *t = &io->timings;
	u64 time_cur, time_start;
	int line_size_bytes;

	ctl.address = FD2110_IO_VDMA_BASE(io) / 1024 / 1024;
	ctl.destination = FD2110_IO_VDMA_STREAM(io);
	ctl.size = 1;
	ctl.enable = 0;

	fmt.stride = t->sdi.active_width;
	fmt.height = t->sdi.active_height[0] + (t->sdi.interlaced ? t->sdi.active_height[1] : 0);
	fmt.interlace = t->sdi.interlaced ? 1 : 0;

	switch (io->format.pixelformat) {
	case V4L2_PIX_FMT_V210:
		fmt.bpp = 1;
		fmt.color = 0;
		break;
	default:
		fmt.bpp = 0;
		fmt.color = 0;
		break;
	}

	switch (t->sdi.mode) {
	case FORWARD_SDI_MODE_SD_SDI:
		field_period = 11ULL * 4096ULL * t->sdi.width * t->sdi.height;
		break;
	case FORWARD_SDI_MODE_HD_SDI:
		field_period = 2ULL * 4096ULL * t->sdi.width * t->sdi.height;
		break;
	case FORWARD_SDI_MODE_3G_SDI:
		field_period = 4096ULL * t->sdi.width * t->sdi.height;
		break;
	case FORWARD_SDI_MODE_6G_SDI:
		field_period = 2048ULL * t->sdi.width * t->sdi.height;
		break;
	case FORWARD_SDI_MODE_12G_SDI:
		field_period = 1024ULL * t->sdi.width * t->sdi.height;
		break;
	default:
		field_period = 148500000ULL * 4096ULL / 25ULL;
		break;
	}
	if (t->sdi.m)
		field_period = field_period * 1001ULL / 1000ULL;

	frame_period_int = field_period / 4096;

	if (t->sdi.interlaced)
		field_period /= 2ULL;

	// TODO: Other colors
	switch (io2110->stream_bpc) {
	case V4L_FORWARD_FD2110_STREAM_BPC_10BIT:
		line_size_bytes = t->sdi.active_width * 20 / 8;
		break;
	case V4L_FORWARD_FD2110_STREAM_BPC_12BIT:
		line_size_bytes = t->sdi.active_width * 3;
		break;
	default:
		line_size_bytes = t->sdi.active_width * 2;
		break;
	}

	packets_per_line = ((line_size_bytes - 1) / 1460) + 1;
	pkt.psize = t->sdi.active_width / packets_per_line;
	pkt.bpp = (int)io2110->stream_bpc;
	pkt.color = (int)io2110->stream_color;
	pkt.pt = io2110->stream_pt;

	//pktt.period = field_period / t->sdi.active_height[0] / packets_per_line;
	pktt.period =
		field_period / (t->sdi.height / (t->sdi.interlaced ? 2 : 1)) / packets_per_line;

	ft.period = field_period / 1024;

	ft90.period = field_period / 1650 / 512;

	FD2110_VDMATxVideoFormat_W(csr, FD2110_IO_ETHERNET(io), FD2110_IO_VDMA_STREAM(io), fmt);
	FD2110_VDMATxPacketization_W(csr, FD2110_IO_ETHERNET(io), FD2110_IO_VDMA_STREAM(io), pkt);
	FD2110_VDMATxST2110TimingsPacket_W(csr, FD2110_IO_ETHERNET(io), FD2110_IO_VDMA_STREAM(io),
					   pktt);
	FD2110_VDMATxST2110TimingsField_W(csr, FD2110_IO_ETHERNET(io), FD2110_IO_VDMA_STREAM(io),
					  ft);
	FD2110_VDMATxST2110Timings90Field_W(csr, FD2110_IO_ETHERNET(io), FD2110_IO_VDMA_STREAM(io),
					    ft90);

	time_cur = (u64)FD2110_VideoClockCounterL_R(csr).counter;
	time_cur |= ((u64)FD2110_VideoClockCounterH_R(csr).counter << 32);
	// FIXME: possible issues when field_period isn't integer
	time_start = (time_cur / frame_period_int + 1) * frame_period_int;
	if ((time_start - time_cur) < 148500) // ~1ms
		time_start += frame_period_int;
	sync.time = time_start & 0xFFFFFFFF;
	rtpts.time = (time_start / 1650) & 0xFFFFFFFF;
	ssrc.ssrc = io2110->stream_ssrc;

	FD2110_VDMATxSync_W(csr, FD2110_IO_ETHERNET(io), FD2110_IO_VDMA_STREAM(io), sync);
	FD2110_VDMATxRTPTimestamp_W(csr, FD2110_IO_ETHERNET(io), FD2110_IO_VDMA_STREAM(io), rtpts);
	FD2110_VDMATxRTPSSRC_W(csr, FD2110_IO_ETHERNET(io), FD2110_IO_VDMA_STREAM(io), ssrc);
	FD2110_VDMATxVideoControl_W(csr, FD2110_IO_ETHERNET(io), FD2110_IO_VDMA_STREAM(io), ctl);

	//FIXME: hack
	schctl.enable = 1;
	FD2110_VDMATxSchedulerCS_W(csr, FD2110_IO_ETHERNET(io), schctl);

	ctl.enable = 1;
	FD2110_VDMATxVideoControl_W(csr, FD2110_IO_ETHERNET(io), FD2110_IO_VDMA_STREAM(io), ctl);
}

static void fd2110_prepare(struct forward_v4l2_io *io)
{
	if (FD2110_IO_IS_SDI(io)) {
		if (io->state == FORWARD_IO_TX)
			fd2110_prepare_sdi_out(io);
		else
			fd2110_prepare_sdi_in(io);
	} else {
		if (io->state == FORWARD_IO_TX)
			fd2110_prepare_net_out(io);
		else
			fd2110_prepare_net_in(io);
	}
}

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

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

static bool fd2110_signal_present(const struct forward_v4l2_io *io)
{
	if (FD2110_IO_IS_SDI(io)) {
		struct forward_dev *dev = io->v4l2->dev;
		FD2110_VideoInCS ics = FD2110_VideoInCS_R(dev->csr, io->index);
		return ics.cd ? true : false;
	} else {
		return false;
	}
}

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

static u32 fd2110_io_timestamp(const struct forward_v4l2_io *io)
{
	struct forward_dev *dev = io->v4l2->dev;

	if (FD2110_IO_IS_SDI(io)) {
		u32 ts;
		if (io->state == FORWARD_IO_RX)
			ts = FD2110_VideoInIRQRefTime_R(dev->csr, io->index).time;
		else
			ts = fd2110_hw_timestamp(io->v4l2);
		return ts;
	} else {
		u32 ts;
		if (io->state == FORWARD_IO_RX)
			ts = FD2110_VDMARxVideoIRQTime_R(dev->csr, FD2110_IO_ETHERNET(io),
							 FD2110_IO_VDMA_STREAM(io))
				     .time;
		else
			ts = fd2110_hw_timestamp(io->v4l2);
		return ts;
	}
}

static int fd2110_analog_rx_video_query(struct forward_v4l2 *v4l2, struct forward_v4l2_timings *t)
{
	FD2110_AnalogSync cnt = FD2110_AnalogSync_R(v4l2->dev->csr);

	if ((cnt.hsync > 2048) || (cnt.vsync > 1000000) || (cnt.hsync == 0) || (cnt.vsync == 0))
		return -ENOLINK;

	return forward_v4l2_timing_guess_analog(24000000 / cnt.hsync, 24000000000 / cnt.vsync, t) ?
		       0 :
		       -ERANGE;
}

static int fd2110_analog_rx_set_mode(struct forward_v4l2 *dev,
				     enum v4l2_forward_analog_rx_mode mode)
{
	return -EOPNOTSUPP;
}

static enum v4l2_forward_analog_rx_mode fd2110_analog_rx_get_mode(struct forward_v4l2 *dev)
{
	return V4L_FORWARD_ANALOG_RX_MODE_GENLOCK;
}

static int fd2110_analog_rx_timestamp(struct forward_v4l2 *dev, bool *valid, u32 *timestamp,
				      u32 *cur_time, u64 *sys_time)
{
	FD2110_VideoClockCounterL clk;
	*timestamp = FD2110_AnalogVSyncTime_R(dev->dev->csr).time;
	*sys_time = ktime_get_ns();
	clk = FD2110_VideoClockCounterL_R(dev->dev->csr);
	*valid = true;
	*cur_time = clk.counter;

	return 0;
}

static int fd2110_analog_rx_timecode(struct forward_v4l2 *dev, bool *valid,
				     struct v4l2_timecode *tc)
{
	return -EOPNOTSUPP;
}

static int fd2110_toggle_clone(const struct forward_v4l2_io *io, bool clone)
{
	return -EOPNOTSUPP;
}

static int fd2110_str_to_ip_port(const char *str, int maxlen, u8 *ip6addr, u16 *port, bool *v6)
{
	int i = 0;
	const char *portp;

	for (i = 0; i < 16; i++)
		ip6addr[i] = 0;
	*port = 0;

	if (str[0] == '[') {
		if (!in6_pton(&str[1], maxlen - 1, ip6addr, ']', &portp))
			return -EINVAL;

		if (((portp - str) >= maxlen) || (*portp != ']'))
			return -EINVAL;
		portp++;

		*v6 = true;
	} else {
		if (!in4_pton(str, maxlen, ip6addr, ':', &portp))
			return -EINVAL;
		for (i = 0; i < 4; i++) {
			ip6addr[i + 12] = ip6addr[i];
			ip6addr[i] = 0x00;
		}
		ip6addr[11] = 0xFF;
		ip6addr[10] = 0xFF;

		*v6 = false;
	}

	if (((portp - str) >= maxlen) || (*portp != ':'))
		return -EINVAL;
	portp++;

	if (kstrtou16(portp, 10, port))
		return -EINVAL;

	return 0;
}

static inline bool fd2110_ip_is_v6(const u8 *ipaddr)
{
	int i = 0;
	for (i = 0; i < 10; i++) {
		if (ipaddr[i] != 0x00)
			return true;
	}
	if ((ipaddr[10] != 0xFF) || (ipaddr[11] != 0xFF))
		return true;

	return false;
}

static int fd2110_v4l2_update_stream_address(struct forward_v4l2_io *io)
{
	struct fd2110_io *io2110 = io->private;

	if (FD2110_IO_IS_TX(io)) {
		if (io2110->stream_addr_valid) {
			forward_fd2110_out_mixer_update(io->v4l2->dev, FD2110_IO_ETHERNET(io),
							FD2110_IO_VDMA_STREAM(io),
							io2110->stream_own_port, io2110->stream_ip,
							io2110->stream_port,
							fd2110_ip_is_v6(io2110->stream_ip), io);
			forward_fd2110_out_mixer_toggle_en(io->v4l2->dev, FD2110_IO_ETHERNET(io),
							   FD2110_IO_VDMA_STREAM(io), true);
		} else
			forward_fd2110_out_mixer_disable_all(io->v4l2->dev, FD2110_IO_ETHERNET(io),
							     io);
	} else {
		if (io2110->stream_addr_valid)
			return forward_fd2110_in_filter_replace(
				io->v4l2->dev, FD2110_IO_ETHERNET(io), FD2110_IO_VDMA_STREAM(io), 0,
				io2110->stream_ip, io2110->stream_port, io);
		else
			forward_fd2110_in_filter_remove_all(io->v4l2->dev, FD2110_IO_ETHERNET(io),
							    io);
	}
	return 0;
}

static int fd2110_v4l2_ctrl_s(struct v4l2_ctrl *ctrl)
{
	struct forward_v4l2_io *io = ctrl->priv;
	struct fd2110_io *io2110 = io->private;
	int result = 0;

	if (ctrl->id == V4L2_CID_FORWARD_FD2110_STREAM_BPC) {
		io2110->stream_bpc = ctrl->val;
		fd2110_set_fmt(io, &io->format);
	} else if (ctrl->id == V4L2_CID_FORWARD_FD2110_STREAM_COLOR) {
		io2110->stream_color = ctrl->val;
		fd2110_set_fmt(io, &io->format);
	} else if (ctrl->id == V4L2_CID_FORWARD_FD2110_STREAM_ADDRESS_STRING) {
		u8 ipaddr[16];
		u16 port = 0;
		bool v6;

		if (strnlen(ctrl->p_new.p_char, ctrl->maximum) == 0) {
			io2110->stream_addr_valid = false;
			fd2110_v4l2_update_stream_address(io);
			return 0;
		}

		result = fd2110_str_to_ip_port(ctrl->p_new.p_char, ctrl->maximum, ipaddr, &port,
					       &v6);
		if (result)
			return result;

		memcpy(io2110->stream_ip, ipaddr, sizeof(io2110->stream_ip));
		io2110->stream_port = port;
		io2110->stream_addr_valid = true;

		fd2110_v4l2_update_stream_address(io);
	} else if (ctrl->id == V4L2_CID_FORWARD_FD2110_STREAM_ADDRESS) {
		struct v4l2_forward_stream_address *addr =
			(struct v4l2_forward_stream_address *)ctrl->p_new.p;
		bool valid = false;
		bool v6 = false;
		int i;

		for (i = 0; i < 16; i++) {
			if (addr->ipv6[i]) {
				valid = true;
				if (i < 10)
					v6 = true;
			}
		}
		v6 = v6 || (addr->ipv6[10] != 0xFF) || (addr->ipv6[11] != 0xFF);

		memcpy(io2110->stream_ip, addr->ipv6, sizeof(addr->ipv6));
		io2110->stream_port = addr->port;
		io2110->stream_addr_valid = valid;

		fd2110_v4l2_update_stream_address(io);
	} else if (ctrl->id == V4L2_CID_FORWARD_FD2110_STREAM_SOURCE_PORT) {
		io2110->stream_own_port = ctrl->val;
	} else if (ctrl->id == V4L2_CID_FORWARD_FD2110_STREAM_PT) {
		io2110->stream_pt = ctrl->val;
	} else if (ctrl->id == V4L2_CID_FORWARD_FD2110_STREAM_SSRC) {
		io2110->stream_ssrc = *ctrl->p_new.p_u32;
	} else
		result = -EINVAL;

	return result;
}

static int fd2110_v4l2_ctrl_g(struct v4l2_ctrl *ctrl)
{
	struct forward_v4l2_io *io = ctrl->priv;
	struct fd2110_io *io2110 = io->private;
	int result = 0;

	if (ctrl->id == V4L2_CID_FORWARD_FD2110_STREAM_BPC) {
		ctrl->val = io2110->stream_bpc;
	} else if (ctrl->id == V4L2_CID_FORWARD_FD2110_STREAM_COLOR) {
		ctrl->val = io2110->stream_color;
	} else if (ctrl->id == V4L2_CID_FORWARD_FD2110_STREAM_ADDRESS_STRING) {
		if (!io2110->stream_addr_valid)
			ctrl->p_new.p_char[0] = '\0';
		else if (fd2110_ip_is_v6(io2110->stream_ip))
			snprintf(ctrl->p_new.p_char, FD2110_STREAM_ADDR_LENGTH, "[%pI6c]:%d",
				 io2110->stream_ip, io2110->stream_port);
		else
			snprintf(ctrl->p_new.p_char, FD2110_STREAM_ADDR_LENGTH, "%pI4:%d",
				 &io2110->stream_ip[12], io2110->stream_port);
	} else if (ctrl->id == V4L2_CID_FORWARD_FD2110_STREAM_ADDRESS_STRING) {
		struct v4l2_forward_stream_address *addr =
			(struct v4l2_forward_stream_address *)ctrl->p_new.p;
		if (!io2110->stream_addr_valid) {
			memset(addr, 0, sizeof(struct v4l2_forward_stream_address));
		} else {
			memcpy(addr->ipv6, io2110->stream_ip, sizeof(addr->ipv6));
			addr->port = io2110->stream_port;
		}
	} else if (ctrl->id == V4L2_CID_FORWARD_FD2110_STREAM_SOURCE_PORT) {
		ctrl->val = io2110->stream_own_port;
	} else if (ctrl->id == V4L2_CID_FORWARD_FD2110_STREAM_PT) {
		ctrl->val = io2110->stream_pt;
	} else if (ctrl->id == V4L2_CID_FORWARD_FD2110_STREAM_SSRC) {
		*ctrl->p_new.p_u32 = io2110->stream_ssrc;
	} else
		result = -EINVAL;

	return result;
}

static const struct v4l2_ctrl_ops fd2110_v4l2_ctrl_ops = {
	.s_ctrl = fd2110_v4l2_ctrl_s,
	.g_volatile_ctrl = fd2110_v4l2_ctrl_g,
};

static const char *const ctrl_fd2110_stream_bpc_menu[] = {
	"8-bit",
	"10-bit",
	"12-bit",
	NULL,
};

static const struct v4l2_ctrl_config ctrl_fd2110_stream_bpc = {
	.ops = &fd2110_v4l2_ctrl_ops,
	.id = V4L2_CID_FORWARD_FD2110_STREAM_BPC,
	.name = "Stream bits per component",
	.type = V4L2_CTRL_TYPE_MENU,
	.min = 0,
	.max = 2,
	.def = V4L_FORWARD_FD2110_STREAM_BPC_10BIT,
	.qmenu = ctrl_fd2110_stream_bpc_menu,
};

static const char *const ctrl_fd2110_stream_color_menu[] = {
	"YCbCr 4:2:2", "YCbCr 4:2:0", "YCbCr 4:4:4", "RGB", NULL,
};

static const struct v4l2_ctrl_config ctrl_fd2110_stream_color = {
	.ops = &fd2110_v4l2_ctrl_ops,
	.id = V4L2_CID_FORWARD_FD2110_STREAM_COLOR,
	.name = "Stream color mode",
	.type = V4L2_CTRL_TYPE_MENU,
	.min = 0,
	.max = 3,
	.def = V4L_FORWARD_FD2110_STREAM_COLOR_YCBCR422,
	.qmenu = ctrl_fd2110_stream_color_menu,
};

static const struct v4l2_ctrl_config ctrl_fd2110_stream_addr_str = {
	.ops = &fd2110_v4l2_ctrl_ops,
	.id = V4L2_CID_FORWARD_FD2110_STREAM_ADDRESS_STRING,
	.name = "Stream address",
	.type = V4L2_CTRL_TYPE_STRING,
	.min = 0,
	.max = FD2110_STREAM_ADDR_LENGTH,
	.step = 1,
};

static const struct v4l2_ctrl_config ctrl_fd2110_stream_addr = {
	.ops = &fd2110_v4l2_ctrl_ops,
	.id = V4L2_CID_FORWARD_FD2110_STREAM_ADDRESS,
	.name = "Stream address bin",
	.type = V4L2_CTRL_TYPE_FORWARD_STREAM_ADDRESS,
	.elem_size = sizeof(struct v4l2_forward_stream_address)
};

static const struct v4l2_ctrl_config ctrl_fd2110_stream_own_port = {
	.ops = &fd2110_v4l2_ctrl_ops,
	.id = V4L2_CID_FORWARD_FD2110_STREAM_SOURCE_PORT,
	.name = "Stream Source Port",
	.type = V4L2_CTRL_TYPE_INTEGER,
	.min = 1,
	.max = 65534,
	.def = 10000,
	.step = 1,
};

static const struct v4l2_ctrl_config ctrl_fd2110_stream_pt = {
	.ops = &fd2110_v4l2_ctrl_ops,
	.id = V4L2_CID_FORWARD_FD2110_STREAM_PT,
	.name = "Stream Payload Type",
	.type = V4L2_CTRL_TYPE_INTEGER,
	.min = 0,
	.max = 127,
	.def = 96,
	.step = 1,
};

static const struct v4l2_ctrl_config ctrl_fd2110_stream_ssrc = {
	.ops = &fd2110_v4l2_ctrl_ops,
	.id = V4L2_CID_FORWARD_FD2110_STREAM_SSRC,
	.name = "Stream SSRC",
	.type = V4L2_CTRL_TYPE_INTEGER,
	.min = INT_MIN,
	.max = INT_MAX,
	.def = 0x0,
	.step = 1
};

static int fd2110_init_io(struct forward_v4l2_io *io)
{
	struct fd2110_io *fd2110;
	struct v4l2_ctrl *c;

	if (FD2110_IO_IS_SDI(io))
		return 0;

	fd2110 = devm_kzalloc(io->v4l2->dev->dev, sizeof(struct fd2110_io), GFP_KERNEL);
	if (!fd2110)
		return -ENOMEM;
	fd2110->io = io;
	io->private = fd2110;

	fd2110->stream_bpc = V4L_FORWARD_FD2110_STREAM_BPC_10BIT;
	fd2110->stream_color = V4L_FORWARD_FD2110_STREAM_COLOR_YCBCR422;
	fd2110->stream_pt = 96;
	fd2110->stream_own_port = 10000;

	c = v4l2_ctrl_new_custom(&io->video.ctl, &ctrl_fd2110_stream_bpc, io);
	c = v4l2_ctrl_new_custom(&io->video.ctl, &ctrl_fd2110_stream_color, io);

	c = v4l2_ctrl_new_custom(&io->video.ctl, &ctrl_fd2110_stream_addr_str, io);
	if (c)
		c->flags |= V4L2_CTRL_FLAG_UPDATE | V4L2_CTRL_FLAG_VOLATILE |
			    V4L2_CTRL_FLAG_EXECUTE_ON_WRITE;

	c = v4l2_ctrl_new_custom(&io->video.ctl, &ctrl_fd2110_stream_addr, io);
	if (c)
		c->flags |= V4L2_CTRL_FLAG_UPDATE | V4L2_CTRL_FLAG_VOLATILE |
			    V4L2_CTRL_FLAG_EXECUTE_ON_WRITE;

	if (FD2110_IO_IS_TX(io)) {
		c = v4l2_ctrl_new_custom(&io->video.ctl, &ctrl_fd2110_stream_own_port, io);
		c = v4l2_ctrl_new_custom(&io->video.ctl, &ctrl_fd2110_stream_pt, io);
		c = v4l2_ctrl_new_custom(&io->video.ctl, &ctrl_fd2110_stream_ssrc, io);
	}

	if (io->video.ctl.error)
		return -EINVAL;

	return 0;
}

static void fd2110_fini_io(struct forward_v4l2_io *io)
{
	struct fd2110_io *io2110 = io->private;

	if (FD2110_IO_IS_SDI(io))
		return;

	forward_fd2110_in_filter_remove_all(io->v4l2->dev, FD2110_IO_ETHERNET(io), io);
	forward_fd2110_out_mixer_disable_all(io->v4l2->dev, FD2110_IO_ETHERNET(io), io);

	if (io2110)
		devm_kfree(io->v4l2->dev->dev, io->private);

	io->private = NULL;
}

struct forward_v4l2_dev_ops fd2110_ops = {
	.init_io = fd2110_init_io,
	.fini_io = fd2110_fini_io,
	.irq_mask = fd2110_irq_mask,
	.irq_info = fd2110_irq_info,
	.region_info = fd2110_region_info,
	.buffer_info = fd2110_buffer_info,
	.timings_valid = fd2110_timings_valid,
	.timings_set = fd2110_timings_set,
	.timings_query = fd2110_timings_query,
	.timings_changed = fd2110_timings_changed,
	.check_fmt = fd2110_check_fmt,
	.enum_fmt = fd2110_enum_fmt,
	.set_fmt = fd2110_set_fmt,
	.update_meta = fd2110_update_meta,
	.update_timecode = fd2110_update_timecode,
	.prepare = fd2110_prepare,
	.start = fd2110_start,
	.stop = fd2110_stop,
	.signal_present = fd2110_signal_present,
	.hw_buffers = FD2110_NUMBER_IN_BUFFERS,
	.vbi_support = true,

	.io_timestamp = fd2110_io_timestamp,
	.hw_timestamp = fd2110_hw_timestamp,

	.toggle_clone = fd2110_toggle_clone,

	.analog_rx_set_mode = fd2110_analog_rx_set_mode,
	.analog_rx_get_mode = fd2110_analog_rx_get_mode,
	.analog_rx_timestamp = fd2110_analog_rx_timestamp,
	.analog_rx_timecode = fd2110_analog_rx_timecode,
	.analog_rx_video_query = fd2110_analog_rx_video_query,
};
