/*
   forward-passthrough.c - example passthrough driver for SoftLab-NSK Forward boards

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

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/mm.h>
#include <linux/uaccess.h>
#include <linux/poll.h>
#include <linux/kthread.h>
#include <linux/delay.h>
#include "forward-vdma.h"

/**
 * Brief FD7xx and FD9xx hardware workflow:
 *   - Boards have access to host memory using special scatter-gather video DMA (VDMA),
 *     which maps scattered pages of host memory into linear address space for device.
 *   - Each board has own defined memory map - where to place video data, where to place ANC data, etc.
 *   - Board places/takes video data directly into/from host memory, without any instructions from host.
 *   - On input, board have 4 or 8 (FD722) video buffers, in which it places video data from SDI.
 *     One buffer = one field (for interlaced) or frame (progressive).
 *     Buffers used as ring buffer, i.e. first frame placed into buffer 0, next - 1, 2, 3, 0, 1, 2 ....
 *     Odd (first, top) field always placed in buffers 0, 2, 4, 6, even (second, bottom) - 1, 3, 5, 7
 *   - On output, board defines 2 video buffers which works same manner as inputs.
 *   - This driver captures and outputs video in 10-bit mode, with top VANC lines, or full-frame 10-bit RAW mode 
 *   - If not in RAW mode, on input ancillary data is placed into separate buffers, same number and order as video
 *   - If not in RAW mode, ancillary data captured as-is from SDI stream, audio extracted by software
 *   - If not in RAW mode, on output, audio is encoded into ancillary data by hardware, so, audio passed as raw samples in single buffer
 * 	 - If in RAW mode, the whole SDI frame is passed into/from video buffer memory
 *   - Board generates IRQ at each field on input and output
 *   - Boards have special jitter-attenuator/PLL IC which "cleans" and converts analog h-sync signal
 *     or digital pixel clock signal into output pixel clock.
 *     Each board has only single PLL and single output clock, so all outputs can be synced only to single input
 *   - Board can be synced to Analog input, IN1, IN2
 */

/**
 * Driver uses following 4:2:2 10-bit pixel format:
 *     Bytes: |0     7|0     7|0     7|0     7|0     7|
 *     YCbCr: |  Cb0/1  |   Y0    |  Cr0/1  |    Y1   |
 *     Bits   |0      10|0      10|0      10|0      10|
 * Each 4 10-bit values (2 pixels) packed into 5 bytes
 */

/**
 * Driver uses S32_LE audio frame format
 */

#define FORWARD_PASSTHROUGH_MODULE_NAME "forward-passthrough"
#define FORWARD_PASSTHROUGH_MODE_NAME "passthrough"

extern struct forward_passthrough_dev_ops fd788_ops;
extern struct forward_passthrough_dev_ops fd722_ops;

/**
 * Main processing function for RAW mode, place processing code here
 * @field - current frame field (false - first/odd/top, true - second/even/bottom)
 * @buffer - input (and next output) video buffer, 10-bit full-frame, geometry and sizes can be taken from io->format
 */
static void forward_passthrough_data_process_raw(struct forward_passthrough_io *io, bool field,
						 void *buffer)
{
	struct forward_passthrough_format *fmt = &io->format;
	size_t size = fmt->raw_size[field ? 1 : 0];
	int hanc_size = fmt->total_width - fmt->width; // HANC area size

	int x = ((ktime_get_ns() / 1000000) % fmt->width) & 0xFFFFFFFC;
	int y = ((ktime_get_ns() / 1000000) / fmt->width) % (fmt->height / 2 - 16) +
		fmt->topVBIs[field];
	int i, j;

	// Draw 16x16 moving rect
	for (i = y; i < y + 16; i++) {
		for (j = x; j < x + 16; j += 4) {
			// Process 4 pixels = 40 bits = 5 bytes
			uint64_t *p = (uint64_t *)&(
				(u8 *)buffer)[(i * fmt->total_width + j + hanc_size) * 20 / 8];
			*p = (*p & 0xFFFFFF0000000000ULL) | 0xFE200FE200;
		}
	}
}

/**
 * Copy audio to output buffer
 * Hardware uses circular buffer with 24-bit frames in lower 32-bit words on output.
 * Software uses S32_LE frames, so memcpy can't be used.
 */
