/* Copyright (c) 2022-2023, Qualcomm Innovation Center, Inc. All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 and
 * only version 2 as published by the Free Software Foundation.
 *
 * 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.
 */

#include "legacyirq_internal.h"

static inline void update_latency_stat(struct legacy2virtual_irqdata *lvirq,
				       int irq, unsigned long delay)
{
	if (delay >= INTERRUPT_TIME_0 && delay <= INTERRUPT_TIME_1)
		lvirq->schedlatency[irq][BUCKET_B0]++;
	else if (delay > INTERRUPT_TIME_1 && delay <= INTERRUPT_TIME_2)
		lvirq->schedlatency[irq][BUCKET_B1]++;
	else if (delay > INTERRUPT_TIME_2 && delay <= INTERRUPT_TIME_3)
		lvirq->schedlatency[irq][BUCKET_B2]++;
	else if (delay > INTERRUPT_TIME_3 && delay <= INTERRUPT_TIME_4)
		lvirq->schedlatency[irq][BUCKET_B3]++;
	else if (delay > INTERRUPT_TIME_4 && delay <= INTERRUPT_TIME_5)
		lvirq->schedlatency[irq][BUCKET_B4]++;
	else if (delay > INTERRUPT_TIME_5 && delay <= INTERRUPT_TIME_6)
		lvirq->schedlatency[irq][BUCKET_B5]++;
	else if (delay > INTERRUPT_TIME_6 && delay <= INTERRUPT_TIME_7)
		lvirq->schedlatency[irq][BUCKET_B6]++;
	else
		lvirq->schedlatency[irq][BUCKET_B7]++;
}

static inline void update_execution_time(struct legacy2virtual_irqdata *lvirq,
					 int irq, unsigned long time)
{
	if (time >= INTERRUPT_TIME_0 && time <= INTERRUPT_TIME_1)
		lvirq->executiontime[irq][BUCKET_B0]++;
	else if (time > INTERRUPT_TIME_1 && time <= INTERRUPT_TIME_2)
		lvirq->executiontime[irq][BUCKET_B1]++;
	else if (time > INTERRUPT_TIME_2 && time <= INTERRUPT_TIME_3)
		lvirq->executiontime[irq][BUCKET_B2]++;
	else if (time > INTERRUPT_TIME_3 && time <= INTERRUPT_TIME_4)
		lvirq->executiontime[irq][BUCKET_B3]++;
	else if (time > INTERRUPT_TIME_4 && time <= INTERRUPT_TIME_5)
		lvirq->executiontime[irq][BUCKET_B4]++;
	else if (time > INTERRUPT_TIME_5 && time <= INTERRUPT_TIME_6)
		lvirq->executiontime[irq][BUCKET_B5]++;
	else if (time > INTERRUPT_TIME_6 && time <= INTERRUPT_TIME_7)
		lvirq->executiontime[irq][BUCKET_B6]++;
	else
		lvirq->executiontime[irq][BUCKET_B7]++;
}

void cnss_pci_enable_legacy_intx(void __iomem *bar,
		struct pci_dev *pci_dev)
{
	writel(QCN9224_ENABLE_LEGACY_INTTERUPT_BIT,
	       bar + QCN9224_LEGACY_INTX_COMMON);
	writel(PCIE_LOCALREG_LEGACY_INTX_EN_BNK0_VAL,
	       bar + PCIE_LOCALREG_LEGACY_INTX_EN_BNK0);
	writel(PCIE_LOCALREG_LEGACY_INTX_EN_BNK1_VAL,
	       bar + PCIE_LOCALREG_LEGACY_INTX_EN_BNK1);
	writel(PCIE_LOCALREG_LEGACY_INTX_EN_BNK2_VAL,
	       bar + PCIE_LOCALREG_LEGACY_INTX_EN_BNK2);
	writel(PCIE_LOCALREG_LEGACY_INTX_EN_BNK3_VAL,
	       bar + PCIE_LOCALREG_LEGACY_INTX_EN_BNK3);
	writel(PCIE_LOCALREG_LEGACY_INTX_EN_BNK4_VAL,
	       bar + PCIE_LOCALREG_LEGACY_INTX_EN_BNK4);
	pci_intx(pci_dev, 1);

}

