// SPDX-License-Identifier: GPL-2.0+
/*
 * Copyright 2021-2022 Bootlin
 * Author: Paul Kocialkowski <paul.kocialkowski@bootlin.com>
 */

#include <linux/clk.h>
#include <linux/dma-mapping.h>
#include <linux/err.h>
#include <linux/interrupt.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/platform_device.h>
#include <linux/pm_runtime.h>
#include <linux/regmap.h>
#include <linux/reset.h>
#include <media/v4l2-device.h>
#include <media/v4l2-mc.h>

#include "sun6i_isp.h"
#include "sun6i_isp_capture.h"
#include "sun6i_isp_params.h"
#include "sun6i_isp_proc.h"
#include "sun6i_isp_reg.h"

/* Helpers */

u32 sun6i_isp_load_read(struct sun6i_isp_device *isp_dev, u32 offset)
{
	u32 *data = (u32 *)(isp_dev->tables.load.data + offset);

	return *data;
}

void sun6i_isp_load_write(struct sun6i_isp_device *isp_dev, u32 offset,
			  u32 value)
{
	u32 *data = (u32 *)(isp_dev->tables.load.data + offset);

	*data = value;
}

/* State */

/*
 * The ISP works with a load buffer, which gets copied to the actual registers
 * by the hardware before processing a frame when a specific flag is set.
 * This is represented by tracking the ISP state in the different parts of
 * the code with explicit sync points:
 * - state update: to update the load buffer for the next frame if necessary;
 * - state complete: to indicate that the state update was applied.
 */

static void sun6i_isp_state_ready(struct sun6i_isp_device *isp_dev)
{
	struct regmap *regmap = isp_dev->regmap;
	u32 value;

	regmap_read(regmap, SUN6I_ISP_FE_CTRL_REG, &value);
	value |= SUN6I_ISP_FE_CTRL_PARA_READY;
	regmap_write(regmap, SUN6I_ISP_FE_CTRL_REG, value);
}

static void sun6i_isp_state_complete(struct sun6i_isp_device *isp_dev)
{
	unsigned long flags;

	spin_lock_irqsave(&isp_dev->state_lock, flags);

	sun6i_isp_capture_state_complete(isp_dev);
	sun6i_isp_params_state_complete(isp_dev);

	spin_unlock_irqrestore(&isp_dev->state_lock, flags);
}

void sun6i_isp_state_update(struct sun6i_isp_device *isp_dev, bool ready_hold)
{
	bool update = false;
	unsigned long flags;

	spin_lock_irqsave(&isp_dev->state_lock, flags);

	sun6i_isp_capture_state_update(isp_dev, &update);
	sun6i_isp_params_state_update(isp_dev, &update);

	if (update && !ready_hold)
		sun6i_isp_state_ready(isp_dev);

	spin_unlock_irqrestore(&isp_dev->state_lock, flags);
}

/* Tables */

static int sun6i_isp_table_setup(struct sun6i_isp_device *isp_dev,
				 struct sun6i_isp_table *table)
{
	table->data = dma_alloc_coherent(isp_dev->dev, table->size,
					 &table->address, GFP_KERNEL);
	if (!table->data)
		return -ENOMEM;

	return 0;
}

static void sun6i_isp_table_cleanup(struct sun6i_isp_device *isp_dev,
				    struct sun6i_isp_table *table)
{
	dma_free_coherent(isp_dev->dev, table->size, table->data,
			  table->address);
}

void sun6i_isp_tables_configure(struct sun6i_isp_device *isp_dev)
{
	struct regmap *regmap = isp_dev->regmap;

	regmap_write(regmap, SUN6I_ISP_REG_LOAD_ADDR_REG,
		     SUN6I_ISP_ADDR_VALUE(isp_dev->tables.load.address));

	regmap_write(regmap, SUN6I_ISP_REG_SAVE_ADDR_REG,
		     SUN6I_ISP_ADDR_VALUE(isp_dev->tables.save.address));

	regmap_write(regmap, SUN6I_ISP_LUT_TABLE_ADDR_REG,
		     SUN6I_ISP_ADDR_VALUE(isp_dev->tables.lut.address));

	regmap_write(regmap, SUN6I_ISP_DRC_TABLE_ADDR_REG,
		     SUN6I_ISP_ADDR_VALUE(isp_dev->tables.drc.address));

	regmap_write(regmap, SUN6I_ISP_STATS_ADDR_REG,
		     SUN6I_ISP_ADDR_VALUE(isp_dev->tables.stats.address));
}