static void forward_passthrough_out_audio(struct forward_passthrough_io *io, u32 *in_a_buffer,
					  u32 out_a_offset, u32 a_size)
{
	u32 *a_out_p = &((u32 *)io->a_out_buffer)[out_a_offset];
	bool a_out_wrap = (out_a_offset + a_size) > (io->a_out_buffer_size / 4);
	int a_out_free = a_out_wrap ? ((io->a_out_buffer_size / 4) - out_a_offset) : a_size;
	int a_out_left = a_size - a_out_free;
	int i;

	for (i = 0; i < a_out_free; i++)
		a_out_p[i] = in_a_buffer[i] >> 8;

	a_out_p = &((u32 *)io->a_out_buffer)[0];
	for (i = 0; i < a_out_left; i++)
		a_out_p[i] = in_a_buffer[a_out_free + i] >> 8;
}

/**
 * Main processing function, place processing code here
 * @field - current frame field (false - first/odd/top, true - second/even/bottom)
 * @in_v_buffer - input video buffer, 10-bit with VBI lines, geometry and sizes can be taken from io->format
 * @in_a_buffer - input audio buffer, with raw frames in S32_LE
 * @out_v_buffer - output video buffer, same format and size as input
 * @out_a_offset - position in output audio buffer
 * @a_size - number of audio frames in field/frame
 */
static void forward_passthrough_data_process(struct forward_passthrough_io *io, bool field,
					     void *in_v_buffer, u32 *in_a_buffer,
					     void *out_v_buffer, u32 out_a_offset, u32 a_size)
{
	struct forward_passthrough_format *fmt = &io->format;
	size_t v_size = fmt->size[field ? 1 : 0];

	// Just copy input -> output
	memcpy(out_v_buffer, in_v_buffer, v_size);
	forward_passthrough_out_audio(io, in_a_buffer, out_a_offset, a_size);
}

/**
 * Setup DMA and enable playback/capture
 */