void set_lvirq_bar(void *lvirqptr, void *bar)
{
	struct legacy2virtual_irqdata *lvirq =
		(struct legacy2virtual_irqdata *)lvirqptr;
	lvirq->regbase = bar;
}

void clear_lvirq_bar(void *lvirqptr)
{
	struct legacy2virtual_irqdata *lvirq =
		(struct legacy2virtual_irqdata *)lvirqptr;
	lvirq->regbase = NULL;
}

static inline int is_irq_set_in(int *isr, int bit)
{
	return (isr[bit/32] & BIT(bit%32));
}

static inline void qcn9224_process_irq(struct legacy2virtual_irqdata *lvirq,
					 int *isr, unsigned long irqtime,
					 int p, int b)
{
	int irq_pin;
	int irq;
	struct irq_desc *desc;
	/* deliveredtime */
	unsigned long dt;
	/* executiontime */
	unsigned long et;
	/* completedtime */
	unsigned long ct;
	/* schedlatencytime */
	unsigned long sclt;

	irq = intx_prioritymap[p][b];
	if ((is_irq_set_in(isr, irq)) || (test_bit(irq, lvirq->irq_enabled)
	    && test_bit(irq, lvirq->irq_pending))) {
		if (test_bit(irq, lvirq->irq_enabled)) {
			if (test_bit(irq, lvirq->irq_pending)) {
				clear_bit(irq, lvirq->irq_pending);
				STAT_INC(pendingirqdelivered, irq);
			}
			irq_pin = irq_find_mapping(lvirq->domain, irq);
			if (irq_pin > 0) {
				STAT_INC(raisedirq, irq);
				dt = jiffies;
				desc = irq_to_desc(irq_pin);
				if (likely(desc))
					handle_simple_irq(desc);
				ct = jiffies;
				et = ct - dt;
				sclt = dt - irqtime;
				et =  jiffies_to_msecs(et);
				sclt =  jiffies_to_msecs(sclt);
				update_latency_stat(lvirq, irq, sclt);
				update_execution_time(lvirq, irq, et);
			} else {
				STAT_INC(unregisteredirq, irq);
			}
		} else {
			if (test_bit(irq, lvirq->irq_requested)) {
				set_bit(irq, lvirq->irq_pending);
				STAT_INC(pendingirq, irq);
			} else {
				STAT_INC(unregisteredirq, irq);
			}
		}

	}
}

/* legacy intx handler */
static irqreturn_t qcn9224_legacy_hw_irq_handler(int irqnum, void *data)
{
	int isr[5] = {0};
	int p, b;
	unsigned long irqtime = jiffies;
	struct legacy2virtual_irqdata *lvirq =
					(struct legacy2virtual_irqdata *)data;

	/* disable global interrupt */
	iowrite32(0x0, BAR + INTX_COMMON_REG_OFFSET);

	isr[0] = ioread32(BAR + INTX_INT_STS0_REG_OFFSET);
	if (isr[0] & 0x2)
		isr[1] = ioread32(BAR + INTX_INT_STS1_REG_OFFSET);
	if (isr[0] & 0x4)
		isr[2] = ioread32(BAR + INTX_INT_STS2_REG_OFFSET);
	if (isr[0] & 0x8)
		isr[3] = ioread32(BAR + INTX_INT_STS3_REG_OFFSET);
	if (isr[0] & 0x16)
		isr[4] = ioread32(BAR + INTX_INT_STS4_REG_OFFSET);

	for (p = 0; p < INTX_MAX_LEVEL; p++) {
		for (b = 0; (b < INTX_MAX_INTERRUPTS_PER_LEVEL) &&
		     intx_prioritymap[p][b]; b++) {
			qcn9224_process_irq(lvirq, isr, irqtime, p, b);
		}
	}
	/* Enable Global Interrupt */
	iowrite32(1, BAR + INTX_COMMON_REG_OFFSET);
	return IRQ_HANDLED;
}

