/*
   forward-passthrough-fd788.c - passthrough driver for FD788

   Copyright (C) 2017 - 2023 Konstantin Oblaukhov <oblaukhov@sl.iae.nsk.su>

   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-passthrough.h"
#include "forward-pll.h"
#include "fd788_reg.h"

#include <linux/types.h>
#include <linux/delay.h>

#define FD788_BUFFER_NUMBER(in, irq) (((irq) >> ((in)*3 + 1)) & 0x3)
#define FD788_BUFFER_NEXT(buf, offset, count) ((((unsigned int)(buf)) + (offset)) % (count))
#define FD788_NUMBER_IN_BUFFERS (4)
#define FD788_NUMBER_OUT_BUFFERS (2)

#define FD788_GENLOCK_ANALOG_DELAY 306
#define FD788_GENLOCK_SD_NTSC_DELAY 27981
#define FD788_GENLOCK_SD_PAL_DELAY 364
#define FD788_GENLOCK_HD_DELAY 193
#define FD788_GENLOCK_3G_DELAY 101

#define FD788_IS_VERSION_2(ver) (((ver >> 24) & 0xF) >= 2)

static u32 fd788_irq_mask(struct forward_passthrough_io *io)
{
	return (1 << (io->in_index * 3)) | (1 << (io->out_index * 3));
}

static bool fd788_irq_info_in(struct forward_passthrough_io *io, u32 irq, int *prev, int *cur,
			      int *next, u32 *in_a_size)
{
	FD788_VideoInANCCounter anc;
	if (!(irq & (1 << io->in_index * 3)))
		return false;

	*cur = FD788_BUFFER_NUMBER(io->in_index, irq);
	*prev = FD788_BUFFER_NEXT(*cur, -1, FD788_NUMBER_IN_BUFFERS);
	*next = FD788_BUFFER_NEXT(*cur, 1, FD788_NUMBER_IN_BUFFERS);

	anc = FD788_VideoInANCCounter_R(io->pass->dev->csr, io->in_index);
	*in_a_size = anc.counter;

	if (!(*cur & 0x1))
		io->last_in_hw_timestamp =
			FD788_VideoInIRQRefTime_R(io->pass->dev->csr, io->out_index).time;

	return true;
}

static bool fd788_irq_info_out(struct forward_passthrough_io *io, u32 irq, int *prev, int *cur,
			       int *next, u32 *audio_ptr)
{
	FD788_VideoOutAudio audio;

	if (!(irq & (1 << io->out_index * 3)))
		return false;

	*cur = FD788_BUFFER_NUMBER(io->out_index, irq);
	*prev = FD788_BUFFER_NEXT(*cur, -1, FD788_NUMBER_OUT_BUFFERS);
	*next = FD788_BUFFER_NEXT(*cur, 1, FD788_NUMBER_OUT_BUFFERS);

	audio = FD788_VideoOutAudio_R(io->pass->dev->csr, io->out_index);
	*audio_ptr = audio.address / 4;

	return true;
}

static bool fd788_buffer_map_address(struct forward_passthrough_io *io, int buffer, bool out,
				     u32 *video, u32 *audio)
{
	*video = (out ? io->out_index : io->in_index) * 0x08000000UL + buffer * 0x01000000UL;
	*audio = (out ? io->out_index : io->in_index) * 0x08000000UL + 0x04000000UL +
		 (out ? 0 : (buffer * 0x00100000UL));

	return true;
}

static void fd788_region_info(struct forward_passthrough_io *io, bool out, u32 *v_address,
			      size_t *v_size, u32 *a_address, size_t *a_size)
{
	*v_size = 0x04000000UL;
	*v_address = (out ? io->out_index : io->in_index) * 0x08000000UL;
	*a_size = 0x00400000UL;
	*a_address = (out ? io->out_index : io->in_index) * 0x08000000UL + 0x04000000UL;
}

static bool fd788_query_in_format(struct forward_passthrough_io *io,
				  struct forward_passthrough_format *fmt)
{
	struct forward_dev *dev = io->pass->dev;
	FD788_VideoInCS ics = FD788_VideoInCS_R(dev->csr, io->in_index);
	FD788_VideoInFrameSize framesize = FD788_VideoInFrameSize_R(dev->csr, io->in_index);
	FD788_VideoInLine line;
	FD788_VideoInPixel pixel;
	FD788_VideoInVPID vpid = FD788_VideoInVPID_R(dev->csr, io->in_index);
	u64 pixel_clk, real_pixel_clk;
	int width, height, active_width, active_height;

	if (!ics.cd)
		return false;

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

	switch (ics.mode) {
	case 0:
		pixel_clk = 74250000;
		break;
	case 1:
		pixel_clk = 13500000;
		break;
	case 2:
		pixel_clk = 148500000;
		break;
	default:
		pixel_clk = 0;
		break;
	}

	line = FD788_VideoInLine_R(dev->csr, io->in_index);
	pixel = FD788_VideoInPixel_R(dev->csr, io->in_index);
	if (!framesize.size)
		framesize.size = 1;
	width = pixel.totalPixels;
	height = line.totalLines;
	active_width = pixel.activePixels;
	active_height = line.activeLines * (ics.progressive ? 1 : 2);

	real_pixel_clk = 27000000ULL * width * height / framesize.size;

	if (!forward_passthrough_format_guess(pixel_clk, !ics.progressive, width, height,
					      active_width, active_height, real_pixel_clk, fmt))
		return false;

	fmt->vpid = vpid.vpid;
	fmt->size[0] =
		((fmt->height / (fmt->interlaced ? 2 : 1)) + fmt->topVBIs[0]) * fmt->width * 20 / 8;
	if (fmt->interlaced)
		fmt->size[1] = ((fmt->height / 2) + fmt->topVBIs[1]) * fmt->width * 20 / 8;
	else
		fmt->size[1] = fmt->size[0];

	return true;
}

static void fd788_setup_out_format(struct forward_passthrough_io *io,
				   struct forward_passthrough_format *fmt)
{
	struct forward_dev *dev = io->pass->dev;
	FD788_VideoOutCS ocs = FD788_VideoOutCS_R(dev->csr, io->out_index);
	FD788_VideoOutLine line;
	FD788_VideoOutPixel pixel;
	FD788_VideoOutStart start;
	FD788_VideoOutStop stop;
	FD788_VideoOutField field;
	FD788_VideoOutVPID vpid;

	ocs.reset = 1;
	ocs.levelB = 0;
	ocs.playbackRaw = 0;
	ocs.vbiTop = 1;
	ocs.vbiBottom = 0;
	ocs.vis10b = 1;
	ocs.vbi10b = 1;
	ocs.dataPresent = 1;
	ocs.atcEnable = 0;
	ocs.atcType = 0;
	ocs.dqQuad = 0;
	ocs.dqEnable = 0;
	ocs.clone = 0;
	ocs.test = 0;

	FD788_VideoOutCS_W(dev->csr, io->out_index, ocs);

	switch (fmt->pixelclock) {
	case 13500000:
		ocs.mode = 1;
		break;
	case 74250000:
		ocs.mode = 0;
		break;
	default:
		ocs.mode = 2;
		break;
	}

	ocs.progressive = fmt->interlaced ? 0 : 1;
	ocs.audioCount = io->a_out_groups - 1;

	line.totalLines = fmt->total_height;
	pixel.totalPixels = fmt->total_width;
	pixel.activePixels = fmt->width;
	start.startOdd = fmt->topVBIs[0] - 1;
	start.startEven = fmt->field_switch[1] + fmt->topVBIs[1];
	stop.stopOdd = start.startOdd + fmt->height / (fmt->interlaced ? 2 : 1);
	stop.stopEven = start.startEven + fmt->height / (fmt->interlaced ? 2 : 1);
	field.switchOdd = fmt->field_switch[0];
	field.switchEven = fmt->field_switch[1];
	vpid.vpid = fmt->vpid;

	FD788_VideoOutLine_W(dev->csr, io->out_index, line);
	FD788_VideoOutPixel_W(dev->csr, io->out_index, pixel);
	FD788_VideoOutStart_W(dev->csr, io->out_index, start);
	FD788_VideoOutStop_W(dev->csr, io->out_index, stop);
	FD788_VideoOutField_W(dev->csr, io->out_index, field);
	FD788_VideoOutVPID_W(dev->csr, io->out_index, vpid);
	FD788_VideoOutCS_W(dev->csr, io->out_index, ocs);

	ocs.reset = 0;
	FD788_VideoOutCS_W(dev->csr, io->out_index, ocs);
}

static void fd788_update_io_state(struct forward_passthrough_io *io)
{
	struct forward_dev *dev = io->pass->dev;
	FD788_VideoInCS ics = FD788_VideoInCS_R(dev->csr, io->in_index);

	io->no_input_signal = !ics.modeLocked || !ics.formatLocked;
}

static void fd788_configure_io(struct forward_passthrough_io *io)
{
	//if (!fd788_query_in_format(io, &io->format))
	//	return;

	// 576i50, see forward-passthrough-formats.c
	io->format = forward_passthrough_formats[FORWARD_FORMAT_SD_576I50];
	fd788_setup_out_format(io, &io->format);
	io->configured = true;
}

static void fd788_configure_genlock(struct forward_passthrough_io *io, bool analog)
{
	struct forward_passthrough *pass = io->pass;
	struct forward_pll *pll = pass->dev->pll;
	FD788_SyncGenerator sg = { 0 };
	FD788_SyncGeneratorField sgf = { 0 };
	struct forward_passthrough_format *fmt = &io->format;
	struct forward_pll_mode *mode;
	FD788_Genlock gl;
	int pll_in_h_freq = 0, pll_in_p_freq = 0;
	int i;
	bool v2 = FD788_IS_VERSION_2(io->pass->dev->fw_version);

	if (!io->configured)
		return;

	gl = FD788_Genlock_R(pass->dev->csr);

	if (analog) {
		pll_in_h_freq = fmt->pixelclock / fmt->total_width;
		gl.mode = 0;
	} else {
		if (io->format.pixelclock == 13500000)
			pll_in_p_freq = v2 ? 13500000 : 6750000;
		else
			pll_in_p_freq = (int)io->format.pixelclock;
		gl.mode = io->in_index ? 3 : 2;
	}

	mode = pll->ops->find_mode(pll, pll_in_p_freq, pll_in_h_freq, fmt->m, 148500000, fmt->m);
	if (!mode)
		return;

	FD788_Genlock_W(pass->dev->csr, gl);

	pll->ops->switch_mode(pll, mode);

	if (v2) {
		int input = 0;
		if (analog)
			input = 0;
		else
			input = io->in_index ? 2 : 1;
		pll->ops->select_input(pll, input, false, true);
		pll->ops->calibrate(pll);
	} else {
		if (!analog) {
			// Hack to avoid PLL->IN->PLL loop stuck
			FD788_VideoInCS cs;
			int count = 0;

			// Force Master clock
			pll->ops->select_input(pll, 1, false, false);
			pll->ops->calibrate(pll);

			// Wait for input
			do {
				msleep(100);
				cs = FD788_VideoInCS_R(io->pass->dev->csr, io->in_index ? 1 : 0);
				count++;
			} while ((!cs.modeLocked) && (count < 100));

			// Switch to input clock
			pll->ops->select_input(pll, 0, false, true);
		} else {
			pll->ops->select_input(pll, 0, false, true);
			pll->ops->calibrate(pll);
		}
	}

	sg.frameSize = fmt->total_width * fmt->total_height;
	if (fmt->pixelclock == 13500000)
		sg.frameSize *= 11;
	else if (fmt->pixelclock == 74250000)
		sg.frameSize *= 2;
	if (fmt->interlaced)
		sgf.fieldSize = sg.frameSize / 2;
	else
		sgf.fieldSize = sg.frameSize;

	if (!sg.frameSize || !sgf.fieldSize) {
		sg.frameSize = 5940000;
		sgf.fieldSize = 2970000;
	}

	FD788_SyncGenerator_W(pass->dev->csr, 0, sg);
	FD788_SyncGeneratorField_W(pass->dev->csr, 0, sgf);

	for (i = 0; i < pass->ops->num_io; i++) {
		struct forward_passthrough_io *o = &pass->io[i];
		FD788_VideoOutCS cs = FD788_VideoOutCS_R(pass->dev->csr, o->out_index);
		cs.syncSource = 0;
		cs.freeRunning = 0;
		FD788_VideoOutCS_W(pass->dev->csr, o->out_index, cs);
	}
}

static void fd788_genlock_offset(struct forward_passthrough_io *io, s32 offset)
{
	struct forward_passthrough *pass = io->pass;
	struct forward_passthrough_format *fmt = &io->format;

	s32 frame_size = fmt->total_height * fmt->total_width;
	FD788_SyncGeneratorPhase ph;
	s32 sync_gen_offset = 0;

	if (pass->analog_master) {
		if (fmt->total_height == 1125)
			sync_gen_offset = -(fmt->total_width - fmt->width - 192);
		else if (fmt->total_height == 750)
			sync_gen_offset = -(fmt->total_width - fmt->width - 260);
		else if (fmt->total_height == 625)
			sync_gen_offset = -12 - 46;
		else if (fmt->total_height == 525)
			sync_gen_offset = -16 - 44;

		sync_gen_offset += -fmt->total_width;
		if (!fmt->interlaced)
			sync_gen_offset += -fmt->total_width;
	}

	if (fmt->pixelclock == 13500000) {
		frame_size *= 11;
		sync_gen_offset *= 11;
	} else if (fmt->pixelclock == 74250000) {
		frame_size *= 2;
		sync_gen_offset *= 2;
	}

	if (pass->analog_master)
		sync_gen_offset += -FD788_GENLOCK_ANALOG_DELAY;
	else {
		if (fmt->pixelclock == 13500000)
			sync_gen_offset += (fmt->total_height == 525) ?
						   -FD788_GENLOCK_SD_NTSC_DELAY :
						   -FD788_GENLOCK_SD_PAL_DELAY;
		else if (fmt->pixelclock == 74250000)
			sync_gen_offset += -FD788_GENLOCK_HD_DELAY;
		else
			sync_gen_offset += -FD788_GENLOCK_3G_DELAY;
	}

	offset += sync_gen_offset;

	if (offset > frame_size)
		offset = offset % frame_size;
	if (offset < 0)
		offset = (offset % frame_size) + frame_size;

	ph.phase = offset;
	FD788_SyncGeneratorPhase_W(pass->dev->csr, 0, ph);
}

u32 fd788_genlock_irq_mask(struct forward_passthrough_io *io)
{
	return 1 << 24;
}

bool fd788_genlock_irq_info(struct forward_passthrough_io *io, u32 irq, bool *field, s32 *gl_offset)
{
	struct forward_passthrough *pass = io->pass;
	u32 ref_time = 0, sync_time = 0;

	if (!(irq & (1 << 24)))
		return false;

	*field = (irq & (1 << 25)) ? true : false;
	sync_time = FD788_SyncGeneratorTime_R(pass->dev->csr, 0).time;

	if (pass->analog_master)
		ref_time = FD788_AnalogVSyncTime_R(pass->dev->csr).time;
	else
		ref_time = io->last_in_hw_timestamp;

	*gl_offset = (s32)(sync_time - ref_time);

	return true;
}

static void fd788_toggle_streaming(struct forward_passthrough_io *io, bool in_enable,
				   bool out_enable)
{
	if (in_enable) {
		FD788_VideoInCS ics = FD788_VideoInCS_R(io->pass->dev->csr, io->in_index);
		ics.vbiTop = 1;
		ics.vbiBottom = 0;
		ics.test = 0;
		ics.vbi10b = 1;
		ics.vis10b = 1;
		ics.captureRaw = io->raw ? 1 : 0;
		FD788_VideoInCS_W(io->pass->dev->csr, io->in_index, ics);
	}

	if (out_enable) {
		FD788_VideoOutOddDataCount oddC;
		FD788_VideoOutEvenDataCount evenC;
		FD788_VideoOutCS ocs = FD788_VideoOutCS_R(io->pass->dev->csr, io->out_index);
		struct forward_passthrough_format *fmt = &io->format;

		ocs.vbiTop = 1;
		ocs.vbiBottom = 0;
		ocs.test = 0;
		ocs.vbi10b = 1;
		ocs.vis10b = 1;
		ocs.playbackRaw = io->raw ? 1 : 0;

		if (io->raw) {
			oddC.count = fmt->raw_size[0];
			evenC.count = fmt->raw_size[1];
		} else {
			oddC.count = fmt->size[0];
			evenC.count = fmt->size[1];
		}

		FD788_VideoOutOddDataCount_W(io->pass->dev->csr, io->out_index, oddC);
		FD788_VideoOutEvenDataCount_W(io->pass->dev->csr, io->out_index, evenC);
		FD788_VideoOutCS_W(io->pass->dev->csr, io->out_index, ocs);
	}

	io->pass->dev->cfg.toggle_streaming(io->pass->dev, io->in_index, in_enable);
	io->pass->dev->cfg.toggle_streaming(io->pass->dev, io->out_index, out_enable);
}

static void fd788_update_meta(struct forward_passthrough_io *io)
{
	FD788_VideoInVPID ivpid;
	FD788_VideoOutVPID ovpid;

	ivpid = FD788_VideoInVPID_R(io->pass->dev->csr, io->in_index);
	if (ivpid.vpid != 0) {
		ovpid.vpid = ivpid.vpid;
		FD788_VideoOutVPID_W(io->pass->dev->csr, io->out_index, ovpid);
	}
}

struct forward_passthrough_dev_ops fd788_ops = {
	.irq_mask = fd788_irq_mask,
	.irq_info_in = fd788_irq_info_in,
	.irq_info_out = fd788_irq_info_out,

	.region_info = fd788_region_info,
	.buffer_map_address = fd788_buffer_map_address,

	.update_io_state = fd788_update_io_state,
	.configure_io = fd788_configure_io,
	.configure_genlock = fd788_configure_genlock,
	.genlock_irq_mask = fd788_genlock_irq_mask,
	.genlock_irq_info = fd788_genlock_irq_info,
	.genlock_offset = fd788_genlock_offset,
	.toggle_streaming = fd788_toggle_streaming,

	.update_meta = fd788_update_meta,

	.num_io = 4,
	.num_in_buffers = 4,
	.num_out_buffers = 2,
	.video_buffer_size = 16 * 1024 * 1024,
	.audio_buffer_size = 1 * 1024 * 1024,
};
