/*
   forward-core.c - 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-irq.h"
#include "forward-ctl.h"
#include "forward-core.h"
#include "forward-vdma.h"
#include "forward-sdma.h"
#include "forward-mcap.h"

#include <linux/module.h>
#include <linux/errno.h>
#include <linux/interrupt.h>
#include <linux/io.h>
#include <linux/pci.h>
#include <linux/fs.h>
#include <linux/ctype.h>
#include <linux/export.h>
#include <linux/of.h>
#include <linux/delay.h>

#define FORWARD_MODULE_NAME "forward"

extern struct forward_dev_config fd788_cfg;
extern struct forward_dev_config fd722_cfg;
extern struct forward_dev_config fd720_cfg;
extern struct forward_dev_config fd922_cfg;
extern struct forward_dev_config fdio_cfg;
extern struct forward_dev_config fd940_cfg;
extern struct forward_dev_config fd2110_cfg;
extern struct forward_dev_config fd722m2_cfg;

static u16 module_major = 0;
static struct class *module_class = 0;

static const char *forward_names[FORWARD_TYPE_COUNT] = { "FD722", "FD788", "FD720",  "FD922",
							 "FDIO",  "FD940", "FD2110", "FD722M2" };

static const char *forward_names_lower[FORWARD_TYPE_COUNT] = {
	"fd722", "fd788", "fd720", "fd922", "fdio", "fd940", "fd2110", "fd722m2"
};

static const char *forward_default_modes[FORWARD_TYPE_COUNT] = {
	"v4l2,alsa", "v4l2,alsa", "v4l2,alsa",		"v4l2,alsa",
	"",	     "v4l2,alsa", "v4l2,alsa,ethernet", "v4l2,alsa"
};

static const struct forward_dev_config *forward_dev_configs[FORWARD_TYPE_COUNT] = {
	&fd722_cfg, &fd788_cfg, &fd720_cfg,  &fd922_cfg,
	&fdio_cfg,  &fd940_cfg, &fd2110_cfg, &fd722m2_cfg
};

static const struct pci_device_id forward_pci_ids[] = {
	{ PCI_DEVICE(0x1C1F, 0x0019), .driver_data = FORWARD_FD722 },
	{ PCI_DEVICE(0x1C1F, 0x001A), .driver_data = FORWARD_FD788 },
	{ PCI_DEVICE(0x1C1F, 0x001B), .driver_data = FORWARD_FD720 },
	{ PCI_DEVICE(0x1C1F, 0x001C), .driver_data = FORWARD_FD922 },
	{ PCI_DEVICE(0x1C1F, 0x001F), .driver_data = FORWARD_FD940 },
	{ PCI_DEVICE(0x1C1F, 0x0020), .driver_data = FORWARD_FD2110 },
	{ PCI_DEVICE(0x1C1F, 0x0021), .driver_data = FORWARD_FD722 },
	{ PCI_DEVICE(0x1C1F, 0x0022), .driver_data = FORWARD_FD788 },
	{ PCI_DEVICE(0x1C1F, 0x0023), .driver_data = FORWARD_FD722M2 },
	{ PCI_DEVICE(0x1C1F, 0x0024), .driver_data = FORWARD_FD722 },
	{ PCI_DEVICE(0x1C1F, 0x0025), .driver_data = FORWARD_FD922 },
	{ PCI_DEVICE(0x1C1F, 0x0026), .driver_data = FORWARD_FD788 },
	{ PCI_DEVICE(0x1C1F, 0x0027), .driver_data = FORWARD_FD940 },
	{},
};

static const struct of_device_id forward_of_ids[] = {
	{ .compatible = "softlab,forward-io", .data = (void *)FORWARD_FDIO },
	{},
};

static LIST_HEAD(forward_devices);
static DEFINE_MUTEX(forward_devices_lock);

LIST_HEAD(forward_extensions);
DEFINE_MUTEX(forward_extensions_lock);

static struct forward_extension_binding *forward_ext_binding(struct forward_dev *dev,
							     const struct forward_extension *ext)
{
	unsigned long sflags;
	struct forward_extension_binding *bind, *result = NULL;

	spin_lock_irqsave(&dev->extensions_lock, sflags);
	list_for_each_entry (bind, &dev->extensions, node) {
		if (bind->extension == ext) {
			result = bind;
			break;
		}
	}
	spin_unlock_irqrestore(&dev->extensions_lock, sflags);

	return result;
}

void *forward_extension_private(struct forward_dev *dev, const struct forward_extension *ext)
{
	struct forward_extension_binding *bind = forward_ext_binding(dev, ext);
	if (!bind)
		return NULL;

	return bind->private;
}
EXPORT_SYMBOL(forward_extension_private);

static struct forward_extension_binding *forward_ext_bind(struct forward_dev *dev,
							  struct forward_extension *ext)
{
	struct forward_extension_binding *bind = NULL;
	int status = 0;
	unsigned long sflags;
	void *private;

	if (ext->probe)
		status = ext->probe(dev, &private);

	if (status)
		return NULL;

	bind = devm_kzalloc(dev->dev, sizeof(struct forward_extension_binding), GFP_KERNEL);

	if (!bind) {
		if (ext->remove)
			ext->remove(dev, &private);

		return NULL;
	}

	bind->device = dev;
	bind->extension = ext;
	strncpy(bind->name, ext->name, FORWARD_MAX_EXTENSION_NAME);
	bind->private = private;

	spin_lock_irqsave(&dev->extensions_lock, sflags);
	list_add_tail(&bind->node, &dev->extensions);
	spin_unlock_irqrestore(&dev->extensions_lock, sflags);

	return bind;
}

static void forward_ext_unbind(struct forward_dev *dev, const struct forward_extension *ext)
{
	struct forward_extension_binding *bind = forward_ext_binding(dev, ext);
	unsigned long sflags;

	if (!bind)
		return;

	spin_lock_irqsave(&dev->extensions_lock, sflags);
	list_del(&bind->node);
	spin_unlock_irqrestore(&dev->extensions_lock, sflags);

	if (ext->remove)
		ext->remove(dev, &bind->private);

	devm_kfree(dev->dev, bind);
}

static void forward_ext_rebind(struct forward_dev *dev, const struct forward_extension *ext)
{
	struct forward_extension_binding *bind = forward_ext_binding(dev, ext);
	int status = 0;

	if (!bind)
		return;

	if (ext->reprobe)
		status = ext->reprobe(dev, &bind->private);

	if (status)
		forward_ext_unbind(dev, ext);
}

static void forward_rebind_extensions(struct forward_dev *dev)
{
	struct forward_extension *ext;
	mutex_lock(&forward_extensions_lock);
	list_for_each_entry (ext, &forward_extensions, node) {
		if (forward_ext_binding(dev, ext))
			forward_ext_rebind(dev, ext);
	}
	list_for_each_entry (ext, &forward_extensions, node) {
		if (!forward_ext_binding(dev, ext))
			forward_ext_bind(dev, ext);
	}
	mutex_unlock(&forward_extensions_lock);
}

static void forward_init_registers(struct forward_dev *dev)
{
	forward_irq_t mask;
	forward_irq_flags_set_all(mask);
	dev->cfg.disable_interrupts(dev, &mask);

	dev->cfg.read_id(dev);

	if (dev->soft_id < 0)
		dev_warn(dev->parent_dev, "Invalid device serial: %lld\n", dev->soft_id);
}

static void forward_fini_registers(struct forward_dev *dev)
{
	const struct forward_dev_config *cfg = forward_dev_configs[dev->type];
	forward_irq_t mask;
	forward_irq_flags_set_all(mask);
	cfg->disable_interrupts(dev, &mask);
}

static void forward_parse_mode(struct forward_dev *dev, const char *mode_string)
{
	int i = 0;
	char *str = dev->mode_string, *token = NULL;

	strncpy(dev->mode_string, mode_string, FORWARD_MAX_MODE_STRING - 1);

	do {
		token = strsep(&str, " \n,;");

		if (!strlen(token))
			continue;

		if (i >= FORWARD_MAX_MODE) {
			forward_warn(dev, "too many modes, mode %s will be ignored", token);
			continue;
		}

		dev->mode[i] = token;
		i++;
	} while (str != NULL);
}

int forward_ref_get(struct forward_dev *dev)
{
	unsigned long sflags;

	if (!mutex_trylock(&dev->lock))
		return -EBUSY;

	spin_lock_irqsave(&dev->ref_lock, sflags);
	kref_get(&dev->ref);
	spin_unlock_irqrestore(&dev->ref_lock, sflags);

	mutex_unlock(&dev->lock);
	return 0;
}
EXPORT_SYMBOL(forward_ref_get);

static void forward_kref_release(struct kref *kref)
{
}

void forward_ref_put(struct forward_dev *dev)
{
	unsigned long sflags;

	spin_lock_irqsave(&dev->ref_lock, sflags);
	kref_put(&dev->ref, forward_kref_release);
	spin_unlock_irqrestore(&dev->ref_lock, sflags);
}
EXPORT_SYMBOL(forward_ref_put);

int forward_ref_read(struct forward_dev *dev)
{
	unsigned long sflags;
	int result;

	spin_lock_irqsave(&dev->ref_lock, sflags);
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 11, 0)
	result = kref_read(&dev->ref);
#else
	result = atomic_read(&dev->ref.refcount);
#endif
	spin_unlock_irqrestore(&dev->ref_lock, sflags);

	return result;
}
EXPORT_SYMBOL(forward_ref_read);

int forward_io_ref_get(struct forward_io *io)
{
	if (forward_ref_get(io->dev))
		return -EBUSY;

	if (!mutex_trylock(&io->lock))
		return -EBUSY;

	kref_get(&io->ref);

	mutex_unlock(&io->lock);
	return 0;
}
EXPORT_SYMBOL(forward_io_ref_get);

void forward_io_ref_put(struct forward_io *io)
{
	kref_put(&io->ref, forward_kref_release);
	forward_ref_put(io->dev);
}
EXPORT_SYMBOL(forward_io_ref_put);

int forward_io_ref_read(struct forward_io *dev)
{
	int result;

#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 11, 0)
	result = kref_read(&dev->ref);
#else
	result = atomic_read(&dev->ref.refcount);
#endif

	return result;
}
EXPORT_SYMBOL(forward_io_ref_read);

int forward_switch_mode(struct forward_dev *dev, const char *mode_string)
{
	int i;

	mutex_lock(&dev->lock);
	if (forward_ref_read(dev) != 1) {
		forward_warn(dev, "Cannot change mode, device is busy");
		mutex_unlock(&dev->lock);
		return -EBUSY;
	}

	for (i = 0; i < FORWARD_MAX_MODE; i++)
		dev->mode[i] = NULL;

	forward_parse_mode(dev, mode_string);

	mutex_unlock(&dev->lock);

	forward_rebind_extensions(dev);

	return 0;
}
EXPORT_SYMBOL(forward_switch_mode);

int forward_switch_io(struct forward_io *io, enum forward_io_state state)
{
	struct forward_dev *dev = io->dev;
	struct forward_extension_binding *bind;
	int ref;
	int result;

	mutex_lock(&io->lock);

	if (io->state == state) {
		mutex_unlock(&io->lock);
		return 0;
	}

#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 11, 0)
	ref = kref_read(&io->ref);
#else
	ref = atomic_read(&io->ref.refcount);
#endif

	if (ref != 1) {
		forward_warn(io->dev, "Cannot change I/O configuration for pin %d, I/O is busy",
			     io->number);
		mutex_unlock(&io->lock);
		return -EBUSY;
	}

	result = dev->cfg.set_io_state(dev, io->index, state);
	if (!result) {
		io->state = state;
		io->number = dev->cfg.io_number(dev, io->index);
	}

	mutex_lock(&forward_extensions_lock);
	list_for_each_entry (bind, &dev->extensions, node)
		bind->extension->io_changed(dev, io, state, &bind->private);
	mutex_unlock(&forward_extensions_lock);

	mutex_unlock(&io->lock);
	return result;
}
EXPORT_SYMBOL(forward_switch_io);

static void dummy(struct kref *kref)
{
}

int forward_toggle_streaming(struct forward_io *io, bool enabled)
{
	int refcount;
	int status = 0;

#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 11, 0)
	refcount = kref_read(&io->streaming_ref);
#else
	refcount = atomic_read(&io->streaming_ref.refcount);
#endif
	if (!enabled)
		kref_put(&io->streaming_ref, &dummy);

	if (refcount <= 1) {
		io->dev->cfg.toggle_streaming(io->dev, io->index, enabled);
		kref_init(&io->streaming_ref);
	}

	if (enabled)
		kref_get(&io->streaming_ref);

	return status;
}
EXPORT_SYMBOL(forward_toggle_streaming);

bool forward_has_mode(struct forward_dev *dev, const char *mode)
{
	int i;
	bool result = false;

	mutex_lock(&dev->lock);
	for (i = 0; (i < FORWARD_MAX_MODE) && (dev->mode[i]); i++) {
		if (!strncasecmp(dev->mode[i], mode, FORWARD_MAX_MODE_STRING)) {
			result = true;
			break;
		}
	}
	mutex_unlock(&dev->lock);

	return result;
}
EXPORT_SYMBOL(forward_has_mode);

const char *forward_dev_model_name(struct forward_dev *dev)
{
	if ((dev->type >= FORWARD_TYPE_COUNT) || (dev->type < 0))
		return NULL;

	return forward_names[dev->type];
}
EXPORT_SYMBOL(forward_dev_model_name);

static int forward_init_io(struct forward_dev *dev, int io)
{
	dev->io[io].dev = dev;
	dev->io[io].index = io;
	kref_init(&dev->io[io].ref);
	mutex_init(&dev->io[io].lock);
	dev->io[io].number = dev->cfg.io_number(dev, io);
	dev->io[io].state = dev->cfg.io_state(dev, io);
	dev->cfg.set_io_state(dev, io, dev->io[io].state);

	return 0;
}

static void forward_fini_io(struct forward_dev *dev, int io)
{
	dev->io[io].dev = NULL;
}

static void forward_fini_device(struct forward_dev *dev)
{
	unsigned long sflags;
	int i;

	forward_sdma_disable(dev);
	forward_vdma_disable(dev);

	spin_lock_irqsave(&dev->extensions_lock, sflags);
	while (!list_empty(&dev->extensions)) {
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3, 13, 0)
		struct forward_extension_binding *bind =
			list_last_entry(&dev->extensions, struct forward_extension_binding, node);
#else
		struct forward_extension_binding *bind =
			list_first_entry(&dev->extensions, struct forward_extension_binding, node);
#endif

		spin_unlock_irqrestore(&dev->extensions_lock, sflags);
		forward_ext_unbind(dev, bind->extension);
		spin_lock_irqsave(&dev->extensions_lock, sflags);
	}
	spin_unlock_irqrestore(&dev->extensions_lock, sflags);

	if (dev->io) {
		for (i = 0; i < dev->cfg.io_count; i++)
			forward_fini_io(dev, i);

		devm_kfree(dev->parent_dev, dev->io);
	}

	forward_sdma_remove(dev);
	forward_vdma_remove(dev);

	forward_irq_remove(dev);
}

static int forward_init_device(struct forward_dev *dev)
{
	int result;
	int i;

	result = forward_irq_probe(dev);
	if (result)
		goto fail;

	if (dev->cfg.vdma_size_mb > 0) {
		result = forward_vdma_probe(dev);
		if (result)
			goto fail;
	}

	if (dev->cfg.sdma_size > 0) {
		result = forward_sdma_probe(dev);
		if (result)
			goto fail;
	}

	if (dev->cfg.io_number != 0) {
		dev->io = devm_kzalloc(dev->parent_dev,
				       dev->cfg.io_count * sizeof(struct forward_io), GFP_KERNEL);

		for (i = 0; i < dev->cfg.io_count; i++) {
			result = forward_init_io(dev, i);
			if (result)
				goto fail;
		}
	}

	if (dev->vdma)
		forward_vdma_enable(dev);

	if (dev->sdma)
		forward_sdma_enable(dev);

fail:
	return result;
}

int forward_reboot_device(struct forward_dev *dev)
{
	struct forward_extension *ext;
	int status;

	if (!dev->cfg.reboot) {
		forward_warn(dev, "Device reboot unsupported");
		return -EOPNOTSUPP;
	}

	mutex_lock(&dev->lock);

	if (forward_ref_read(dev) != 1) {
		forward_warn(dev, "Cannot reboot device, device is busy");
		mutex_unlock(&dev->lock);
		return -EBUSY;
	}

	forward_fini_device(dev);

	forward_fini_registers(dev);

	dev->cfg.reboot(dev);

	forward_init_registers(dev);

	status = forward_init_device(dev);

	mutex_unlock(&dev->lock);

	mutex_lock(&forward_extensions_lock);
	list_for_each_entry (ext, &forward_extensions, node)
		forward_ext_bind(dev, ext);
	mutex_unlock(&forward_extensions_lock);

	return status;
}
EXPORT_SYMBOL(forward_reboot_device);

static void forward_remove_common(struct forward_dev *dev)
{
	forward_fini_device(dev);

	dev->cfg.fini(dev);

	forward_ctl_remove(dev);

	mutex_lock(&forward_devices_lock);
	if (dev->number != 0) {
		list_del(&dev->list);
	}
	mutex_unlock(&forward_devices_lock);

	forward_fini_registers(dev);
}

static int forward_probe_common(struct forward_dev *dev)
{
	int result = 0;
	struct forward_extension *ext;

	mutex_init(&dev->lock);
	mutex_lock(&dev->lock);

	INIT_LIST_HEAD(&dev->extensions);
	spin_lock_init(&dev->extensions_lock);
	kref_init(&dev->ref);

	dev->cfg = *forward_dev_configs[dev->type];

	result = dev->cfg.init(dev);
	if (result)
		goto fail;

	forward_init_registers(dev);

	mutex_lock(&forward_devices_lock);
	if (list_empty(&forward_devices))
		dev->number = 1;
	else {
		// Track all devices to get free number
		int candidate = 1;
		struct forward_dev *idev;
		for (candidate = 1; candidate < FORWARD_MAX_DEVICES + 1; candidate++) {
			bool busy = false;
			list_for_each_entry (idev, &forward_devices, list) {
				if (idev->number == candidate)
					busy = true;
			}
			if (!busy) {
				dev->number = candidate;
				break;
			}
		}
		if (candidate > FORWARD_MAX_DEVICES) {
			dev_err(dev->parent_dev, "Maximum number of devices reached\n");
			result = -ENOMEM;
			mutex_unlock(&forward_devices_lock);
			goto fail;
		}
	}
	if (dev->soft_id < 0)
		dev->soft_id = dev->number;

	snprintf(dev->name, sizeof(dev->name), "%s %lld", forward_names[dev->type], dev->soft_id);

	snprintf(dev->dev_name, sizeof(dev->dev_name), "%s-%lld", forward_names_lower[dev->type],
		 dev->soft_id);

	list_add_tail(&dev->list, &forward_devices);
	mutex_unlock(&forward_devices_lock);

	forward_parse_mode(dev, forward_default_modes[dev->type]);

	result = forward_ctl_probe(dev);
	if (result)
		goto fail;

	result = forward_init_device(dev);
	if (result)
		goto fail;

	forward_info(dev, "Added successfully\n");

	mutex_unlock(&dev->lock);

	mutex_lock(&forward_extensions_lock);
	list_for_each_entry (ext, &forward_extensions, node)
		forward_ext_bind(dev, ext);
	mutex_unlock(&forward_extensions_lock);

fail:
	return result;
}

static void forward_remove_pci(struct pci_dev *pdev)
{
	struct forward_dev *dev = pci_get_drvdata(pdev);

	if (!dev)
		return;

	forward_remove_common(dev);

	forward_mcap_fini(dev);

	dev->csr_size = 0;
	dev->csr = NULL;
	pcim_iounmap_regions(pdev, (1 << 0));

	pci_disable_device(pdev);

	pci_set_drvdata(pdev, 0);

	devm_kfree(&pdev->dev, dev);
}

static void forward_shutdown_pci(struct pci_dev *pdev)
{
	// FIXME: Is that correct in all use-cases?
	forward_remove_pci(pdev);
}

static int forward_probe_pci(struct pci_dev *pdev, const struct pci_device_id *ent)
{
	struct forward_dev *dev;
	int result = 0;

	dev = devm_kzalloc(&pdev->dev, sizeof(struct forward_dev), GFP_KERNEL);
	if (!dev)
		return -ENOMEM;

	pci_set_drvdata(pdev, dev);
	dev->pci_dev = pdev;
	dev->parent_dev = &pdev->dev;
	dev->type = (enum forward_dev_type)ent->driver_data;

	result = pcim_enable_device(pdev);
	if (result) {
		dev_err(dev->parent_dev, "Unable to enable PCIe device (%d)\n", result);
		goto fail;
	}
	if (!(pci_resource_flags(pdev, 0) & IORESOURCE_MEM)) {
		dev_err(dev->parent_dev, "Incorrect BAR configuration: %08lx\n",
			pci_resource_flags(pdev, 0));
		goto fail;
	}
	result = pcim_iomap_regions(pdev, (1 << 0), FORWARD_MODULE_NAME);
	if (result) {
		dev_err(dev->parent_dev, "Failed to map BAR 0.\n");
		goto fail;
	}
	dev->csr = pcim_iomap_table(pdev)[0];
	dev->csr_size = pci_resource_len(pdev, 0);

	pci_set_master(pdev);

	forward_mcap_init(dev);

	result = forward_probe_common(dev);
	if (result)
		goto fail;

	return 0;

fail:
	forward_remove_pci(pdev);
	return result;
}

int forward_register_extension(struct forward_extension *ext)
{
	struct forward_dev *dev;

	if (ext->version != FORWARD_VERSION_CODE) {
		printk(KERN_ERR FORWARD_MODULE_NAME
		       ": cannot register extension %s - wrong verion (%d.%d.%d, should be %d.%d.%d)\n",
		       ext->name, (ext->version >> 16) & 0xFF, (ext->version >> 8) & 0xFF,
		       (ext->version >> 0) & 0xFF, FORWARD_VERSION_MAJOR, FORWARD_VERSION_MINOR,
		       FORWARD_VERSION_PATCH);

		return -EINVAL;
	}

	mutex_lock(&forward_extensions_lock);
	list_add_tail(&ext->node, &forward_extensions);
	mutex_unlock(&forward_extensions_lock);

	mutex_lock(&forward_devices_lock);
	list_for_each_entry (dev, &forward_devices, list) {
		forward_ext_bind(dev, ext);
	}
	mutex_unlock(&forward_devices_lock);

	printk(KERN_INFO FORWARD_MODULE_NAME ": registered extension %s\n", ext->name);

	return 0;
}
EXPORT_SYMBOL(forward_register_extension);

void forward_unregister_extension(struct forward_extension *ext)
{
	struct forward_dev *dev;

	mutex_lock(&forward_devices_lock);
	list_for_each_entry (dev, &forward_devices, list) {
		forward_ext_unbind(dev, ext);
	}
	mutex_unlock(&forward_devices_lock);

	mutex_lock(&forward_extensions_lock);
	list_del(&ext->node);
	mutex_unlock(&forward_extensions_lock);
}
EXPORT_SYMBOL(forward_unregister_extension);

MODULE_DEVICE_TABLE(pci, forward_pci_ids);

static struct pci_driver forward_pci_driver = {
	.name = FORWARD_MODULE_NAME,
	.id_table = forward_pci_ids,
	.probe = forward_probe_pci,
	.remove = forward_remove_pci,
	.shutdown = forward_shutdown_pci,
};

#if (LINUX_VERSION_CODE >= KERNEL_VERSION(6, 11, 0))
static void forward_remove_plat(struct platform_device *pdev)
#else
static int forward_remove_plat(struct platform_device *pdev)
#endif
{
	struct forward_dev *dev = platform_get_drvdata(pdev);

	if (!dev)
#if (LINUX_VERSION_CODE >= KERNEL_VERSION(6, 11, 0))
		return;
#else
		return 0;
#endif

	forward_remove_common(dev);

	if (dev->csr)
		devm_iounmap(&pdev->dev, dev->csr);
	dev->csr_size = 0;
	dev->csr = NULL;

	platform_set_drvdata(pdev, 0);

	devm_kfree(&pdev->dev, dev);

#if (LINUX_VERSION_CODE < KERNEL_VERSION(6, 11, 0))
	return 0;
#endif
}

static void forward_shutdown_plat(struct platform_device *pdev)
{
	// FIXME: Is that correct in all use-cases?
	forward_remove_plat(pdev);
}

static int forward_probe_plat(struct platform_device *pdev)
{
	struct forward_dev *dev;
	int result = 0;
	const struct of_device_id *of_id;
	struct resource *res = NULL;

	dev = devm_kzalloc(&pdev->dev, sizeof(struct forward_dev), GFP_KERNEL);
	if (!dev)
		return -ENOMEM;

	platform_set_drvdata(pdev, dev);
	dev->plat_dev = pdev;
	dev->parent_dev = &pdev->dev;
	of_id = of_match_node(forward_of_ids, pdev->dev.of_node);
	dev->type = (enum forward_dev_type)of_id->data;

	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	if (!res) {
		dev_err(dev->parent_dev, "Cannot find CSR memory resource!\n");
		goto fail;
	}
	if (!devm_request_mem_region(&pdev->dev, res->start, resource_size(res),
				     FORWARD_MODULE_NAME)) {
		dev_err(dev->parent_dev, "Cannot request CSR memory region!\n");
		goto fail;
	}
	dev->csr = devm_ioremap(&pdev->dev, res->start, resource_size(res));
	if (!dev->csr) {
		dev_err(dev->parent_dev, "Failed to map CSR!\n");
		result = -ENOMEM;
		goto fail;
	}
	dev->csr_size = resource_size(res);

	result = forward_probe_common(dev);
	if (result)
		goto fail;

	return 0;

fail:
	forward_remove_plat(pdev);
	return result;
}

static struct platform_driver forward_plat_driver = {
	.probe = forward_probe_plat,
	.remove = forward_remove_plat,
	.shutdown = forward_shutdown_plat,
	.driver = {
		.name = FORWARD_MODULE_NAME,
		.of_match_table = forward_of_ids,
	},
};

#if ((LINUX_VERSION_CODE >= KERNEL_VERSION(6, 2, 0)) || \
     (RHEL_RELEASE_CODE >= RHEL_RELEASE_VERSION(9, 3)))
static char *forward_devnode(const struct device *dev, umode_t *mode)
#else
static char *forward_devnode(struct device *dev, umode_t *mode)
#endif
{
	size_t size = strlen(dev_name(dev)) + sizeof(FORWARD_CLASS_NAME) + 2;
	char *result = kmalloc(size, GFP_KERNEL);

	snprintf(result, size, "%s/%s", FORWARD_CLASS_NAME, dev_name(dev));

	return result;
}

struct class *forward_class(void)
{
	return module_class;
}
EXPORT_SYMBOL(forward_class);

dev_t forward_dev_number(struct forward_dev *dev, int subdev)
{
	return MKDEV(module_major,
		     dev->number * FORWARD_MAX_DEVICES + (subdev % FORWARD_MAX_DEVICES));
}
EXPORT_SYMBOL(forward_dev_number);

static int forward_init(void)
{
	int status = 0;
	dev_t num;

	printk(KERN_INFO FORWARD_MODULE_NAME
	       ": SoftLab-NSK Forward boards driver v" FORWARD_VERSION_STRING "\n");

	status = alloc_chrdev_region(&num, 0, FORWARD_MAX_DEVICES * FORWARD_MAX_SUBDEVICES,
				     FORWARD_MODULE_NAME);

	if (status < 0) {
		printk(KERN_ERR FORWARD_MODULE_NAME ": Cannot alloc ctl device chrdev region!\n");
		return status;
	}

	module_major = MAJOR(num);
#if ((LINUX_VERSION_CODE >= KERNEL_VERSION(6, 4, 0)) || \
     (RHEL_RELEASE_CODE >= RHEL_RELEASE_VERSION(9, 4)))
	module_class = class_create(FORWARD_CLASS_NAME);
#else
	module_class = class_create(THIS_MODULE, FORWARD_CLASS_NAME);
#endif

	if (IS_ERR(module_class)) {
		printk(KERN_ERR FORWARD_MODULE_NAME ": Cannot create device class!\n");
		return -ENOMEM;
	}

	module_class->devnode = &forward_devnode;

	printk(KERN_INFO FORWARD_MODULE_NAME
	       ": Registered devices major number %d and class \"%s\"\n",
	       module_major, FORWARD_CLASS_NAME);

	status = pci_register_driver(&forward_pci_driver);
	if (status)
		return status;

	status = platform_driver_register(&forward_plat_driver);
	if (status)
		return status;

	request_module_nowait(FORWARD_MODULE_NAME "-v4l2");
	request_module_nowait(FORWARD_MODULE_NAME "-alsa");
	request_module_nowait(FORWARD_MODULE_NAME "-splicer");
	request_module_nowait(FORWARD_MODULE_NAME "-ethernet");

	return status;
}

static void forward_exit(void)
{
	platform_driver_unregister(&forward_plat_driver);
	pci_unregister_driver(&forward_pci_driver);

	if (module_major)
		unregister_chrdev_region(MKDEV(module_major, 0),
					 FORWARD_MAX_DEVICES * FORWARD_MAX_SUBDEVICES);
	if (module_class)
		class_destroy(module_class);
}

module_init(forward_init);
module_exit(forward_exit);

MODULE_LICENSE("GPL");