static void qcn9224_legacy_unmask_irq(struct irq_data *irqd)
{
}
static void qcn9224_legacy_mask_irq(struct irq_data *irqd)
{
}

static void qcn9224_legacy_enable_irq(struct irq_data *irqd)
{
	struct legacy2virtual_irqdata *lvirq = irq_data_get_irq_chip_data(irqd);
	irq_hw_number_t irq = irqd_to_hwirq(irqd);

	STAT_INC(enableirq, irq);
	set_bit(irq, lvirq->irq_enabled);
	iowrite32(IRQ_BIT_IN_REG(irq),
		  BAR + LEGACY_INTX_EN_SET_BANK0 + IRQ_TO_REG_OFFSET(irq));
}

static void qcn9224_legacy_disable_irq(struct irq_data *irqd)
{
	struct legacy2virtual_irqdata *lvirq = irq_data_get_irq_chip_data(irqd);
	irq_hw_number_t irq = irqd_to_hwirq(irqd);

	STAT_INC(disableirq, irq);
	iowrite32(IRQ_BIT_IN_REG(irq),
		  BAR + LEGACY_INTX_EN_CLR_BANK0 + IRQ_TO_REG_OFFSET(irq));
	clear_bit(irq, lvirq->irq_enabled);
}

static int qcn9224_legacy_set_irq_type(struct irq_data *irqd, unsigned int type)
{
	return 0;
}

static struct irq_chip qcn9224_legacy_irq_chip = {
	.name           = "QCNVIC",
	.irq_mask       = qcn9224_legacy_mask_irq,
	.irq_unmask     = qcn9224_legacy_unmask_irq,
	.irq_set_type	= qcn9224_legacy_set_irq_type,
	.irq_enable     = qcn9224_legacy_enable_irq,
	.irq_disable    = qcn9224_legacy_disable_irq,
};

static int qcn9224_irq_map(struct irq_domain *d,
			 unsigned int irq,
			 irq_hw_number_t hw)
{
	struct legacy2virtual_irqdata *lvirq = d->host_data;

	irq_set_chip_and_handler(irq, &qcn9224_legacy_irq_chip,
				 handle_level_irq);
	irq_set_chip_data(irq, lvirq);
	irq_set_noprobe(irq);
	STAT_INC(registeredirq, hw);
	set_bit(hw, lvirq->irq_requested);
	return 0;
}

static const struct irq_domain_ops qcn9224_irq_ops = {
	.map = qcn9224_irq_map,
	.xlate = irq_domain_xlate_twocell,
};

static const struct of_device_id qcom_qcn9224_of_match[] = {
	{ .compatible = "qcom,qcn9224_legacy_irq" },
	{}
};
MODULE_DEVICE_TABLE(of, qcom_qcn9224_of_match);