static int sun6i_isp_tables_setup(struct sun6i_isp_device *isp_dev,
				  const struct sun6i_isp_variant *variant)
{
	struct sun6i_isp_tables *tables = &isp_dev->tables;
	int ret;

	tables->load.size = variant->table_load_save_size;
	ret = sun6i_isp_table_setup(isp_dev, &tables->load);
	if (ret)
		return ret;

	tables->save.size = variant->table_load_save_size;
	ret = sun6i_isp_table_setup(isp_dev, &tables->save);
	if (ret)
		return ret;

	tables->lut.size = variant->table_lut_size;
	ret = sun6i_isp_table_setup(isp_dev, &tables->lut);
	if (ret)
		return ret;

	tables->drc.size = variant->table_drc_size;
	ret = sun6i_isp_table_setup(isp_dev, &tables->drc);
	if (ret)
		return ret;

	tables->stats.size = variant->table_stats_size;
	ret = sun6i_isp_table_setup(isp_dev, &tables->stats);
	if (ret)
		return ret;

	return 0;
}

static void sun6i_isp_tables_cleanup(struct sun6i_isp_device *isp_dev)
{
	struct sun6i_isp_tables *tables = &isp_dev->tables;

	sun6i_isp_table_cleanup(isp_dev, &tables->stats);
	sun6i_isp_table_cleanup(isp_dev, &tables->drc);
	sun6i_isp_table_cleanup(isp_dev, &tables->lut);
	sun6i_isp_table_cleanup(isp_dev, &tables->save);
	sun6i_isp_table_cleanup(isp_dev, &tables->load);
}

/* Media */

static const struct media_device_ops sun6i_isp_media_ops = {
	.link_notify = v4l2_pipeline_link_notify,
};

/* V4L2 */

static int sun6i_isp_v4l2_setup(struct sun6i_isp_device *isp_dev)
{
	struct sun6i_isp_v4l2 *v4l2 = &isp_dev->v4l2;
	struct v4l2_device *v4l2_dev = &v4l2->v4l2_dev;
	struct media_device *media_dev = &v4l2->media_dev;
	struct device *dev = isp_dev->dev;
	int ret;

	/* Media Device */

	strscpy(media_dev->model, SUN6I_ISP_DESCRIPTION,
		sizeof(media_dev->model));
	media_dev->ops = &sun6i_isp_media_ops;
	media_dev->hw_revision = 0;
	media_dev->dev = dev;

	media_device_init(media_dev);

	ret = media_device_register(media_dev);
	if (ret) {
		dev_err(dev, "failed to register media device\n");
		return ret;
	}

	/* V4L2 Device */

	v4l2_dev->mdev = media_dev;

	ret = v4l2_device_register(dev, v4l2_dev);
	if (ret) {
		dev_err(dev, "failed to register v4l2 device\n");
		goto error_media;
	}

	return 0;

error_media:
	media_device_unregister(media_dev);
	media_device_cleanup(media_dev);

	return ret;
}

static void sun6i_isp_v4l2_cleanup(struct sun6i_isp_device *isp_dev)
{
	struct sun6i_isp_v4l2 *v4l2 = &isp_dev->v4l2;

	media_device_unregister(&v4l2->media_dev);
	v4l2_device_unregister(&v4l2->v4l2_dev);
	media_device_cleanup(&v4l2->media_dev);
}

/* Platform */

static irqreturn_t sun6i_isp_interrupt(int irq, void *private)
{
	struct sun6i_isp_device *isp_dev = private;
	struct regmap *regmap = isp_dev->regmap;
	u32 status = 0, enable = 0;

	regmap_read(regmap, SUN6I_ISP_FE_INT_STA_REG, &status);
	regmap_read(regmap, SUN6I_ISP_FE_INT_EN_REG, &enable);

	if (!status)
		return IRQ_NONE;
	else if (!(status & enable))
		goto complete;

	/*
	 * The ISP working cycle starts with a params-load, which makes the
	 * state from the load buffer active. Then it starts processing the
	 * frame and gives a finish interrupt. Soon after that, the next state
	 * coming from the load buffer will be applied for the next frame,
	 * giving a params-load as well.
	 *
	 * Because both frame finish and params-load are received almost
	 * at the same time (one ISR call), handle them in chronology order.
	 */

	if (status & SUN6I_ISP_FE_INT_STA_FINISH)
		sun6i_isp_capture_finish(isp_dev);

	if (status & SUN6I_ISP_FE_INT_STA_PARA_LOAD) {
		sun6i_isp_state_complete(isp_dev);
		sun6i_isp_state_update(isp_dev, false);
	}

complete:
	regmap_write(regmap, SUN6I_ISP_FE_INT_STA_REG, status);

	return IRQ_HANDLED;
}

