// SPDX-License-Identifier: GPL-2.0-only
/*
 * Copyright (C) 2024-2025 ARM Limited, All Rights Reserved.
 */
#define pr_fmt(fmt)	"GICv5 IWB: " fmt

#include <linux/acpi.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/msi.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_platform.h>

#include <linux/irqchip.h>
#include <linux/irqchip/arm-gic-v5.h>

struct gicv5_iwb_chip_data {
	void __iomem	*iwb_base;
	u16		nr_regs;
};

static u32 iwb_readl_relaxed(struct gicv5_iwb_chip_data *iwb_node, const u32 reg_offset)
{
	return readl_relaxed(iwb_node->iwb_base + reg_offset);
}

static void iwb_writel_relaxed(struct gicv5_iwb_chip_data *iwb_node, const u32 val,
			       const u32 reg_offset)
{
	writel_relaxed(val, iwb_node->iwb_base + reg_offset);
}

static int gicv5_iwb_wait_for_wenabler(struct gicv5_iwb_chip_data *iwb_node)
{
	return gicv5_wait_for_op_atomic(iwb_node->iwb_base, GICV5_IWB_WENABLE_STATUSR,
					GICV5_IWB_WENABLE_STATUSR_IDLE, NULL);
}

static int __gicv5_iwb_set_wire_enable(struct gicv5_iwb_chip_data *iwb_node,
				       u32 iwb_wire, bool enable)
{
	u32 n = iwb_wire / 32;
	u8 i = iwb_wire % 32;
	u32 val;

	if (n >= iwb_node->nr_regs) {
		pr_err("IWB_WENABLER<n> is invalid for n=%u\n", n);
		return -EINVAL;
	}

	/*
	 * Enable IWB wire/pin at this point
	 * Note: This is not the same as enabling the interrupt
	 */
	val = iwb_readl_relaxed(iwb_node, GICV5_IWB_WENABLER + (4 * n));
	if (enable)
		val |= BIT(i);
	else
		val &= ~BIT(i);
	iwb_writel_relaxed(iwb_node, val, GICV5_IWB_WENABLER + (4 * n));

	return gicv5_iwb_wait_for_wenabler(iwb_node);
}

static int gicv5_iwb_enable_wire(struct gicv5_iwb_chip_data *iwb_node,
				 u32 iwb_wire)
{
	return __gicv5_iwb_set_wire_enable(iwb_node, iwb_wire, true);
}

static int gicv5_iwb_disable_wire(struct gicv5_iwb_chip_data *iwb_node,
				  u32 iwb_wire)
{
	return __gicv5_iwb_set_wire_enable(iwb_node, iwb_wire, false);
}

static void gicv5_iwb_irq_disable(struct irq_data *d)
{
	struct gicv5_iwb_chip_data *iwb_node = irq_data_get_irq_chip_data(d);

	gicv5_iwb_disable_wire(iwb_node, d->hwirq);
	irq_chip_disable_parent(d);
}

static void gicv5_iwb_irq_enable(struct irq_data *d)
{
	struct gicv5_iwb_chip_data *iwb_node = irq_data_get_irq_chip_data(d);

	gicv5_iwb_enable_wire(iwb_node, d->hwirq);
	irq_chip_enable_parent(d);
}

static int gicv5_iwb_set_type(struct irq_data *d, unsigned int type)
{
	struct gicv5_iwb_chip_data *iwb_node = irq_data_get_irq_chip_data(d);
	u32 iwb_wire, n, wtmr;
	u8 i;

	iwb_wire = d->hwirq;
	i = iwb_wire % 32;
	n = iwb_wire / 32;

	if (n >= iwb_node->nr_regs) {
		pr_err_once("reg %u out of range\n", n);
		return -EINVAL;
	}

	wtmr = iwb_readl_relaxed(iwb_node, GICV5_IWB_WTMR + (4 * n));

	switch (type) {
	case IRQ_TYPE_LEVEL_HIGH:
	case IRQ_TYPE_LEVEL_LOW:
		wtmr |= BIT(i);
		break;
	case IRQ_TYPE_EDGE_RISING:
	case IRQ_TYPE_EDGE_FALLING:
		wtmr &= ~BIT(i);
		break;
	default:
		pr_debug("unexpected wire trigger mode");
		return -EINVAL;
	}

	iwb_writel_relaxed(iwb_node, wtmr, GICV5_IWB_WTMR + (4 * n));

	return 0;
}

static void gicv5_iwb_domain_set_desc(msi_alloc_info_t *alloc_info, struct msi_desc *desc)
{
	alloc_info->desc = desc;
	alloc_info->hwirq = (u32)desc->data.icookie.value;
}

static int gicv5_iwb_irq_domain_translate(struct irq_domain *d, struct irq_fwspec *fwspec,
					  irq_hw_number_t *hwirq,
					  unsigned int *type)
{
	if (is_of_node(fwspec->fwnode)) {

		if (fwspec->param_count < 2)
			return -EINVAL;

		/*
		 * param[0] is be the wire
		 * param[1] is the interrupt type
		 */
		*hwirq = fwspec->param[0];
		*type = fwspec->param[1] & IRQ_TYPE_SENSE_MASK;
	}

	if (is_acpi_device_node(fwspec->fwnode)) {

		if (fwspec->param_count < 2)
			return -EINVAL;

		/*
		 * Extract the wire from param[0]
		 * param[1] is the interrupt type
		 */
		*hwirq = FIELD_GET(GICV5_GSI_IWB_WIRE, fwspec->param[0]);
		*type = fwspec->param[1] & IRQ_TYPE_SENSE_MASK;
	}

	return 0;
}