static ssize_t cnss_statdebug_debug_write(struct file *fp,
					 const char __user *user_buf,
					 size_t count, loff_t *off)
{
	struct legacy2virtual_irqdata *lvirq =
		((struct seq_file *)fp->private_data)->private;
	char buf[64];
	char *cmd;
	unsigned int len = 0;
	int option = 0;
	int i;

	if (!lvirq) {
		pr_emerg("lvirq is not initalized\n");
		return count;
	}

	len = min(count, sizeof(buf) - 1);
	if (copy_from_user(buf, user_buf, len))
		return -EFAULT;

	buf[len] = '\0';
	cmd = (char *)buf;
	if (strstr(cmd, "enable"))
		option = enableirq;
	else if (strstr(cmd, "disable"))
		option = disableirq;
	else if (strstr(cmd, "unregistered"))
		option = unregisteredirq;
	else if (strstr(cmd, "registered"))
		option = registeredirq;
	else if (strstr(cmd, "raised"))
		option = raisedirq;
	else if (strstr(cmd, "pendingirqdelivered"))
		option = pendingirqdelivered;
	else if (strstr(cmd, "pendingirq"))
		option = pendingirq;
	else if (strstr(cmd, "executiontime"))
		option = executiontime;
	else if (strstr(cmd, "schedlatency"))
		option = schedlatency;
	else
		option = maxirq;

	for (i = 0; i < INTX_MAX_INTERRUPTS; i++) {
		switch (option) {
		case enableirq:
			if (lvirq->stats[enableirq][i]) {
				pr_err("E %4d D %4d RAI %4d, REG %4d UNREG %4d P %4d PD %4d %30s[%d]\n",
					lvirq->stats[enableirq][i],
					lvirq->stats[disableirq][i],
					lvirq->stats[raisedirq][i],
					lvirq->stats[registeredirq][i],
					lvirq->stats[unregisteredirq][i],
					lvirq->stats[pendingirq][i],
					lvirq->stats[pendingirqdelivered][i],
					intx_irqname[i], i);
			}
		break;
		case disableirq:
			if (lvirq->stats[disableirq][i]) {
				pr_err("E %4d D %4d RAI %4d, REG %4d UNREG %4d P %4d PD %4d %30s[%d]\n",
					lvirq->stats[enableirq][i],
					lvirq->stats[disableirq][i],
					lvirq->stats[raisedirq][i],
					lvirq->stats[registeredirq][i],
					lvirq->stats[unregisteredirq][i],
					lvirq->stats[pendingirq][i],
					lvirq->stats[pendingirqdelivered][i],
					intx_irqname[i], i);
			}
		break;
		case registeredirq:
			if (lvirq->stats[registeredirq][i]) {
				pr_err("E %4d D %4d RAI %4d, REG %4d UNREG %4d P %4d PD %4d %30s[%d]\n",
					lvirq->stats[enableirq][i],
					lvirq->stats[disableirq][i],
					lvirq->stats[raisedirq][i],
					lvirq->stats[registeredirq][i],
					lvirq->stats[unregisteredirq][i],
					lvirq->stats[pendingirq][i],
					lvirq->stats[pendingirqdelivered][i],
					intx_irqname[i], i);
			}
		break;
		case unregisteredirq:
			if (lvirq->stats[unregisteredirq][i]) {
				pr_err("E %4d D %4d RAI %4d, REG %4d UNREG %4d P %4d PD %4d %30s[%d]\n",
					lvirq->stats[enableirq][i],
					lvirq->stats[disableirq][i],
					lvirq->stats[raisedirq][i],
					lvirq->stats[registeredirq][i],
					lvirq->stats[unregisteredirq][i],
					lvirq->stats[pendingirq][i],
					lvirq->stats[pendingirqdelivered][i],
					intx_irqname[i], i);
			}
		break;
		case raisedirq:
			if (lvirq->stats[raisedirq][i]) {
				pr_err("E %4d D %4d RAI %4d, REG %4d UNREG %4d P %4d PD %4d %30s[%d]\n",
					lvirq->stats[enableirq][i],
					lvirq->stats[disableirq][i],
					lvirq->stats[raisedirq][i],
					lvirq->stats[registeredirq][i],
					lvirq->stats[unregisteredirq][i],
					lvirq->stats[pendingirq][i],
					lvirq->stats[pendingirqdelivered][i],
					intx_irqname[i], i);
			}
		break;
		case pendingirq:
			if (lvirq->stats[pendingirq][i]) {
				pr_err("E %4d D %4d RAI %4d, REG %4d UNREG %4d P %4d PD %4d %30s[%d]\n",
					lvirq->stats[enableirq][i],
					lvirq->stats[disableirq][i],
					lvirq->stats[raisedirq][i],
					lvirq->stats[registeredirq][i],
					lvirq->stats[unregisteredirq][i],
					lvirq->stats[pendingirq][i],
					lvirq->stats[pendingirqdelivered][i],
					intx_irqname[i], i);
			}
		break;
		case pendingirqdelivered:
			if (lvirq->stats[pendingirqdelivered][i]) {
				pr_err("E %4d D %4d RAI %4d, REG %4d UNREG %4d P %4d PD %4d %30s[%d]\n",
					lvirq->stats[enableirq][i],
					lvirq->stats[disableirq][i],
					lvirq->stats[raisedirq][i],
					lvirq->stats[registeredirq][i],
					lvirq->stats[unregisteredirq][i],
					lvirq->stats[pendingirq][i],
					lvirq->stats[pendingirqdelivered][i],
					intx_irqname[i], i);
			}
		break;
		case maxirq:
			pr_err("E %4d D %4d RAI %4d, REG %4d UNREG %4d P %4d PD %4d %30s[%d]\n",
				lvirq->stats[enableirq][i],
				lvirq->stats[disableirq][i],
				lvirq->stats[raisedirq][i],
				lvirq->stats[registeredirq][i],
				lvirq->stats[unregisteredirq][i],
				lvirq->stats[pendingirq][i],
				lvirq->stats[pendingirqdelivered][i],
				intx_irqname[i], i);
		break;
		case executiontime:
			pr_err("%5d %5d %5d %5d %5d %5d %5d %5d %s [%d]",
				lvirq->executiontime[i][BUCKET_B0],
				lvirq->executiontime[i][BUCKET_B1],
				lvirq->executiontime[i][BUCKET_B2],
				lvirq->executiontime[i][BUCKET_B3],
				lvirq->executiontime[i][BUCKET_B4],
				lvirq->executiontime[i][BUCKET_B5],
				lvirq->executiontime[i][BUCKET_B6],
				lvirq->executiontime[i][BUCKET_B7],
				intx_irqname[i], i);
		break;
		case schedlatency:
			pr_err("%5d %5d %5d %5d %5d %5d %5d %5d %s [%d]",
				lvirq->schedlatency[i][BUCKET_B0],
				lvirq->schedlatency[i][BUCKET_B1],
				lvirq->schedlatency[i][BUCKET_B2],
				lvirq->schedlatency[i][BUCKET_B3],
				lvirq->schedlatency[i][BUCKET_B4],
				lvirq->schedlatency[i][BUCKET_B5],
				lvirq->schedlatency[i][BUCKET_B6],
				lvirq->schedlatency[i][BUCKET_B7],
				intx_irqname[i], i);
		break;
		}

	}

	return count;
}

