// SPDX-FileCopyrightText: Copyright (c) 2019-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: GPL-2.0-only

#include <dce.h>
#include <linux/module.h>
#include <linux/interrupt.h>
#include <linux/of_device.h>
#include <linux/of_address.h>
#include <linux/of_platform.h>
#include <linux/platform_device.h>
#include <linux/device.h>

/**
 * The following platform info is needed for backdoor
 * booting of dce.
 */
static const struct dce_platform_data t234_dce_platform_data = {
	.stream_id = 0x08,
	.phys_stream_id = 0x7f,
	.fw_carveout_id = 9,
	.hsp_id = 0x0,
	.fw_vmindex = 0,
	.fw_name = "display-t234-dce.bin",
	.fw_dce_addr = 0x40000000,
	.fw_info_valid = true,
	.use_physical_id = false,
};

__weak const struct of_device_id tegra_dce_of_match[] = {
	{
		.compatible = "nvidia,tegra234-dce",
		.data = (struct dce_platform_data *)&t234_dce_platform_data
	},
	{ },
};
MODULE_DEVICE_TABLE(of, tegra_dce_of_match);

/**
 * dce_get_pdata_dce - inline function to get the tegra_dce pointer
 *						from platform devicve.
 *
 * @pdev : Pointer to the platform device data structure.
 *
 * Return :  Pointer pointing to tegra_dce data structure.
 */
static inline struct tegra_dce *dce_get_pdata_dce(struct platform_device *pdev)
{
	return (&((struct dce_device *)dev_get_drvdata(&pdev->dev))->d);
}

/**
 * dce_get_tegra_dce_from_dev - inline function to get the tegra_dce pointer
 *						from devicve struct.
 *
 * @pdev : Pointer to the device data structure.
 *
 * Return :  Pointer pointing to tegra_dce data structure.
 */
static inline struct tegra_dce *dce_get_tegra_dce_from_dev(struct device *dev)
{
	return (&((struct dce_device *)dev_get_drvdata(dev))->d);
}

/**
 * dce_init_dev_data - Function to initialize the dce device data structure.
 *
 * @pdev : Pointer to Linux's platform device used for registering DCE.
 *
 * Primarily used during initialization sequence and is expected to be called
 * from probe only.
 *
 * Return : 0 if success else the corresponding error value.
 */
static int dce_init_dev_data(struct platform_device *pdev, struct dce_platform_data *pdata)
{
	struct device *dev = &pdev->dev;
	struct dce_device *d_dev = NULL;

	d_dev = devm_kzalloc(dev, sizeof(*d_dev), GFP_KERNEL);
	if (!d_dev)
		return -ENOMEM;

	d_dev->dev = dev;
	d_dev->pdata = pdata;
	d_dev->regs = of_iomap(dev->of_node, 0);
	if (!d_dev->regs) {
		dev_err(dev, "failed to map dce cluster IO space\n");
		return -EINVAL;
	}

	dev_set_drvdata(dev, d_dev);
	return 0;
}

/**
 * dce_isr - Handles dce interrupts
 */
static irqreturn_t dce_isr(int irq, void *data)
{
	struct tegra_dce *d = data;

	dce_mailbox_isr(d);

	return IRQ_HANDLED;
}

static void dce_set_irqs(struct platform_device *pdev, bool en)
{
	int i = 0;
	struct tegra_dce *d;
	struct dce_device *d_dev = NULL;

	d_dev = dev_get_drvdata(&pdev->dev);
	d = dce_get_pdata_dce(pdev);

	for (i = 0; i < d_dev->max_cpu_irqs; i++) {
		if (en)
			enable_irq(d->irq[i]);
		else
			disable_irq(d->irq[i]);
	}
}

/**
 * dce_req_interrupts - function to initialize CPU irqs for DCE cpu driver.
 *
 * @pdev : Pointet to Dce Linux Platform Device.
 *
 * Return : 0 if success else the corresponding error value.
 */
static int dce_req_interrupts(struct platform_device *pdev)
{
	int i = 0;
	int ret = 0;
	int no_ints = 0;
	struct tegra_dce *d;
	struct dce_device *d_dev = NULL;

	d_dev = dev_get_drvdata(&pdev->dev);
	d = dce_get_pdata_dce(pdev);

	no_ints = platform_irq_count(pdev);
	if (no_ints == 0) {
		dev_err(&pdev->dev,
			"Invalid number of interrupts configured = %d",
			no_ints);
		return -EINVAL;
	}

	d_dev->max_cpu_irqs = no_ints;

	for (i = 0; i < no_ints; i++) {
		ret = platform_get_irq(pdev, i);
		if (ret < 0) {
			dev_err(&pdev->dev,
				"Getting dce intr lines failed with ret = %d",
				ret);
			return ret;
		}
		d->irq[i] = ret;
		ret = devm_request_threaded_irq(&pdev->dev, d->irq[i],
				NULL, dce_isr, IRQF_ONESHOT, "tegra_dce_isr",
				d);
		if (ret) {
			dev_err(&pdev->dev,
				"failed to request irq @ with ret = %d\n",
				ret);
		}
		disable_irq(d->irq[i]);
	}
	return ret;
}