static void gicv5_iwb_write_msi_msg(struct irq_data *d, struct msi_msg *msg) {}

static const struct msi_domain_template iwb_msi_template = {
	.chip = {
		.name			= "GICv5-IWB",
		.irq_mask		= irq_chip_mask_parent,
		.irq_unmask		= irq_chip_unmask_parent,
		.irq_enable		= gicv5_iwb_irq_enable,
		.irq_disable		= gicv5_iwb_irq_disable,
		.irq_eoi		= irq_chip_eoi_parent,
		.irq_set_type		= gicv5_iwb_set_type,
		.irq_write_msi_msg	= gicv5_iwb_write_msi_msg,
		.irq_set_affinity	= irq_chip_set_affinity_parent,
		.irq_get_irqchip_state	= irq_chip_get_parent_state,
		.irq_set_irqchip_state	= irq_chip_set_parent_state,
		.flags			= IRQCHIP_SET_TYPE_MASKED |
					  IRQCHIP_SKIP_SET_WAKE |
					  IRQCHIP_MASK_ON_SUSPEND,
	},

	.ops = {
		.set_desc		= gicv5_iwb_domain_set_desc,
		.msi_translate		= gicv5_iwb_irq_domain_translate,
	},

	.info = {
		.bus_token		= DOMAIN_BUS_WIRED_TO_MSI,
		.flags			= MSI_FLAG_USE_DEV_FWNODE,
	},

	.alloc_info = {
		.flags			= MSI_ALLOC_FLAGS_FIXED_MSG_DATA,
	},
};

static bool gicv5_iwb_create_device_domain(struct device *dev, unsigned int size,
				     struct gicv5_iwb_chip_data *iwb_node)
{
	if (WARN_ON_ONCE(!dev->msi.domain))
		return false;

	return msi_create_device_irq_domain(dev, MSI_DEFAULT_DOMAIN,
					    &iwb_msi_template, size,
					    NULL, iwb_node);
}

static struct gicv5_iwb_chip_data *
gicv5_iwb_init_bases(void __iomem *iwb_base, struct platform_device *pdev)
{
	u32 nr_wires, idr0, cr0;
	unsigned int n;
	int ret;

	struct gicv5_iwb_chip_data *iwb_node __free(kfree) = kzalloc_obj(*iwb_node);
	if (!iwb_node)
		return ERR_PTR(-ENOMEM);

	iwb_node->iwb_base = iwb_base;

	idr0 = iwb_readl_relaxed(iwb_node, GICV5_IWB_IDR0);
	nr_wires = (FIELD_GET(GICV5_IWB_IDR0_IW_RANGE, idr0) + 1) * 32;

	cr0 = iwb_readl_relaxed(iwb_node, GICV5_IWB_CR0);
	if (!FIELD_GET(GICV5_IWB_CR0_IWBEN, cr0)) {
		dev_err(&pdev->dev, "IWB must be enabled in firmware\n");
		return ERR_PTR(-EINVAL);
	}

	iwb_node->nr_regs = FIELD_GET(GICV5_IWB_IDR0_IW_RANGE, idr0) + 1;

	for (n = 0; n < iwb_node->nr_regs; n++)
		iwb_writel_relaxed(iwb_node, 0, GICV5_IWB_WENABLER + (sizeof(u32) * n));

	ret = gicv5_iwb_wait_for_wenabler(iwb_node);
	if (ret)
		return ERR_PTR(ret);

	if (!gicv5_iwb_create_device_domain(&pdev->dev, nr_wires, iwb_node))
		return ERR_PTR(-ENOMEM);

	return_ptr(iwb_node);
}

static int gicv5_iwb_device_probe(struct platform_device *pdev)
{
	struct gicv5_iwb_chip_data *iwb_node;
	void __iomem *iwb_base;
	struct resource *res;

	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	if (!res)
		return -EINVAL;

	iwb_base = devm_ioremap(&pdev->dev, res->start, resource_size(res));
	if (!iwb_base) {
		dev_err(&pdev->dev, "failed to ioremap %pR\n", res);
		return -ENOMEM;
	}

	iwb_node = gicv5_iwb_init_bases(iwb_base, pdev);
	if (IS_ERR(iwb_node))
		return PTR_ERR(iwb_node);

	return 0;
}

static const struct of_device_id gicv5_iwb_of_match[] = {
	{ .compatible = "arm,gic-v5-iwb" },
	{ /* END */ }
};
MODULE_DEVICE_TABLE(of, gicv5_iwb_of_match);

#ifdef CONFIG_ACPI
static const struct acpi_device_id iwb_acpi_match[] = {
	{ "ARMH0003", 0 },
	{}
};
#endif

static struct platform_driver gicv5_iwb_platform_driver = {
	.driver = {
		.name			= "GICv5 IWB",
		.of_match_table		= gicv5_iwb_of_match,
		.acpi_match_table	= ACPI_PTR(iwb_acpi_match),
		.suppress_bind_attrs	= true,
	},
	.probe				= gicv5_iwb_device_probe,
};

module_platform_driver(gicv5_iwb_platform_driver);