static int cnss_statdebug_debug_show(struct seq_file *s, void *data)
{
	seq_puts(s, "echo enable > statdebug\n");
	seq_puts(s, "echo disable > statdebug\n");
	seq_puts(s, "echo registered > statdebug\n");
	seq_puts(s, "echo unregistered > statdebug\n");
	seq_puts(s, "echo raised > statdebug\n");
	seq_puts(s, "echo pendingirq > statdebug\n");
	seq_puts(s, "echo pendingirqdelivered > statdebug\n");
	seq_puts(s, "echo executiontime > statdebug\n");
	seq_puts(s, "echo schedlatency > statdebug\n");
	seq_puts(s, "echo all > statdebug\n");

	return 0;
}

static int cnss_statdebug_debug_open(struct inode *inode, struct file *file)
{
	return single_open(file, cnss_statdebug_debug_show, inode->i_private);
}

static const struct file_operations cnss_statdebug_debug_fops = {
	.read		= seq_read,
	.write		= cnss_statdebug_debug_write,
	.release	= single_release,
	.open		= cnss_statdebug_debug_open,
	.owner		= THIS_MODULE,
	.llseek		= seq_lseek,
};

static int qcom_qcn9224_probe(struct platform_device *pdev)
{
	struct device_node *n, *irqnode = NULL;
	u32 node_id = 0;
	struct legacy2virtual_irqdata *lvirq;
	char name[10];

	lvirq = kzalloc(sizeof(struct legacy2virtual_irqdata), GFP_KERNEL);
	if (!lvirq)
		return -ENOMEM;

	lvirq->pdev = pdev;
	lvirq_list[lvirq_index++] = lvirq;

	of_property_read_u32(pdev->dev.of_node, "qrtr_node_id", &node_id);
	lvirq->qrtr_node_id = node_id;

	snprintf(name, sizeof(name), "qcnvic%d", node_id - QCN9224_0);
	lvirq->irq_root_dentry = debugfs_create_dir(name, 0);
	if (IS_ERR(lvirq->irq_root_dentry)) {
		pr_err("failed to create debugfs directory(0x%lx)\n",
			PTR_ERR(lvirq->irq_root_dentry));
		return (int)PTR_ERR(lvirq->irq_root_dentry);
	}
	debugfs_create_file("statdebug", 0600, lvirq->irq_root_dentry, lvirq,
			    &cnss_statdebug_debug_fops);

	for_each_available_child_of_node(pdev->dev.of_node, n) {
		if (of_property_read_bool(n, "interrupt-controller")) {
			irqnode = n;
			break;
		}
	}

	lvirq->domain = irq_domain_add_linear(irqnode, INTX_MAX_INTERRUPTS,
					      &qcn9224_irq_ops, lvirq);
	if (!lvirq->domain) {
		pr_err("failed to add irq_domain\n");
		return -ENOMEM;
	}

	platform_set_drvdata(pdev, lvirq);
	return 0;
}

