// SPDX-License-Identifier: GPL-2.0
/*
 * AD7606 Parallel Interface ADC driver
 *
 * Copyright 2011 - 2024 Analog Devices Inc.
 * Copyright 2024 BayLibre SAS.
 */

#include <linux/err.h>
#include <linux/gpio/consumer.h>
#include <linux/io.h>
#include <linux/mod_devicetable.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/property.h>
#include <linux/types.h>

#include <linux/iio/backend.h>
#include <linux/iio/iio.h>

#include "ad7606.h"
#include "ad7606_bus_iface.h"

static const struct iio_chan_spec ad7606b_bi_channels[] = {
	AD7606_BI_CHANNEL(0),
	AD7606_BI_CHANNEL(1),
	AD7606_BI_CHANNEL(2),
	AD7606_BI_CHANNEL(3),
	AD7606_BI_CHANNEL(4),
	AD7606_BI_CHANNEL(5),
	AD7606_BI_CHANNEL(6),
	AD7606_BI_CHANNEL(7),
};

static const struct iio_chan_spec ad7606b_bi_sw_channels[] = {
	AD7606_BI_SW_CHANNEL(0),
	AD7606_BI_SW_CHANNEL(1),
	AD7606_BI_SW_CHANNEL(2),
	AD7606_BI_SW_CHANNEL(3),
	AD7606_BI_SW_CHANNEL(4),
	AD7606_BI_SW_CHANNEL(5),
	AD7606_BI_SW_CHANNEL(6),
	AD7606_BI_SW_CHANNEL(7),
};

static int ad7606_par_bus_update_scan_mode(struct iio_dev *indio_dev,
					   const unsigned long *scan_mask)
{
	struct ad7606_state *st = iio_priv(indio_dev);
	unsigned int c, ret;

	for (c = 0; c < indio_dev->num_channels; c++) {
		if (test_bit(c, scan_mask))
			ret = iio_backend_chan_enable(st->back, c);
		else
			ret = iio_backend_chan_disable(st->back, c);
		if (ret)
			return ret;
	}

	return 0;
}

static int ad7606_par_bus_setup_iio_backend(struct device *dev,
					    struct iio_dev *indio_dev)
{
	struct ad7606_state *st = iio_priv(indio_dev);
	unsigned int ret, c;
	struct iio_backend_data_fmt data = {
		.sign_extend = true,
		.enable = true,
	};

	st->back = devm_iio_backend_get(dev, NULL);
	if (IS_ERR(st->back))
		return PTR_ERR(st->back);

	/* If the device is iio_backend powered the PWM is mandatory */
	if (!st->cnvst_pwm)
		return dev_err_probe(st->dev, -EINVAL,
				     "A PWM is mandatory when using backend.\n");

	ret = devm_iio_backend_request_buffer(dev, st->back, indio_dev);
	if (ret)
		return ret;

	ret = devm_iio_backend_enable(dev, st->back);
	if (ret)
		return ret;

	for (c = 0; c < indio_dev->num_channels; c++) {
		ret = iio_backend_data_format_set(st->back, c, &data);
		if (ret)
			return ret;
	}

	indio_dev->channels = ad7606b_bi_channels;
	indio_dev->num_channels = 8;

	return 0;
}

static int ad7606_par_bus_reg_read(struct ad7606_state *st, unsigned int addr)
{
	struct ad7606_platform_data *pdata = st->dev->platform_data;
	int val, ret;

	ret = pdata->bus_reg_read(st->back, addr, &val);
	if (ret)
		return ret;

	return val;
}

static int ad7606_par_bus_reg_write(struct ad7606_state *st, unsigned int addr,
				    unsigned int val)
{
	struct ad7606_platform_data *pdata = st->dev->platform_data;

	return pdata->bus_reg_write(st->back, addr, val);
}

static int ad7606_par_bus_sw_mode_config(struct iio_dev *indio_dev)
{
	indio_dev->channels = ad7606b_bi_sw_channels;

	return 0;
}

static const struct ad7606_bus_ops ad7606_bi_bops = {
	.iio_backend_config = ad7606_par_bus_setup_iio_backend,
	.update_scan_mode = ad7606_par_bus_update_scan_mode,
	.reg_read = ad7606_par_bus_reg_read,
	.reg_write = ad7606_par_bus_reg_write,
	.sw_mode_config = ad7606_par_bus_sw_mode_config,
};

static int ad7606_par16_read_block(struct device *dev,
				   int count, void *buf)
{
	struct iio_dev *indio_dev = dev_get_drvdata(dev);
	struct ad7606_state *st = iio_priv(indio_dev);


	/*
	 * On the parallel interface, the frstdata signal is set to high while
	 * and after reading the sample of the first channel and low for all
	 * other channels.  This can be used to check that the incoming data is
	 * correctly aligned.  During normal operation the data should never
	 * become unaligned, but some glitch or electrostatic discharge might
	 * cause an extra read or clock cycle.  Monitoring the frstdata signal
	 * allows to recover from such failure situations.
	 */
	int num = count;
	u16 *_buf = buf;

	if (st->gpio_frstdata) {
		insw((unsigned long)st->base_address, _buf, 1);
		if (!gpiod_get_value(st->gpio_frstdata)) {
			ad7606_reset(st);
			return -EIO;
		}
		_buf++;
		num--;
	}
	insw((unsigned long)st->base_address, _buf, num);
	return 0;
}

