/*
   forward-v4l2.c - Video4Linux2 driver for SoftLab-NSK Forward boards

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

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

#include <linux/module.h>
#include <linux/kthread.h>
#include <linux/sched.h>
#include <linux/smp.h>
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 11, 0)
#include <uapi/linux/sched/types.h>
#endif
#include <linux/sched/rt.h>

#define FORWARD_V4L2_MODE_NAME "v4l2"
#define FORWARD_V4L2_POLL_PERIOD_MS 100

bool default_mute = true;

module_param(default_mute, bool, S_IRUGO);

extern struct forward_v4l2_dev_ops fd788_ops;
extern struct forward_v4l2_dev_ops fd722_ops;
extern struct forward_v4l2_dev_ops fd720_ops;
extern struct forward_v4l2_dev_ops fd922_ops;
extern struct forward_v4l2_dev_ops fd940_ops;
extern struct forward_v4l2_dev_ops fd2110_ops;

static void forward_v4l2_work(struct work_struct *work)
{
	struct forward_v4l2 *v4l2 = container_of(to_delayed_work(work), struct forward_v4l2, work);
	int i;

	if (!v4l2->ops->genlock_switch)
		return;

	for (i = 0; i < v4l2->dev->cfg.io_count; i++) {
		if (v4l2->io[i].genlock)
			forward_v4l2_genlock_process_io(v4l2->io[i].genlock);
	}
	forward_v4l2_genlock_process(v4l2);

	queue_delayed_work(system_power_efficient_wq, to_delayed_work(work),
			   msecs_to_jiffies(FORWARD_V4L2_POLL_PERIOD_MS));
}

static void forward_v4l2_remove(struct forward_dev *dev, void **private)
{
	struct forward_v4l2 *v4l2 = *private;
	int i;

	if (!v4l2)
		return;

	cancel_delayed_work_sync(&v4l2->work);

	if (v4l2->io) {
		for (i = 0; i < v4l2->dev->cfg.io_count; i++) {
			forward_v4l2_io_deinit(&v4l2->io[i]);
			dev->io[i].v4l2 = NULL;
		}

		devm_kfree(dev->dev, v4l2->io);
		v4l2->io = NULL;
	}

	if (v4l2->analog) {
		devm_kfree(dev->dev, v4l2->analog);
		v4l2->analog = NULL;
	}

	v4l2->ops = NULL;

	v4l2_device_unregister(&v4l2->v4l2_dev);

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

static int forward_v4l2_probe(struct forward_dev *dev, void **private)
{
	int result = 0;
	struct forward_v4l2 *v4l2;
	int i;

	if (!forward_has_mode(dev, FORWARD_V4L2_MODE_NAME))
		return -EOPNOTSUPP;

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

	v4l2->dev = dev;
	*private = v4l2;

	INIT_DELAYED_WORK(&v4l2->work, forward_v4l2_work);

	switch (dev->type) {
	case FORWARD_FD788:
		v4l2->ops = &fd788_ops;
		break;
	case FORWARD_FD722:
	case FORWARD_FD722M2:
		v4l2->ops = &fd722_ops;
		break;
	case FORWARD_FD720:
		v4l2->ops = &fd720_ops;
		break;
	case FORWARD_FD922:
		v4l2->ops = &fd922_ops;
		break;
	case FORWARD_FD940:
		v4l2->ops = &fd940_ops;
		break;
	case FORWARD_FD2110:
		v4l2->ops = &fd2110_ops;
		break;
	default:
		result = -EOPNOTSUPP;
		goto fail;
		break;
	};

	snprintf(v4l2->v4l2_dev.name, sizeof(v4l2->v4l2_dev.name), "%s", v4l2->dev->name);

	result = v4l2_device_register(v4l2->dev->dev, &v4l2->v4l2_dev);
	if (result)
		goto fail;

	v4l2->io = devm_kzalloc(dev->dev, dev->cfg.io_count * sizeof(struct forward_v4l2_io),
				GFP_KERNEL);

	if (!v4l2->io) {
		result = -ENOMEM;
		goto fail;
	}

	if (v4l2->ops->analog_rx_video_query) {
		v4l2->analog =
			devm_kzalloc(dev->dev, sizeof(struct forward_v4l2_analog_in), GFP_KERNEL);

		if (!v4l2->analog)
			goto fail;

		v4l2->analog->v4l2 = v4l2;
	}

	for (i = 0; i < dev->cfg.io_count; i++) {
		v4l2->io[i].v4l2 = v4l2;
		v4l2->io[i].index = i;
		v4l2->io[i].number = dev->io[i].number;
		v4l2->io[i].state = dev->io[i].state;
		dev->io[i].v4l2 = &v4l2->io[i];
		mutex_init(&v4l2->io[i].lock);
		init_waitqueue_head(&v4l2->io[i].wait);

		result = forward_v4l2_io_init(&v4l2->io[i]);
		if (result)
			goto fail;
	}

	queue_delayed_work(system_power_efficient_wq, &v4l2->work,
			   msecs_to_jiffies(FORWARD_V4L2_POLL_PERIOD_MS));

	forward_info(dev, "v4l2: %d devices created", dev->cfg.io_count);

	return 0;

fail:
	forward_v4l2_remove(dev, private);

	return result;
}

static int forward_v4l2_reprobe(struct forward_dev *dev, void **private)
{
	if (!forward_has_mode(dev, FORWARD_V4L2_MODE_NAME))
		return -EOPNOTSUPP;

	forward_v4l2_remove(dev, private);
	return forward_v4l2_probe(dev, private);
}

static void forward_v4l2_io_changed(struct forward_dev *dev, struct forward_io *io,
				    enum forward_io_state state, void **private)
{
	struct forward_v4l2 *v4l2 = *private;

	if (!v4l2 || !v4l2->io)
		return;

	forward_v4l2_io_deinit(&v4l2->io[io->index]);
	v4l2->io[io->index].number = io->number;
	v4l2->io[io->index].state = io->state;
	forward_v4l2_io_init(&v4l2->io[io->index]);
}

static bool forward_v4l2_available(struct forward_dev *dev)
{
	switch (dev->type) {
	case FORWARD_FD722:
	case FORWARD_FD788:
	case FORWARD_FD720:
	case FORWARD_FD922:
	case FORWARD_FD940:
	case FORWARD_FD722M2:
	case FORWARD_FD2110:
		return true;
	default:
		return false;
	}
}

static struct forward_extension forward_v4l2_ext = {
	.name = FORWARD_V4L2_MODE_NAME,
	.version = FORWARD_VERSION_CODE,
	.available = forward_v4l2_available,
	.probe = forward_v4l2_probe,
	.reprobe = forward_v4l2_reprobe,
	.remove = forward_v4l2_remove,
	.io_changed = forward_v4l2_io_changed,
};

static int forward_v4l2_init(void)
{
	return forward_register_extension(&forward_v4l2_ext);
}

static void forward_v4l2_exit(void)
{
	forward_unregister_extension(&forward_v4l2_ext);
}

module_init(forward_v4l2_init);
module_exit(forward_v4l2_exit);

MODULE_LICENSE("GPL");
