/*
   forward-irq.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 <linux/kthread.h>
#include <linux/interrupt.h>

static irqreturn_t forward_irq(int irq, void *data)
{
	u64 irq_time = ktime_to_ns(ktime_get());
	struct forward_dev *dev = data;
	forward_irq_t irq_flags, irq_data, irq_data_mask;
	unsigned long sflags;
	struct list_head *cur;
	struct forward_irq_listener *cur_listener;

	if (!dev->cfg.read_interrupts(dev, &irq_flags, &irq_data, &irq_data_mask))
		return IRQ_NONE;

	spin_lock_irqsave(&dev->listeners_lock, sflags);
	list_for_each (cur, &dev->listeners) {
		int requested = 0;
		forward_irq_t lflags, ldata, lirq;

		cur_listener = list_entry(cur, struct forward_irq_listener, list);

		spin_lock_irqsave(&cur_listener->slock, sflags);

		forward_irq_flags_and(lflags, irq_flags, cur_listener->mask);
		forward_irq_flags_and(ldata, irq_data, irq_data_mask);
		forward_irq_flags_or(lirq, lflags, ldata);
		forward_irq_flags_clear(cur_listener->flags, cur_listener->flags, irq_data_mask);
		forward_irq_flags_or(cur_listener->flags, cur_listener->flags, lirq);
		requested = !forward_irq_flags_is_zero(lflags);

		spin_unlock_irqrestore(&cur_listener->slock, sflags);

		if (requested) {
			cur_listener->time = irq_time;
			if ((cur_listener->type & FORWARD_IRQ_LISTENER_CALLBACK) &&
			    cur_listener->func)
				cur_listener->func(cur_listener->private, &lirq, irq_time);
			if ((cur_listener->type & FORWARD_IRQ_LISTENER_WORKQUEUE) &&
			    cur_listener->wq_func) {
				int q = (cur_listener->work_queue) % dev->irq_wq_count;
				queue_work(dev->irq_wqs[q], &cur_listener->work);
			}
			if (cur_listener->type & FORWARD_IRQ_LISTENER_WAIT)
				wake_up_interruptible(&cur_listener->wait);
		}
	}
	spin_unlock_irqrestore(&dev->listeners_lock, sflags);

	return IRQ_HANDLED;
}

static void forward_irq_work(struct work_struct *work)
{
	unsigned long sflags;
	struct forward_irq_listener *listener =
		container_of(work, struct forward_irq_listener, work);
	void *private;
	forward_irq_t irq;
	u64 timestamp;

	spin_lock_irqsave(&listener->slock, sflags);
	private = listener->private;
	irq = listener->flags;
	timestamp = listener->time;
	spin_unlock_irqrestore(&listener->slock, sflags);

	listener->wq_func(private, &irq, timestamp);
}

forward_irq_t forward_irq_listener_wait(struct forward_irq_listener *listener, bool clear,
					int timeout)
{
	unsigned long sflags;
	forward_irq_t irq = { 0 };

	if (!(listener->type & FORWARD_IRQ_LISTENER_WAIT))
		return irq;

	if (timeout > 0)
		wait_event_interruptible_timeout(
			listener->wait, forward_irq_has_any_irq(listener->flags, listener->mask),
			timeout);
	else
		wait_event_interruptible(listener->wait,
					 forward_irq_has_any_irq(listener->flags, listener->mask));

	spin_lock_irqsave(&listener->slock, sflags);
	irq = listener->flags;
	if (clear)
		forward_irq_flags_clear(listener->flags, listener->flags, listener->mask);
	spin_unlock_irqrestore(&listener->slock, sflags);

	return irq;
}
EXPORT_SYMBOL(forward_irq_listener_wait);

forward_irq_t forward_irq_listener_wait_kthread(struct forward_irq_listener *listener, bool clear,
						int timeout)
{
	unsigned long sflags;
	forward_irq_t irq = { 0 };

	if (!(listener->type & FORWARD_IRQ_LISTENER_WAIT))
		return irq;

	if (timeout > 0)
		wait_event_interruptible_timeout(listener->wait,
						 forward_irq_has_any_irq(listener->flags,
									 listener->mask) ||
							 kthread_should_stop(),
						 timeout);
	else
		wait_event_interruptible(listener->wait,
					 forward_irq_has_any_irq(listener->flags, listener->mask) ||
						 kthread_should_stop());

	spin_lock_irqsave(&listener->slock, sflags);
	irq = listener->flags;
	if (clear)
		forward_irq_flags_clear(listener->flags, listener->flags, listener->mask);
	spin_unlock_irqrestore(&listener->slock, sflags);

	return irq;
}
EXPORT_SYMBOL(forward_irq_listener_wait_kthread);

void forward_irq_listener_clear(struct forward_irq_listener *listener)
{
	unsigned long sflags;

	spin_lock_irqsave(&listener->slock, sflags);
	forward_irq_flags_clear(listener->flags, listener->flags, listener->mask);
	spin_unlock_irqrestore(&listener->slock, sflags);
}
EXPORT_SYMBOL(forward_irq_listener_clear);

void forward_irq_listener_init(struct forward_irq_listener *listener)
{
	init_waitqueue_head(&listener->wait);
	spin_lock_init(&listener->slock);
	INIT_LIST_HEAD(&listener->list);
	INIT_WORK(&listener->work, forward_irq_work);
	listener->type = 0;
	forward_irq_flags_clear_all(listener->flags);
	forward_irq_flags_clear_all(listener->mask);
	listener->func = NULL;
	listener->wq_func = NULL;
	listener->private = listener;
	listener->time = ktime_to_ns(ktime_get());
	listener->work_queue = 0;
	listener->priority = 0;
}
EXPORT_SYMBOL(forward_irq_listener_init);

int forward_irq_listener_add(struct forward_dev *dev, struct forward_irq_listener *listener)
{
	unsigned long sflags;
	struct list_head *it;

	spin_lock_irqsave(&dev->listeners_lock, sflags);
	list_for_each (it, &dev->listeners) {
		struct forward_irq_listener *cur =
			list_entry(it, struct forward_irq_listener, list);
		if (cur->priority > listener->priority)
			break;
	}
	list_add_tail(&listener->list, it);
	spin_unlock_irqrestore(&dev->listeners_lock, sflags);

	return 0;
}
EXPORT_SYMBOL(forward_irq_listener_add);

struct forward_irq_listener *forward_irq_listener_find(struct forward_dev *dev, void *private)
{
	unsigned long sflags;
	struct list_head *cur = NULL;
	struct forward_irq_listener *cur_listener = NULL;

	spin_lock_irqsave(&dev->listeners_lock, sflags);
	list_for_each (cur, &dev->listeners) {
		cur_listener = list_entry(cur, struct forward_irq_listener, list);
		if (cur_listener->private == private)
			break;
	}
	spin_unlock_irqrestore(&dev->listeners_lock, sflags);
	return cur_listener;
}
EXPORT_SYMBOL(forward_irq_listener_find);

int forward_irq_listener_remove(struct forward_dev *dev, struct forward_irq_listener *listener)
{
	unsigned long sflags;

	spin_lock_irqsave(&dev->listeners_lock, sflags);
	list_del(&listener->list);
	spin_unlock_irqrestore(&dev->listeners_lock, sflags);

	if (listener->type & FORWARD_IRQ_LISTENER_WORKQUEUE)
		cancel_work_sync(&listener->work);

	return 0;
}
EXPORT_SYMBOL(forward_irq_listener_remove);

void forward_irq_remove(struct forward_dev *dev)
{
	int i;
	forward_irq_t mask;

	if (dev->irq <= 0)
		return;

	forward_irq_flags_set_all(mask);
	dev->cfg.disable_interrupts(dev, &mask);

	for (i = 0; i < dev->irq_wq_count; i++) {
		if (dev->irq_wqs[i])
			destroy_workqueue(dev->irq_wqs[i]);
	}

	devm_free_irq(dev->parent_dev, dev->irq, dev);

	if (dev->pci_dev)
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 8, 0)
		pci_free_irq_vectors(dev->pci_dev);
#else
		pci_disable_msi(dev->pci_dev);
#endif
}

int forward_irq_probe(struct forward_dev *dev)
{
	forward_irq_t mask;
	int result = 0;
	int i;

	INIT_LIST_HEAD(&dev->listeners);
	spin_lock_init(&dev->listeners_lock);

	forward_irq_flags_set_all(mask);
	dev->cfg.disable_interrupts(dev, &mask);

	dev->irq_wq_count = min(FORWARD_IRQ_MAX_WQ, dev->cfg.io_count / FORWARD_IRQ_IO_PER_WQ);

	if (dev->pci_dev) {
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 8, 0)
		int n = pci_alloc_irq_vectors(dev->pci_dev, 1, 1, PCI_IRQ_MSI | PCI_IRQ_MSIX);
		if (n < 0) {
			forward_warn(dev, "cannot use MSI on device, fallback to INTx");
			n = pci_alloc_irq_vectors(dev->pci_dev, 1, 1, PCI_IRQ_ALL_TYPES);
		}
		if (n >= 1)
			dev->irq = pci_irq_vector(dev->pci_dev, 0);
#else
		if (pci_enable_msi(dev->pci_dev)) {
			forward_warn(dev, "cannot use MSI on device, fallback to INTx");
		}
		dev->irq = dev->pci_dev->irq;
#endif
	} else if (dev->plat_dev)
		dev->irq = platform_get_irq(dev->plat_dev, 0);

	if (dev->irq < 0) {
		forward_warn(dev, "device has no interrupts");
		result = dev->irq;
		goto fail;
	}

	result = devm_request_irq(dev->parent_dev, dev->irq, forward_irq, IRQF_SHARED,
				  dev->dev_name, dev);
	if (result) {
		forward_err(dev, "Cannot register IRQ: %d", result);
		goto fail;
	}

	for (i = 0; i < dev->irq_wq_count; i++) {
		dev->irq_wqs[i] = alloc_ordered_workqueue("fd-%llu-%d", WQ_UNBOUND | WQ_HIGHPRI,
							  dev->soft_id, i);
		if (!dev->irq_wqs[i]) {
			result = -ENOMEM;
			goto fail;
		}
	}

	dev->cfg.enable_interrupts(dev, &mask);

	return 0;

fail:
	forward_irq_remove(dev);

	return result;
}