static const struct ad7606_bus_ops ad7606_par16_bops = {
	.read_block = ad7606_par16_read_block,
};

static int ad7606_par8_read_block(struct device *dev,
				  int count, void *buf)
{
	struct iio_dev *indio_dev = dev_get_drvdata(dev);
	struct ad7606_state *st = iio_priv(indio_dev);
	/*
	 * On the parallel interface, the frstdata signal is set to high while
	 * and after reading the sample of the first channel and low for all
	 * other channels.  This can be used to check that the incoming data is
	 * correctly aligned.  During normal operation the data should never
	 * become unaligned, but some glitch or electrostatic discharge might
	 * cause an extra read or clock cycle.  Monitoring the frstdata signal
	 * allows to recover from such failure situations.
	 */
	int num = count;
	u16 *_buf = buf;

	if (st->gpio_frstdata) {
		insb((unsigned long)st->base_address, _buf, 2);
		if (!gpiod_get_value(st->gpio_frstdata)) {
			ad7606_reset(st);
			return -EIO;
		}
		_buf++;
		num--;
	}
	insb((unsigned long)st->base_address, _buf, num * 2);

	return 0;
}

static const struct ad7606_bus_ops ad7606_par8_bops = {
	.read_block = ad7606_par8_read_block,
};

static int ad7606_par_probe(struct platform_device *pdev)
{
	const struct ad7606_chip_info *chip_info;
	const struct platform_device_id *id;
	struct resource *res;
	void __iomem *addr;
	resource_size_t remap_size;
	int irq;

	/*
	 * If a firmware node is available (ACPI or DT), platform_device_id is null
	 * and we must use get_match_data.
	 */
	if (dev_fwnode(&pdev->dev)) {
		chip_info = device_get_match_data(&pdev->dev);
		if (device_property_present(&pdev->dev, "io-backends"))
			/*
			 * If a backend is available ,call the core probe with backend
			 * bops, otherwise use the former bops.
			 */
			return ad7606_probe(&pdev->dev, 0, NULL,
					    chip_info,
					    &ad7606_bi_bops);
	} else {
		id = platform_get_device_id(pdev);
		chip_info = (const struct ad7606_chip_info *)id->driver_data;
	}

	irq = platform_get_irq(pdev, 0);
	if (irq < 0)
		return irq;

	addr = devm_platform_get_and_ioremap_resource(pdev, 0, &res);
	if (IS_ERR(addr))
		return PTR_ERR(addr);

	remap_size = resource_size(res);

	return ad7606_probe(&pdev->dev, irq, addr, chip_info,
			    remap_size > 1 ? &ad7606_par16_bops :
			    &ad7606_par8_bops);
}

static const struct platform_device_id ad7606_driver_ids[] = {
	{ .name	= "ad7605-4", .driver_data = (kernel_ulong_t)&ad7605_4_info, },
	{ .name	= "ad7606-4", .driver_data = (kernel_ulong_t)&ad7606_4_info, },
	{ .name	= "ad7606-6", .driver_data = (kernel_ulong_t)&ad7606_6_info, },
	{ .name	= "ad7606-8", .driver_data = (kernel_ulong_t)&ad7606_8_info, },
	{ .name	= "ad7606b", .driver_data = (kernel_ulong_t)&ad7606b_info, },
	{ .name	= "ad7607", .driver_data = (kernel_ulong_t)&ad7607_info, },
	{ .name	= "ad7608", .driver_data = (kernel_ulong_t)&ad7608_info, },
	{ .name	= "ad7609", .driver_data = (kernel_ulong_t)&ad7609_info, },
	{ }
};
MODULE_DEVICE_TABLE(platform, ad7606_driver_ids);

static const struct of_device_id ad7606_of_match[] = {
	{ .compatible = "adi,ad7605-4", .data = &ad7605_4_info },
	{ .compatible = "adi,ad7606-4", .data = &ad7606_4_info },
	{ .compatible = "adi,ad7606-6", .data = &ad7606_6_info },
	{ .compatible = "adi,ad7606-8", .data = &ad7606_8_info },
	{ .compatible = "adi,ad7606b", .data = &ad7606b_info },
	{ .compatible = "adi,ad7607", .data = &ad7607_info },
	{ .compatible = "adi,ad7608", .data = &ad7608_info },
	{ .compatible = "adi,ad7609", .data = &ad7609_info },
	{ }
};
MODULE_DEVICE_TABLE(of, ad7606_of_match);

static struct platform_driver ad7606_driver = {
	.probe = ad7606_par_probe,
	.id_table = ad7606_driver_ids,
	.driver = {
		.name = "ad7606",
		.pm = AD7606_PM_OPS,
		.of_match_table = ad7606_of_match,
	},
};
module_platform_driver(ad7606_driver);

MODULE_AUTHOR("Michael Hennerich <michael.hennerich@analog.com>");
MODULE_DESCRIPTION("Analog Devices AD7606 ADC");
MODULE_LICENSE("GPL v2");
MODULE_IMPORT_NS("IIO_AD7606");
MODULE_IMPORT_NS("IIO_BACKEND");