static void forward_passthrough_toggle_streaming(struct forward_passthrough_io *io, bool enable)
{
	const struct forward_passthrough_dev_ops *ops = io->pass->ops;

	if (enable && !io->streaming) {
		u32 v_region_address, a_region_address;
		size_t v_region_size, a_region_size;
		int i;
		u32 audio_addr, video_addr;

		ops->region_info(io, true, &v_region_address, &v_region_size, &a_region_address,
				 &a_region_size);
		forward_vdma_get_region(io->pass->dev->vdma, io, v_region_address, v_region_size);
		forward_vdma_get_region(io->pass->dev->vdma, io, a_region_address, a_region_size);

		ops->region_info(io, false, &v_region_address, &v_region_size, &a_region_address,
				 &a_region_size);
		forward_vdma_get_region(io->pass->dev->vdma, io, v_region_address, v_region_size);
		forward_vdma_get_region(io->pass->dev->vdma, io, a_region_address, a_region_size);

		if (!io->raw)
			io->v_buffer_size = max(io->format.size[0], io->format.size[1]);
		else
			io->v_buffer_size = max(io->format.raw_size[0], io->format.raw_size[1]);
		io->v_buffer_size = (((io->v_buffer_size - 1) >> PAGE_SHIFT) + 1) << PAGE_SHIFT;

		if (!io->raw) {
			io->a_in_buffer_size = ops->audio_buffer_size;
			io->a_in_buffer_size = (((io->a_in_buffer_size - 1) >> PAGE_SHIFT) + 1)
					       << PAGE_SHIFT;

			io->a_out_buffer_size = ops->audio_buffer_size;
			io->a_out_buffer_size = (((io->a_out_buffer_size - 1) >> PAGE_SHIFT) + 1)
						<< PAGE_SHIFT;

			for (i = 0; i < 2; i++) {
				io->a_in_decode_buffers[i] = vmalloc(io->a_in_buffer_size);
				io->a_in_decoded_buffers[i] = vmalloc(io->a_in_buffer_size);
			}
		}

		for (i = 0; i < ops->num_in_buffers; i++) {
			io->v_in_buffers[i] = vmalloc(io->v_buffer_size);
			if (!io->raw)
				io->a_in_buffers[i] = vmalloc(io->a_in_buffer_size);
			if (ops->buffer_map_address(io, i, false, &video_addr, &audio_addr)) {
				forward_vdma_map_kernel_buf(io->pass->dev->vdma, io,
							    io->v_in_buffers[i], video_addr,
							    io->v_buffer_size, true);
				if (!io->raw)
					forward_vdma_map_kernel_buf(io->pass->dev->vdma, io,
								    io->a_in_buffers[i], audio_addr,
								    io->a_in_buffer_size, true);
			}
		}

		if (!io->raw) {
			for (i = 0; i < ops->num_out_buffers; i++) {
				io->v_out_buffers[i] = vmalloc(io->v_buffer_size);
				if (ops->buffer_map_address(io, i, true, &video_addr,
							    &audio_addr)) {
					forward_vdma_map_kernel_buf(io->pass->dev->vdma, io,
								    io->v_out_buffers[i],
								    video_addr, io->v_buffer_size,
								    false);
				}
			}
			io->a_out_buffer = vmalloc(io->a_out_buffer_size);
			if (ops->buffer_map_address(io, i, true, &video_addr, &audio_addr)) {
				forward_vdma_map_kernel_buf(io->pass->dev->vdma, io,
							    io->a_out_buffer, audio_addr,
							    io->a_out_buffer_size, false);
			}
		}

		// Toggle only input for first frame to avoid output garbage
		ops->toggle_streaming(io, true, false);

		io->streaming = true;
	} else if (!enable && io->streaming) {
		u32 v_region_address, a_region_address;
		size_t v_region_size, a_region_size;
		int i;
		u32 audio_addr, video_addr;

		io->streaming = false;
		ops->toggle_streaming(io, false, false);

		for (i = 0; i < ops->num_in_buffers; i++) {
			if (ops->buffer_map_address(io, i, false, &video_addr, &audio_addr)) {
				forward_vdma_unmap_buf(io->pass->dev->vdma, io, video_addr,
						       io->v_buffer_size);
				if (!io->raw)
					forward_vdma_unmap_buf(io->pass->dev->vdma, io, audio_addr,
							       io->a_in_buffer_size);
			}

			vfree(io->v_in_buffers[i]);
			io->v_in_buffers[i] = NULL;
			vfree(io->a_in_buffers[i]);
			io->a_in_buffers[i] = NULL;
		}
		for (i = 0; i < ops->num_out_buffers; i++) {
			if (ops->buffer_map_address(io, i, true, &video_addr, &audio_addr)) {
				forward_vdma_unmap_buf(io->pass->dev->vdma, io, video_addr,
						       io->v_buffer_size);
			}

			vfree(io->v_out_buffers[i]);
			io->v_out_buffers[i] = NULL;
		}
		if (!io->raw) {
			if (ops->buffer_map_address(io, i, true, &video_addr, &audio_addr)) {
				forward_vdma_unmap_buf(io->pass->dev->vdma, io, audio_addr,
						       io->a_out_buffer_size);
			}
		}

		vfree(io->a_out_buffer);
		io->a_out_buffer = NULL;

		for (i = 0; i < 2; i++) {
			vfree(io->a_in_decoded_buffers[i]);
			io->a_in_decoded_buffers[i] = NULL;

			vfree(io->a_in_decode_buffers[i]);
			io->a_in_decode_buffers[i] = NULL;
		}

		ops->region_info(io, true, &v_region_address, &v_region_size, &a_region_address,
				 &a_region_size);
		forward_vdma_put_region(io->pass->dev->vdma, io, v_region_address);
		forward_vdma_put_region(io->pass->dev->vdma, io, a_region_address);

		ops->region_info(io, false, &v_region_address, &v_region_size, &a_region_address,
				 &a_region_size);
		forward_vdma_put_region(io->pass->dev->vdma, io, v_region_address);
		forward_vdma_put_region(io->pass->dev->vdma, io, a_region_address);
	}
}

/**
 * IO processing function
 */