void *cnss_get_lvirq_by_qrtr_id(int qrtr_node_id)
{
	int i;

	for (i = 0; i < lvirq_index; i++) {
		if (qrtr_node_id == lvirq_list[i]->qrtr_node_id)
			return lvirq_list[i];
	}
	return NULL;
}

int qcn9224_register_legacy_irq(void *lvirqptr, int irq)
{
	int ret = 0;
	struct legacy2virtual_irqdata *lvirq =
				(struct legacy2virtual_irqdata *)lvirqptr;

	lvirq->pci_legacy_irq = irq;
	ret = request_irq(irq, qcn9224_legacy_hw_irq_handler,
				   IRQF_ONESHOT, "qcn9224_hwirq", lvirq);
	if (ret < 0) {
		pr_err("PCI wifi IRQ request failed\n");
		return -ENOENT;
	}
	return ret;

}

int qcn9224_unregister_legacy_irq(void *lvirqptr, int irq)
{
	struct legacy2virtual_irqdata *lvirq =
				(struct legacy2virtual_irqdata *)lvirqptr;

	if (lvirq->pci_legacy_irq) {
		disable_irq(lvirq->pci_legacy_irq);
		free_irq(lvirq->pci_legacy_irq, lvirq);
		lvirq->pci_legacy_irq = 0;
	}
	return 0;
}

static int qcom_qcn9224_remove(struct platform_device *pdev)
{
	struct legacy2virtual_irqdata *lvirq = platform_get_drvdata(pdev);

	if (!lvirq)
		return -EINVAL;
	disable_irq(lvirq->pci_legacy_irq);
	free_irq(lvirq->pci_legacy_irq, lvirq);
	irq_domain_remove(lvirq->domain);
	if (lvirq->irq_root_dentry) {
		debugfs_remove_recursive(lvirq->irq_root_dentry);
		lvirq->irq_root_dentry = NULL;
	}
	kfree(lvirq);
	lvirq = NULL;
	return 0;
}

struct platform_driver qcom_qcn9224_driver = {
	.probe = qcom_qcn9224_probe,
	.remove = qcom_qcn9224_remove,
	.driver  = {
		.name  = "qcom,qcn9224_legacy_irq",
		.of_match_table = qcom_qcn9224_of_match,
	},
};

int cnss_legacy_irq_init(void)
{
	return platform_driver_register(&qcom_qcn9224_driver);
}

void cnss_legacy_irq_deinit(void)
{
	platform_driver_unregister(&qcom_qcn9224_driver);
}