static int sun6i_isp_suspend(struct device *dev)
{
	struct sun6i_isp_device *isp_dev = dev_get_drvdata(dev);

	reset_control_assert(isp_dev->reset);
	clk_disable_unprepare(isp_dev->clock_ram);
	clk_disable_unprepare(isp_dev->clock_mod);

	return 0;
}

static int sun6i_isp_resume(struct device *dev)
{
	struct sun6i_isp_device *isp_dev = dev_get_drvdata(dev);
	int ret;

	ret = reset_control_deassert(isp_dev->reset);
	if (ret) {
		dev_err(dev, "failed to deassert reset\n");
		return ret;
	}

	ret = clk_prepare_enable(isp_dev->clock_mod);
	if (ret) {
		dev_err(dev, "failed to enable module clock\n");
		goto error_reset;
	}

	ret = clk_prepare_enable(isp_dev->clock_ram);
	if (ret) {
		dev_err(dev, "failed to enable ram clock\n");
		goto error_clock_mod;
	}

	return 0;

error_clock_mod:
	clk_disable_unprepare(isp_dev->clock_mod);

error_reset:
	reset_control_assert(isp_dev->reset);

	return ret;
}

static const struct dev_pm_ops sun6i_isp_pm_ops = {
	.runtime_suspend	= sun6i_isp_suspend,
	.runtime_resume		= sun6i_isp_resume,
};

static const struct regmap_config sun6i_isp_regmap_config = {
	.reg_bits       = 32,
	.reg_stride     = 4,
	.val_bits       = 32,
	.max_register	= 0x400,
};

static int sun6i_isp_resources_setup(struct sun6i_isp_device *isp_dev,
				     struct platform_device *platform_dev)
{
	struct device *dev = isp_dev->dev;
	void __iomem *io_base;
	int irq;
	int ret;

	/* Registers */

	io_base = devm_platform_ioremap_resource(platform_dev, 0);
	if (IS_ERR(io_base))
		return PTR_ERR(io_base);

	isp_dev->regmap = devm_regmap_init_mmio_clk(dev, "bus", io_base,
						    &sun6i_isp_regmap_config);
	if (IS_ERR(isp_dev->regmap)) {
		dev_err(dev, "failed to init register map\n");
		return PTR_ERR(isp_dev->regmap);
	}

	/* Clocks */

	isp_dev->clock_mod = devm_clk_get(dev, "mod");
	if (IS_ERR(isp_dev->clock_mod)) {
		dev_err(dev, "failed to acquire module clock\n");
		return PTR_ERR(isp_dev->clock_mod);
	}

	isp_dev->clock_ram = devm_clk_get(dev, "ram");
	if (IS_ERR(isp_dev->clock_ram)) {
		dev_err(dev, "failed to acquire ram clock\n");
		return PTR_ERR(isp_dev->clock_ram);
	}

	ret = clk_set_rate_exclusive(isp_dev->clock_mod, 297000000);
	if (ret) {
		dev_err(dev, "failed to set mod clock rate\n");
		return ret;
	}

	/* Reset */

	isp_dev->reset = devm_reset_control_get_shared(dev, NULL);
	if (IS_ERR(isp_dev->reset)) {
		dev_err(dev, "failed to acquire reset\n");
		ret = PTR_ERR(isp_dev->reset);
		goto error_clock_rate_exclusive;
	}

	/* Interrupt */

	irq = platform_get_irq(platform_dev, 0);
	if (irq < 0) {
		dev_err(dev, "failed to get interrupt\n");
		ret = -ENXIO;
		goto error_clock_rate_exclusive;
	}

	ret = devm_request_irq(dev, irq, sun6i_isp_interrupt, IRQF_SHARED,
			       SUN6I_ISP_NAME, isp_dev);
	if (ret) {
		dev_err(dev, "failed to request interrupt\n");
		goto error_clock_rate_exclusive;
	}

	/* Runtime PM */

	pm_runtime_enable(dev);

	return 0;

error_clock_rate_exclusive:
	clk_rate_exclusive_put(isp_dev->clock_mod);

	return ret;
}