static void forward_passthrough_process(struct forward_passthrough_io *io, u32 irq)
{
	const struct forward_passthrough_dev_ops *ops = io->pass->ops;
	int in_prev, in_cur, in_next;
	u32 in_a_size;
	int out_prev, out_cur, out_next;
	u32 out_a_ptr;

	// Configure IO if not configured
	if (!io->configured) {
		ops->configure_io(io);
		return;
	}

	// Enable playback/capture
	if (!io->streaming) {
		forward_passthrough_toggle_streaming(io, true);
		return;
	}

	// Update I/O state - is signal present, valid, etc.
	ops->update_io_state(io);

	// End of input field/frame
	if (ops->irq_info_in(io, irq, &in_prev, &in_cur, &in_next, &in_a_size)) {
		int field = io->format.interlaced ? (in_prev & 0x1) : 0;
		io->last_in_buffers[field] = in_prev;

		// Extract audio from input
		if (!io->raw)
			forward_passthrough_decode_hanc(io, field, io->a_in_buffers[in_prev],
							in_a_size);

		// Enable output
		ops->toggle_streaming(io, true, true);
	}

	// End of output field/frame, select corresponding input buffer and process
	if (ops->irq_info_out(io, irq, &out_prev, &out_cur, &out_next, &out_a_ptr)) {
		int field = io->format.interlaced ? (out_next & 0x1) : 0;
		int in_buf = io->last_in_buffers[field];

		if (!io->raw) {
			u32 out_a_ptr_wr = io->last_a_wr_ptr;
			s32 realdelta = (s32)io->last_a_wr_ptr - (s32)out_a_ptr;

			// Compensate possible output DMA jitter (max +/-2 samples for i formats)
			if (realdelta < 0)
				realdelta += io->a_out_buffer_size / 4;

			if ((realdelta / io->a_out_groups / 4) > 2)
				out_a_ptr_wr = out_a_ptr;

			out_a_ptr_wr +=
				io->a_in_decoded_size[(!io->format.interlaced || field) ? 0 : 1];
			if (out_a_ptr_wr > io->a_out_buffer_size / 4)
				out_a_ptr_wr -= io->a_out_buffer_size / 4;

			io->last_a_wr_ptr = out_a_ptr_wr;

			ops->update_meta(io);

			// Process data
			forward_passthrough_data_process(io, field, io->v_in_buffers[in_buf],
							 io->a_in_decoded_buffers[field],
							 io->v_out_buffers[out_next], out_a_ptr_wr,
							 io->a_in_decoded_size[field]);
		} else {
			u32 video_addr, audio_addr;
			if (ops->buffer_map_address(io, out_prev, true, &video_addr, &audio_addr)) {
				forward_vdma_unmap_buf(io->pass->dev->vdma, io, video_addr,
						       io->v_buffer_size);
			}

			forward_passthrough_data_process_raw(io, field, io->v_in_buffers[in_buf]);

			if (ops->buffer_map_address(io, out_next, true, &video_addr, &audio_addr)) {
				forward_vdma_map_kernel_buf(io->pass->dev->vdma, io,
							    io->v_in_buffers[in_buf], video_addr,
							    io->v_buffer_size, false);
			}
		}
	}
}

/**
 * IO processing thread
 */
static int forward_passthrough_thread(void *p)
{
	struct forward_passthrough_io *io = p;
	struct forward_passthrough *pass = io->pass;
	struct forward_irq_listener listener;

	mutex_lock(&io->lock);
	io->raw = true;
	io->a_out_groups = 1;
	pass->ops->configure_io(io);
	mutex_unlock(&io->lock);

	forward_irq_listener_init(&listener);
	listener.type = FORWARD_IRQ_LISTENER_WAIT;
	listener.mask = pass->ops->irq_mask(io);
	forward_irq_listener_add(pass->dev, &listener);

	while (!kthread_should_stop()) {
		u32 irq = forward_irq_listener_wait_kthread(&listener, true, HZ / 10);

		if (kthread_should_stop())
			break;

		mutex_lock(&io->lock);
		forward_passthrough_process(io, irq);
		mutex_unlock(&io->lock);
	}

	mutex_lock(&io->lock);
	forward_passthrough_toggle_streaming(io, false);
	mutex_unlock(&io->lock);

	forward_irq_listener_remove(pass->dev, &listener);

	return 0;
}

/**
 * Genlock thread
 */
static int forward_passthrough_genlock_thread(void *p)
{
	struct forward_passthrough *pass = p;
	struct forward_irq_listener listener;
	bool configured = false;
	u64 last_genlock_check = ktime_get_ns();
	s32 gl_old_offset = 0;

	// Genlock master selection
	pass->master = &pass->io[0];
	pass->analog_master = false;
	pass->genlocked = false;

	forward_irq_listener_init(&listener);
	listener.type = FORWARD_IRQ_LISTENER_WAIT;
	listener.mask = pass->ops->genlock_irq_mask(pass->master);
	forward_irq_listener_add(pass->dev, &listener);

	while (!kthread_should_stop()) {
		u32 irq = forward_irq_listener_wait_kthread(&listener, true, HZ / 10);
		s32 gl_offset;
		bool gl_field;

		if (kthread_should_stop())
			break;

		// Delay because genlock irq comes a bit (microseconds) earlier than input
		// This cause race condition on offset readings.
		msleep(1);

		if (pass->ops->genlock_irq_info(pass->master, irq, &gl_field, &gl_offset)) {
			if ((ktime_to_ms(listener.time - last_genlock_check) > 1000) && !gl_field) {
				if (!pass->genlocked && (gl_old_offset == gl_offset)) {
					pass->ops->genlock_offset(pass->master, -gl_offset);
					pass->genlocked = true;
				}
				if (abs(gl_old_offset - gl_offset) > 1000) {
					pass->genlocked = false;
				}
				gl_old_offset = gl_offset;
				last_genlock_check = listener.time;
			}
		}

		if (!configured && pass->master->configured) {
			pass->ops->configure_genlock(pass->master, pass->analog_master);
			configured = true;
		}
	}

	forward_irq_listener_remove(pass->dev, &listener);

	return 0;
}

