/*
   forward-ctl.c - device control driver for SoftLab-NSK Forward video 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-ctl.h"
#include "forward-core.h"
#include "forward-irq.h"
#include "forward-vdma.h"

#include <forward-ioctl.h>

#include <linux/cdev.h>
#include <linux/mm.h>
#include <linux/uaccess.h>
#include <linux/fs.h>
#include <linux/delay.h>
#include <linux/poll.h>

extern struct list_head forward_extensions;
extern struct mutex forward_extensions_lock;

struct forward_ctl_ctx {
	struct forward_dev *dev;
	struct forward_irq_listener irq;
};

static int forward_ctl_open(struct inode *inode, struct file *filp)
{
	struct forward_dev *dev;
	struct forward_ctl_ctx *ctx;
	int result = 0;

	dev = container_of(inode->i_cdev, struct forward_dev, ctl_dev);

	result = forward_ref_get(dev);
	if (result)
		return result;

	ctx = devm_kzalloc(dev->dev, sizeof(struct forward_ctl_ctx), GFP_KERNEL);

	if (!ctx) {
		forward_ref_put(dev);
		return -ENOMEM;
	}
	ctx->dev = dev;

	forward_irq_listener_init(&ctx->irq);
	ctx->irq.type = FORWARD_IRQ_LISTENER_WAIT;
	forward_irq_listener_add(dev, &ctx->irq);

	filp->private_data = ctx;

	return 0;
}

static int forward_ctl_release(struct inode *inode, struct file *filp)
{
	struct forward_ctl_ctx *ctx = filp->private_data;
	struct forward_dev *dev = ctx->dev;

	forward_vdma_put_all_regions(dev->vdma, ctx);
	forward_irq_listener_remove(dev, &ctx->irq);
	devm_kfree(dev->dev, ctx);
	forward_ref_put(dev);

	return 0;
}

static long forward_ctl_reg_io(struct forward_dev *dev, struct forward_ioc_reg_io *ios, int n)
{
	int i = 0;
	for (i = 0; i < n; i++) {
		struct forward_ioc_reg_io *io = &ios[i];
		u32 data;

		if (io->reg * 4 >= dev->csr_size)
			return -EINVAL;

		if ((io->op == FWIOC_REG_IO_READ) || (io->op == FWIOC_REG_IO_SET) ||
		    (io->op == FWIOC_REG_IO_CLEAR))
			data = ioread32(&dev->csr[io->reg]);
		else
			data = io->data;

		if (io->op == FWIOC_REG_IO_SET)
			data |= io->data;
		else if (io->op == FWIOC_REG_IO_CLEAR)
			data &= ~io->data;

		if ((io->op == FWIOC_REG_IO_WRITE) || (io->op == FWIOC_REG_IO_SET) ||
		    (io->op == FWIOC_REG_IO_CLEAR))
			iowrite32(data, &dev->csr[io->reg]);

		io->data = data;

		if (io->delay != 0)
			usleep_range(io->delay / 1000, 2 * io->delay / 1000);
	}

	return 0;
}

static long forward_ctl_ioctl(struct file *filp, unsigned int cmd, unsigned long param)
{
	struct forward_ctl_ctx *ctx = filp->private_data;
	struct forward_dev *dev = ctx->dev;

	int result = -EOPNOTSUPP;

	if (_IOC_TYPE(cmd) != FWIOC_BASE)
		return -EINVAL;

	if (_IOC_NR(cmd) == _IOC_NR(FWIOC_REG_IO(0)) && _IOC_DIR(cmd) == (_IOC_READ | _IOC_WRITE)) {
		int ioctl_size, n_ios;
		struct forward_ioc_reg_io *ios;

		ioctl_size = _IOC_SIZE(cmd);
		if ((ioctl_size % sizeof(struct forward_ioc_reg_io)) != 0)
			return -EINVAL;

		n_ios = ioctl_size / sizeof(struct forward_ioc_reg_io);
		if (n_ios == 0)
			return -EINVAL;

		ios = memdup_user((void __user *)param, ioctl_size);

		if (IS_ERR(ios))
			return -ENOMEM;

		result = forward_ctl_reg_io(dev, ios, n_ios);
		if (!result)
			result = copy_to_user((void __user *)param, ios, ioctl_size);
		kfree(ios);
	} else if ((cmd == FWIOC_IRQ_ENABLE) || (cmd == FWIOC_IRQ_DISABLE)) {
		struct forward_ioc_irq uirq;
		struct forward_irq_t irq;
		unsigned long sflags;

		result =
			copy_from_user(&uirq, (void __user *)param, sizeof(struct forward_ioc_irq));
		if (result)
			goto end;

		forward_irq_flags_pack(irq, uirq.flags);

		spin_lock_irqsave(&ctx->irq.slock, sflags);
		if (cmd == FWIOC_IRQ_ENABLE) {
			forward_irq_flags_clear(ctx->irq.flags, ctx->irq.flags, irq);
			forward_irq_flags_set(ctx->irq.mask, ctx->irq.mask, irq);
		} else {
			forward_irq_flags_clear(ctx->irq.mask, ctx->irq.mask, irq);
			forward_irq_flags_clear(ctx->irq.flags, ctx->irq.flags, irq);
		}
		spin_unlock_irqrestore(&ctx->irq.slock, sflags);

		result = 0;
	}

	if ((_IOC_NR(cmd) >= FWIOC_VDMA_BASE) && (_IOC_NR(cmd) <= FWIOC_VDMA_END)) {
		struct forward_ioc_vdma_region reg;

		if (cmd != FWIOC_VDMA_PUT_ALL_REGIONS) {
			result = copy_from_user(&reg, (void __user *)param,
						sizeof(struct forward_ioc_vdma_region));
			if (result)
				goto end;
		}

		switch (cmd) {
		case FWIOC_VDMA_GET_REGION:
			result = forward_vdma_get_region(dev->vdma, ctx, reg.address, reg.size);
			break;
		case FWIOC_VDMA_PUT_REGION:
			result = forward_vdma_put_region(dev->vdma, ctx, reg.address);
			break;
		case FWIOC_VDMA_PUT_ALL_REGIONS:
			forward_vdma_put_all_regions(dev->vdma, ctx);
			result = 0;
			break;
		case FWIOC_VDMA_MAP:
			result = forward_vdma_map_user_buf(
				dev->vdma, ctx, reg.buffer, reg.address, reg.size,
				(reg.dir == FWIOC_VDMA_DIR_BOARD_TO_HOST));
			break;
		case FWIOC_VDMA_UNMAP:
			result = forward_vdma_unmap_buf(dev->vdma, ctx, reg.address, reg.size);
			break;
		case FWIOC_VDMA_SYNC:
			forward_vdma_sync_buf(dev->vdma, ctx, reg.address, reg.size);
			result = 0;
			break;
		default:
			result = -EOPNOTSUPP;
			break;
		}
	}

end:
	return result;
}

static ssize_t forward_ctl_read(struct file *filp, char __user *buf, size_t count, loff_t *pos)
{
	struct forward_ctl_ctx *ctx = filp->private_data;
	forward_irq_t irq_flags;
	struct forward_ioc_irq uirq;

	if (count != sizeof(struct forward_ioc_irq))
		return -EPROTO;

	if (*pos % sizeof(struct forward_ioc_irq))
		return -EPROTO;

	if ((filp->f_flags & O_NONBLOCK) &&
	    (!forward_irq_has_any_irq(ctx->irq.flags, ctx->irq.mask)))
		return -EAGAIN;

	irq_flags = forward_irq_listener_wait(&ctx->irq, true, 0);
	forward_irq_flags_unpack(uirq.flags, irq_flags);

	if (copy_to_user(buf, &uirq, sizeof(struct forward_ioc_irq)))
		return 0;

	return sizeof(struct forward_ioc_irq);
}

static unsigned int forward_ctl_poll(struct file *filp, poll_table *wait)
{
	struct forward_ctl_ctx *ctx = filp->private_data;
	u32 result = 0;
	unsigned long sflags;

	spin_lock_irqsave(&ctx->irq.slock, sflags);
	result = forward_irq_has_any_irq(ctx->irq.flags, ctx->irq.mask) ? (POLLIN | POLLRDNORM) : 0;
	spin_unlock_irqrestore(&ctx->irq.slock, sflags);

	if (result)
		return result;

	poll_wait(filp, &ctx->irq.wait, wait);

	spin_lock_irqsave(&ctx->irq.slock, sflags);
	result = forward_irq_has_any_irq(ctx->irq.flags, ctx->irq.mask) ? (POLLIN | POLLRDNORM) : 0;
	spin_unlock_irqrestore(&ctx->irq.slock, sflags);

	return result;
}

static struct file_operations forward_ctl_fops = { .owner = THIS_MODULE,
						   .read = forward_ctl_read,
						   .poll = forward_ctl_poll,
						   .open = forward_ctl_open,
						   .release = forward_ctl_release,
						   .unlocked_ioctl = forward_ctl_ioctl };

static ssize_t forward_attr_show_software_id(struct device *dev, struct device_attribute *attr,
					     char *buf)
{
	struct forward_dev *fdev = dev_get_drvdata(dev);
	return scnprintf(buf, PAGE_SIZE, "%lld\n", fdev->soft_id);
}

static ssize_t forward_attr_show_hardware_id(struct device *dev, struct device_attribute *attr,
					     char *buf)
{
	struct forward_dev *fdev = dev_get_drvdata(dev);
	return scnprintf(buf, PAGE_SIZE, "%016llX\n", fdev->hard_id);
}

static ssize_t forward_attr_show_driver_version(struct device *dev, struct device_attribute *attr,
						char *buf)
{
	return scnprintf(buf, PAGE_SIZE, FORWARD_VERSION_STRING "\n");
}

static ssize_t forward_attr_show_name(struct device *dev, struct device_attribute *attr, char *buf)
{
	struct forward_dev *fdev = dev_get_drvdata(dev);
	return scnprintf(buf, PAGE_SIZE, "%s", fdev->name);
}

static ssize_t forward_attr_show_mode(struct device *dev, struct device_attribute *attr, char *buf)
{
	struct forward_dev *fdev = dev_get_drvdata(dev);
	char *str = buf;
	int i;

	for (i = 0; (i < FORWARD_MAX_MODE) && fdev->mode[i]; i++)
		str += snprintf(str, PAGE_SIZE - (str - buf), "%s,", fdev->mode[i]);

	if (str != buf)
		str--;

	*(str) = '\n';

	str++;
	*(str) = '\0';

	return str - buf;
}

static ssize_t forward_attr_store_mode(struct device *dev, struct device_attribute *attr,
				       const char *buf, size_t count)
{
	struct forward_dev *fdev = dev_get_drvdata(dev);
	int result = forward_switch_mode(fdev, buf);
	return result ? result : count;
}

static ssize_t forward_attr_show_available_modes(struct device *dev, struct device_attribute *attr,
						 char *buf)
{
	struct forward_dev *fdev = dev_get_drvdata(dev);
	struct forward_extension *ext;
	char *str = buf;

	mutex_lock(&forward_extensions_lock);
	list_for_each_entry (ext, &forward_extensions, node) {
		if (ext->available(fdev))
			str += snprintf(str, PAGE_SIZE - (str - buf), "%s,", ext->name);
	}
	mutex_unlock(&forward_extensions_lock);

	if (str != buf)
		str--;

	*(str) = '\n';

	str++;
	*(str) = '\0';

	return str - buf;
}

static ssize_t forward_attr_show_io_config(struct device *dev, struct device_attribute *attr,
					   char *buf)
{
	struct forward_dev *fdev = dev_get_drvdata(dev);
	int i;

	for (i = 0; i < fdev->cfg.io_count; i++) {
		if (fdev->io[i].state == FORWARD_IO_TX)
			buf[i] = 'O';
		else if (fdev->io[i].state == FORWARD_IO_RX)
			buf[i] = 'I';
		else if (fdev->io[i].state == FORWARD_IO_TXRX)
			buf[i] = 'B';
		else if (fdev->io[i].state == FORWARD_IO_DISABLED)
			buf[i] = '-';
		else
			buf[i] = '?';
	}
	buf[i] = '\n';
	i++;
	buf[i] = '\0';

	return i;
}

static ssize_t forward_attr_store_io_config(struct device *dev, struct device_attribute *attr,
					    const char *buf, size_t count)
{
	struct forward_dev *fdev = dev_get_drvdata(dev);
	int i;

	if (!fdev->cfg.io_count)
		return -EOPNOTSUPP;

	for (i = 0; (i < (int)count) && (i < fdev->cfg.io_count); i++) {
		enum forward_io_state req_state = FORWARD_IO_DISABLED;

		if (buf[i] == '\0')
			break;

		if (buf[i] == 'I')
			req_state = FORWARD_IO_RX;
		else if (buf[i] == 'O')
			req_state = FORWARD_IO_TX;
		else if (buf[i] == 'B')
			req_state = FORWARD_IO_TXRX;
		else if (buf[i] == '-')
			req_state = FORWARD_IO_DISABLED;
		else
			continue;

		if (req_state != fdev->io[i].state) {
			int result = forward_switch_io(&fdev->io[i], req_state);
			if (result)
				return result;
		}
	}

	return count;
}

static ssize_t forward_attr_store_reboot(struct device *dev, struct device_attribute *attr,
					 const char *buf, size_t count)
{
	struct forward_dev *fdev = dev_get_drvdata(dev);
	int status;

	if (count < 1)
		return -EINVAL;

	if (buf[0] == '1')
		status = forward_reboot_device(fdev);
	else if (buf[0] == '0')
		status = 0;
	else
		status = -EINVAL;

	return status ? status : count;
}

static DEVICE_ATTR(reboot, S_IWUSR, NULL, forward_attr_store_reboot);
static DEVICE_ATTR(software_id, S_IRUGO, forward_attr_show_software_id, NULL);
static DEVICE_ATTR(hardware_id, S_IRUGO, forward_attr_show_hardware_id, NULL);
static DEVICE_ATTR(driver_version, S_IRUGO, forward_attr_show_driver_version, NULL);
static DEVICE_ATTR(name, S_IRUGO, forward_attr_show_name, NULL);
static DEVICE_ATTR(mode, S_IWUSR | S_IWGRP | S_IRUGO, forward_attr_show_mode,
		   forward_attr_store_mode);
static DEVICE_ATTR(available_modes, S_IRUGO, forward_attr_show_available_modes, NULL);
static DEVICE_ATTR(io_config, S_IWUSR | S_IWGRP | S_IRUGO, forward_attr_show_io_config,
		   forward_attr_store_io_config);

static struct attribute *forward_dev_attrs[] = {
	&dev_attr_software_id.attr, &dev_attr_hardware_id.attr, &dev_attr_driver_version.attr,
	&dev_attr_name.attr,	    &dev_attr_mode.attr,	&dev_attr_available_modes.attr,
	&dev_attr_io_config.attr,   &dev_attr_reboot.attr,	NULL,
};

static struct attribute_group forward_dev_attr_group = {
	.attrs = forward_dev_attrs,
};

void forward_ctl_remove(struct forward_dev *dev)
{
	dev_t number = forward_dev_number(dev, 0);

	if (dev->dev) {
		device_destroy(forward_class(), number);
		dev->dev = NULL;
	}

	if (dev->ctl_dev.owner == THIS_MODULE) {
		cdev_del(&dev->ctl_dev);
		dev->ctl_dev.owner = 0;
	}
}

int forward_ctl_probe(struct forward_dev *dev)
{
	int status = 0;
	dev_t number = forward_dev_number(dev, 0);

	cdev_init(&dev->ctl_dev, &forward_ctl_fops);
	dev->ctl_dev.owner = THIS_MODULE;

	status = cdev_add(&dev->ctl_dev, number, 1);
	if (status < 0) {
		forward_err(dev, "unable to add character device!\n");
		dev->ctl_dev.owner = 0;
		return -ENODEV;
	}

	dev->attr_groups[0] = &forward_dev_attr_group;
	dev->attr_groups[1] = dev->cfg.attributes;
	dev->attr_groups[2] = NULL;

#if LINUX_VERSION_CODE >= KERNEL_VERSION(3, 11, 0)
	dev->dev = device_create_with_groups(forward_class(), dev->parent_dev, number, dev,
					     dev->attr_groups, "%s", dev->dev_name);
#else
	dev->dev =
		device_create(forward_class(), dev->parent_dev, number, dev, "%s", dev->dev_name);

	if (!IS_ERR(dev->dev)) {
		int g, i;
		for (g = 0; dev->attr_groups[g]; g++) {
			const struct attribute_group *group = dev->attr_groups[g];
			for (i = 0; group->attrs[i]; i++)
				device_create_file(dev->dev,
						   (struct device_attribute *)group->attrs[i]);
		}
	}
#endif

	if (IS_ERR(dev->dev)) {
		forward_err(dev, "unable to create sysdev!\n");
		dev->dev = 0;
		status = -ENODEV;
		goto fail;
	}

	return 0;

fail:
	forward_ctl_remove(dev);
	return status;
}