static void sun6i_isp_resources_cleanup(struct sun6i_isp_device *isp_dev)
{
	struct device *dev = isp_dev->dev;

	pm_runtime_disable(dev);
	clk_rate_exclusive_put(isp_dev->clock_mod);
}

static int sun6i_isp_probe(struct platform_device *platform_dev)
{
	struct sun6i_isp_device *isp_dev;
	struct device *dev = &platform_dev->dev;
	const struct sun6i_isp_variant *variant;
	int ret;

	variant = of_device_get_match_data(dev);
	if (!variant)
		return -EINVAL;

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

	isp_dev->dev = dev;
	platform_set_drvdata(platform_dev, isp_dev);

	spin_lock_init(&isp_dev->state_lock);

	ret = sun6i_isp_resources_setup(isp_dev, platform_dev);
	if (ret)
		return ret;

	ret = sun6i_isp_tables_setup(isp_dev, variant);
	if (ret) {
		dev_err(dev, "failed to setup tables\n");
		goto error_resources;
	}

	ret = sun6i_isp_v4l2_setup(isp_dev);
	if (ret) {
		dev_err(dev, "failed to setup v4l2\n");
		goto error_tables;
	}

	ret = sun6i_isp_proc_setup(isp_dev);
	if (ret) {
		dev_err(dev, "failed to setup proc\n");
		goto error_v4l2;
	}

	ret = sun6i_isp_capture_setup(isp_dev);
	if (ret) {
		dev_err(dev, "failed to setup capture\n");
		goto error_proc;
	}

	ret = sun6i_isp_params_setup(isp_dev);
	if (ret) {
		dev_err(dev, "failed to setup params\n");
		goto error_capture;
	}

	return 0;

error_capture:
	sun6i_isp_capture_cleanup(isp_dev);

error_proc:
	sun6i_isp_proc_cleanup(isp_dev);

error_v4l2:
	sun6i_isp_v4l2_cleanup(isp_dev);

error_tables:
	sun6i_isp_tables_cleanup(isp_dev);

error_resources:
	sun6i_isp_resources_cleanup(isp_dev);

	return ret;
}

static void sun6i_isp_remove(struct platform_device *platform_dev)
{
	struct sun6i_isp_device *isp_dev = platform_get_drvdata(platform_dev);

	sun6i_isp_params_cleanup(isp_dev);
	sun6i_isp_capture_cleanup(isp_dev);
	sun6i_isp_proc_cleanup(isp_dev);
	sun6i_isp_v4l2_cleanup(isp_dev);
	sun6i_isp_tables_cleanup(isp_dev);
	sun6i_isp_resources_cleanup(isp_dev);
}

/*
 * History of sun6i-isp:
 * - sun4i-a10-isp: initial ISP tied to the CSI0 controller,
 *   apparently unused in software implementations;
 * - sun6i-a31-isp: separate ISP loosely based on sun4i-a10-isp,
 *   adding extra modules and features;
 * - sun9i-a80-isp: based on sun6i-a31-isp with some register offset changes
 *   and new modules like saturation and cnr;
 * - sun8i-a23-isp/sun8i-h3-isp: based on sun9i-a80-isp with most modules
 *   related to raw removed;
 * - sun8i-a83t-isp: based on sun9i-a80-isp with some register offset changes
 * - sun8i-v3s-isp: based on sun8i-a83t-isp with a new disc module;
 */

static const struct sun6i_isp_variant sun8i_v3s_isp_variant = {
	.table_load_save_size	= 0x1000,
	.table_lut_size		= 0xe00,
	.table_drc_size		= 0x600,
	.table_stats_size	= 0x2100,
};

static const struct of_device_id sun6i_isp_of_match[] = {
	{
		.compatible	= "allwinner,sun8i-v3s-isp",
		.data		= &sun8i_v3s_isp_variant,
	},
	{},
};

MODULE_DEVICE_TABLE(of, sun6i_isp_of_match);

static struct platform_driver sun6i_isp_platform_driver = {
	.probe	= sun6i_isp_probe,
	.remove_new = sun6i_isp_remove,
	.driver	= {
		.name		= SUN6I_ISP_NAME,
		.of_match_table	= sun6i_isp_of_match,
		.pm		= &sun6i_isp_pm_ops,
	},
};

module_platform_driver(sun6i_isp_platform_driver);

MODULE_DESCRIPTION("Allwinner A31 Image Signal Processor driver");
MODULE_AUTHOR("Paul Kocialkowski <paul.kocialkowski@bootlin.com>");
MODULE_LICENSE("GPL"