/**
 * Remove board
 */
static void forward_passthrough_remove(struct forward_dev *dev, void **private)
{
	struct forward_passthrough *pass = *private;
	const struct forward_passthrough_dev_ops *ops = pass->ops;
	int i;

	if (pass->gl_thread) {
		kthread_stop(pass->gl_thread);
		pass->gl_thread = NULL;
	}

	for (i = 0; i < ops->num_io; i++) {
		if (pass->threads[i]) {
			kthread_stop(pass->threads[i]);
			pass->threads[i] = NULL;
		}
	}

	if (pass->io) {
		devm_kfree(dev->dev, pass->io);
	}
	pass->io = NULL;

	*private = NULL;
	devm_kfree(dev->dev, pass);
}

/**
 * Add board
 */
static int forward_passthrough_probe(struct forward_dev *dev, void **private)
{
	int result = 0;
	struct forward_passthrough *pass;
	const struct forward_passthrough_dev_ops *ops;
	int i;

	if (!forward_has_mode(dev, FORWARD_PASSTHROUGH_MODE_NAME))
		return -ENOTSUPP;

	switch (dev->type) {
	case FORWARD_FD722:
		ops = &fd722_ops;
		break;
	case FORWARD_FD788:
		ops = &fd788_ops;
		break;
	default:
		return -ENOTSUPP;
	}

	pass = devm_kzalloc(dev->dev, sizeof(struct forward_passthrough), GFP_KERNEL);
	if (!pass)
		return -ENOMEM;

	pass->dev = dev;
	*private = pass;
	pass->ops = ops;

	pass->io = devm_kzalloc(dev->dev, ops->num_io * sizeof(struct forward_passthrough_io),
				GFP_KERNEL);
	if (!pass->io) {
		result = -ENOMEM;
		goto fail;
	}

	for (i = 0; i < ops->num_io; i++) {
		struct forward_passthrough_io *io = &pass->io[i];
		io->pass = pass;
		mutex_init(&io->lock);

		if (dev->type == FORWARD_FD722) {
			io->in_index = i;
			io->out_index = i + 2;
		} else if (dev->type == FORWARD_FD788) {
			io->in_index = i;
			io->out_index = i + 4;
		}
	}

	for (i = 0; i < ops->num_io; i++)
		pass->threads[i] = kthread_run(forward_passthrough_thread, &pass->io[i],
					       "fd-%llu-pt-%d", dev->soft_id, i);
	pass->gl_thread = kthread_run(forward_passthrough_genlock_thread, pass, "fd-%llu-pt-gl",
				      dev->soft_id);

	forward_info(dev, "Created %d passthrough devices: \n", ops->num_io);
	for (i = 0; i < ops->num_io; i++)
		forward_info(dev, "  IN%d -> OUT%d\n", dev->io[pass->io[i].in_index].number,
			     dev->io[pass->io[i].out_index].number);

	return 0;

fail:
	forward_passthrough_remove(dev, private);

	return result;
}

/**
 * Board reprobed - not used
 */
static int forward_passthrough_reprobe(struct forward_dev *dev, void **private)
{
	if (!forward_has_mode(dev, FORWARD_PASSTHROUGH_MODE_NAME))
		return -ENOTSUPP;

	return 0;
}

/**
 * Board I/O direction changed - not used
 */
static void forward_passthrough_io_changed(struct forward_dev *dev, struct forward_io *io,
					   enum forward_io_state state, void **private)
{
}

/**
 * Register forward driver extension
 */
static struct forward_extension forward_passthrough_ext = {
	.name = FORWARD_PASSTHROUGH_MODULE_NAME,
	.version = FORWARD_VERSION_CODE,
	.probe = forward_passthrough_probe,
	.reprobe = forward_passthrough_reprobe,
	.remove = forward_passthrough_remove,
	.io_changed = forward_passthrough_io_changed,
};

static int forward_passthrough_init(void)
{
	return forward_register_extension(&forward_passthrough_ext);
}

static void forward_passthrough_exit(void)
{
	forward_unregister_extension(&forward_passthrough_ext);
}

module_init(forward_passthrough_init);
module_exit(forward_passthrough_exit);

MODULE_LICENSE("GPL");