static int match_display_dev(struct device *dev, const void *data)
{
	if ((dev != NULL) && (dev->of_node != NULL)) {
		if (of_device_is_compatible(dev->of_node, "nvidia,tegra234-display"))
			return 1;
	}

	return 0;
}

static int tegra_dce_probe(struct platform_device *pdev)
{
	int err = 0;
	struct tegra_dce *d = NULL;
	struct device *dev = &pdev->dev;
	struct dce_platform_data *pdata = NULL;
	const struct of_device_id *match = NULL;
	struct device *c_dev;
	struct device_link *link;

	match = of_match_device(tegra_dce_of_match, dev);
	if (!match) {
		dev_info(dev, "no device match found\n");
		return -ENODEV;
	}

	pdata = (struct dce_platform_data *)match->data;

	WARN_ON(!pdata);
	if (!pdata) {
		dev_info(dev, "no platform data\n");
		err = -ENODATA;
		goto err_get_pdata;
	}

	err = dce_init_dev_data(pdev, pdata);
	if (err) {
		dev_err(dev, "failed to init device data with err = %d\n",
			err);
		goto os_init_err;
	}

	err = dce_req_interrupts(pdev);
	if (err) {
		dev_err(dev, "failed to get interrupts with err = %d\n",
			err);
		goto req_intr_err;
	}

	d = dce_get_pdata_dce(pdev);

	/**
	 * TODO: Get HSP_ID from DT
	 */
	d->hsp_id = pdata->hsp_id;


	err = dce_driver_init(d);
	if (err) {
		dce_err(d, "DCE Driver Init Failed");
		goto err_driver_init;
	}

	dce_set_irqs(pdev, true);

#ifdef CONFIG_DEBUG_FS
	dce_init_debug(d);
#endif

	c_dev = bus_find_device(&platform_bus_type, NULL, NULL, match_display_dev);
	if (c_dev != NULL) {
		dce_info(d, "Found display consumer device");
		link = device_link_add(c_dev, dev,
				       DL_FLAG_PM_RUNTIME | DL_FLAG_AUTOREMOVE_SUPPLIER);
		if (link == NULL) {
			dce_err(d, "Failed to create device link to %s\n", dev_name(c_dev));
			return -EINVAL;
		}
	}

	/**
	 * FIXME: Allow tegra_dce.ko unloading.
	 */
	if (!try_module_get(THIS_MODULE)) {
		dce_info(d, "Failed to get lock of DCE Module.\n");
		dce_info(d, "modprobe --remove of kernel modules depending on tegra_dce.ko will fail.\n");
	}

	return 0;

req_intr_err:
os_init_err:
err_get_pdata:
err_driver_init:
	return err;
}

static int tegra_dce_remove(struct platform_device *pdev)
{
	/* TODO */
	struct tegra_dce *d =
			dce_get_pdata_dce(pdev);

#ifdef CONFIG_DEBUG_FS
	dce_remove_debug(d);
#endif

	dce_set_irqs(pdev, false);
	dce_driver_deinit(d);
	return 0;
}

#ifdef CONFIG_PM
static int dce_pm_suspend(struct device *dev)
{
	struct tegra_dce *d = dce_get_tegra_dce_from_dev(dev);

	return dce_pm_enter_sc7(d);
}

static int dce_pm_resume(struct device *dev)
{
	struct tegra_dce *d = dce_get_tegra_dce_from_dev(dev);

	return dce_pm_exit_sc7(d);
}

static const struct dev_pm_ops dce_pm_ops = {
	.suspend = dce_pm_suspend,
	.resume  = dce_pm_resume,
};
#endif

static struct platform_driver tegra_dce_driver = {
	.driver = {
		.name   = "tegra-dce",
		.of_match_table =
			of_match_ptr(tegra_dce_of_match),
#ifdef CONFIG_PM
		.pm	= &dce_pm_ops,
#endif
	},
	.probe = tegra_dce_probe,
	.remove = tegra_dce_remove,
};
module_platform_driver(tegra_dce_driver);

MODULE_DESCRIPTION("DCE Linux driver");
MODULE_AUTHOR("NVIDIA");
MODULE_LICENSE("GPL v2");
