// SPDX-License-Identifier: GPL-2.0-only
/*
 * Copyright (C) 2024 Analog Devices Inc.
 * Copyright (C) 2024 BayLibre, SAS
 */

/*
 * SPI Offloading support.
 *
 * Some SPI controllers support offloading of SPI transfers. Essentially, this
 * is the ability for a SPI controller to perform SPI transfers with minimal
 * or even no CPU intervention, e.g. via a specialized SPI controller with a
 * hardware trigger or via a conventional SPI controller using a non-Linux MCU
 * processor core to offload the work.
 */

#define DEFAULT_SYMBOL_NAMESPACE "SPI_OFFLOAD"

#include <linux/cleanup.h>
#include <linux/device.h>
#include <linux/dmaengine.h>
#include <linux/export.h>
#include <linux/kref.h>
#include <linux/list.h>
#include <linux/mutex.h>
#include <linux/of.h>
#include <linux/property.h>
#include <linux/spi/offload/consumer.h>
#include <linux/spi/offload/provider.h>
#include <linux/spi/offload/types.h>
#include <linux/spi/spi.h>
#include <linux/types.h>

struct spi_controller_and_offload {
	struct spi_controller *controller;
	struct spi_offload *offload;
};

struct spi_offload_trigger {
	struct list_head list;
	struct kref ref;
	struct fwnode_handle *fwnode;
	/* synchronizes calling ops and driver registration */
	struct mutex lock;
	/*
	 * If the provider goes away while the consumer still has a reference,
	 * ops and priv will be set to NULL and all calls will fail with -ENODEV.
	 */
	const struct spi_offload_trigger_ops *ops;
	void *priv;
};

static LIST_HEAD(spi_offload_triggers);
static DEFINE_MUTEX(spi_offload_triggers_lock);

/**
 * devm_spi_offload_alloc() - Allocate offload instance
 * @dev: Device for devm purposes and assigned to &struct spi_offload.provider_dev
 * @priv_size: Size of private data to allocate
 *
 * Offload providers should use this to allocate offload instances.
 *
 * Return: Pointer to new offload instance or error on failure.
 */
struct spi_offload *devm_spi_offload_alloc(struct device *dev,
					   size_t priv_size)
{
	struct spi_offload *offload;
	void *priv;

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

	priv = devm_kzalloc(dev, priv_size, GFP_KERNEL);
	if (!priv)
		return ERR_PTR(-ENOMEM);

	offload->provider_dev = dev;
	offload->priv = priv;

	return offload;
}
EXPORT_SYMBOL_GPL(devm_spi_offload_alloc);

static void spi_offload_put(void *data)
{
	struct spi_controller_and_offload *resource = data;

	resource->controller->put_offload(resource->offload);
	kfree(resource);
}

/**
 * devm_spi_offload_get() - Get an offload instance
 * @dev: Device for devm purposes
 * @spi: SPI device to use for the transfers
 * @config: Offload configuration
 *
 * Peripheral drivers call this function to get an offload instance that meets
 * the requirements specified in @config. If no suitable offload instance is
 * available, -ENODEV is returned.
 *
 * Return: Offload instance or error on failure.
 */
struct spi_offload *devm_spi_offload_get(struct device *dev,
					 struct spi_device *spi,
					 const struct spi_offload_config *config)
{
	struct spi_controller_and_offload *resource;
	struct spi_offload *offload;
	int ret;

	if (!spi || !config)
		return ERR_PTR(-EINVAL);

	if (!spi->controller->get_offload)
		return ERR_PTR(-ENODEV);

	resource = kzalloc_obj(*resource);
	if (!resource)
		return ERR_PTR(-ENOMEM);

	offload = spi->controller->get_offload(spi, config);
	if (IS_ERR(offload)) {
		kfree(resource);
		return offload;
	}

	resource->controller = spi->controller;
	resource->offload = offload;

	ret = devm_add_action_or_reset(dev, spi_offload_put, resource);
	if (ret)
		return ERR_PTR(ret);

	return offload;
}
EXPORT_SYMBOL_GPL(devm_spi_offload_get);

static void spi_offload_trigger_free(struct kref *ref)
{
	struct spi_offload_trigger *trigger =
		container_of(ref, struct spi_offload_trigger, ref);

	mutex_destroy(&trigger->lock);
	fwnode_handle_put(trigger->fwnode);
	kfree(trigger);
}

static void spi_offload_trigger_put(void *data)
{
	struct spi_offload_trigger *trigger = data;

	scoped_guard(mutex, &trigger->lock)
		if (trigger->ops && trigger->ops->release)
			trigger->ops->release(trigger);

	kref_put(&trigger->ref, spi_offload_trigger_free);
}

static struct spi_offload_trigger
*spi_offload_trigger_get(enum spi_offload_trigger_type type,
			 struct fwnode_reference_args *args)
{
	struct spi_offload_trigger *trigger;
	bool match = false;
	int ret;

	guard(mutex)(&spi_offload_triggers_lock);

	list_for_each_entry(trigger, &spi_offload_triggers, list) {
		if (trigger->fwnode != args->fwnode)
			continue;

		match = trigger->ops->match(trigger, type, args->args, args->nargs);
		if (match)
			break;
	}

	if (!match)
		return ERR_PTR(-EPROBE_DEFER);

	guard(mutex)(&trigger->lock);

	if (trigger->ops->request) {
		ret = trigger->ops->request(trigger, type, args->args, args->nargs);
		if (ret)
			return ERR_PTR(ret);
	}

	kref_get(&trigger->ref);

	return trigger;
}

/**
 * devm_spi_offload_trigger_get() - Get an offload trigger instance
 * @dev: Device for devm purposes.
 * @offload: Offload instance connected to a trigger.
 * @type: Trigger type to get.
 *
 * Return: Offload trigger instance or error on failure.
 */
struct spi_offload_trigger
*devm_spi_offload_trigger_get(struct device *dev,
			      struct spi_offload *offload,
			      enum spi_offload_trigger_type type)
{
	struct spi_offload_trigger *trigger;
	struct fwnode_reference_args args;
	int ret;

	ret = fwnode_property_get_reference_args(dev_fwnode(offload->provider_dev),
						 "trigger-sources",
						 "#trigger-source-cells", 0, 0,
						 &args);
	if (ret)
		return ERR_PTR(ret);

	trigger = spi_offload_trigger_get(type, &args);
	fwnode_handle_put(args.fwnode);
	if (IS_ERR(trigger))
		return trigger;

	ret = devm_add_action_or_reset(dev, spi_offload_trigger_put, trigger);
	if (ret)
		return ERR_PTR(ret);

	return trigger;
}
EXPORT_SYMBOL_GPL(devm_spi_offload_trigger_get);

/**
 * spi_offload_trigger_validate - Validate the requested trigger
 * @trigger: Offload trigger instance
 * @config: Trigger config to validate
 *
 * On success, @config may be modifed to reflect what the hardware can do.
 * For example, the frequency of a periodic trigger may be adjusted to the
 * nearest supported value.
 *
 * Callers will likely need to do additional validation of the modified trigger
 * parameters.
 *
 * Return: 0 on success, negative error code on failure.
 */
int spi_offload_trigger_validate(struct spi_offload_trigger *trigger,
				 struct spi_offload_trigger_config *config)
{
	guard(mutex)(&trigger->lock);

	if (!trigger->ops)
		return -ENODEV;

	if (!trigger->ops->validate)
		return -EOPNOTSUPP;

	return trigger->ops->validate(trigger, config);
}
EXPORT_SYMBOL_GPL(spi_offload_trigger_validate);

/**
 * spi_offload_trigger_enable - enables trigger for offload
 * @offload: Offload instance
 * @trigger: Offload trigger instance
 * @config: Trigger config to validate
 *
 * There must be a prepared offload instance with the specified ID (i.e.
 * spi_optimize_message() was called with the same offload assigned to the
 * message). This will also reserve the bus for exclusive use by the offload
 * instance until the trigger is disabled. Any other attempts to send a
 * transfer or lock the bus will fail with -EBUSY during this time.
 *
 * Calls must be balanced with spi_offload_trigger_disable().
 *
 * Context: can sleep
 * Return: 0 on success, else a negative error code.
 */
int spi_offload_trigger_enable(struct spi_offload *offload,
			       struct spi_offload_trigger *trigger,
			       struct spi_offload_trigger_config *config)
{
	int ret;

	guard(mutex)(&trigger->lock);

	if (!trigger->ops)
		return -ENODEV;

	if (offload->ops && offload->ops->trigger_enable) {
		ret = offload->ops->trigger_enable(offload);
		if (ret)
			return ret;
	}

	if (trigger->ops->enable) {
		ret = trigger->ops->enable(trigger, config);
		if (ret) {
			if (offload->ops && offload->ops->trigger_disable)
				offload->ops->trigger_disable(offload);
			return ret;
		}
	}

	return 0;
}
EXPORT_SYMBOL_GPL(spi_offload_trigger_enable);

/**
 * spi_offload_trigger_disable - disables hardware trigger for offload
 * @offload: Offload instance
 * @trigger: Offload trigger instance
 *
 * Disables the hardware trigger for the offload instance with the specified ID
 * and releases the bus for use by other clients.
 *
 * Context: can sleep
 */
void spi_offload_trigger_disable(struct spi_offload *offload,
				 struct spi_offload_trigger *trigger)
{
	if (offload->ops && offload->ops->trigger_disable)
		offload->ops->trigger_disable(offload);

	guard(mutex)(&trigger->lock);

	if (!trigger->ops)
		return;

	if (trigger->ops->disable)
		trigger->ops->disable(trigger);
}
EXPORT_SYMBOL_GPL(spi_offload_trigger_disable);

static void spi_offload_release_dma_chan(void *chan)
{
	dma_release_channel(chan);
}

/**
 * devm_spi_offload_tx_stream_request_dma_chan - Get the DMA channel info for the TX stream
 * @dev: Device for devm purposes.
 * @offload: Offload instance
 *
 * This is the DMA channel that will provide data to transfers that use the
 * %SPI_OFFLOAD_XFER_TX_STREAM offload flag.
 *
 * Return: Pointer to DMA channel info, or negative error code
 */
struct dma_chan
*devm_spi_offload_tx_stream_request_dma_chan(struct device *dev,
					     struct spi_offload *offload)
{
	struct dma_chan *chan;
	int ret;

	if (!offload->ops || !offload->ops->tx_stream_request_dma_chan)
		return ERR_PTR(-EOPNOTSUPP);

	chan = offload->ops->tx_stream_request_dma_chan(offload);
	if (IS_ERR(chan))
		return chan;

	ret = devm_add_action_or_reset(dev, spi_offload_release_dma_chan, chan);
	if (ret)
		return ERR_PTR(ret);

	return chan;
}
EXPORT_SYMBOL_GPL(devm_spi_offload_tx_stream_request_dma_chan);

/**
 * devm_spi_offload_rx_stream_request_dma_chan - Get the DMA channel info for the RX stream
 * @dev: Device for devm purposes.
 * @offload: Offload instance
 *
 * This is the DMA channel that will receive data from transfers that use the
 * %SPI_OFFLOAD_XFER_RX_STREAM offload flag.
 *
 * Return: Pointer to DMA channel info, or negative error code
 */
struct dma_chan
*devm_spi_offload_rx_stream_request_dma_chan(struct device *dev,
					     struct spi_offload *offload)
{
	struct dma_chan *chan;
	int ret;

	if (!offload->ops || !offload->ops->rx_stream_request_dma_chan)
		return ERR_PTR(-EOPNOTSUPP);

	chan = offload->ops->rx_stream_request_dma_chan(offload);
	if (IS_ERR(chan))
		return chan;

	ret = devm_add_action_or_reset(dev, spi_offload_release_dma_chan, chan);
	if (ret)
		return ERR_PTR(ret);

	return chan;
}
EXPORT_SYMBOL_GPL(devm_spi_offload_rx_stream_request_dma_chan);

/* Triggers providers */

static void spi_offload_trigger_unregister(void *data)
{
	struct spi_offload_trigger *trigger = data;

	scoped_guard(mutex, &spi_offload_triggers_lock)
		list_del(&trigger->list);

	scoped_guard(mutex, &trigger->lock) {
		trigger->priv = NULL;
		trigger->ops = NULL;
	}

	kref_put(&trigger->ref, spi_offload_trigger_free);
}

/**
 * devm_spi_offload_trigger_register() - Allocate and register an offload trigger
 * @dev: Device for devm purposes.
 * @info: Provider-specific trigger info.
 *
 * Return: 0 on success, else a negative error code.
 */
int devm_spi_offload_trigger_register(struct device *dev,
				      struct spi_offload_trigger_info *info)
{
	struct spi_offload_trigger *trigger;

	if (!info->fwnode || !info->ops || !info->ops->match)
		return -EINVAL;

	trigger = kzalloc(sizeof(*trigger), GFP_KERNEL);
	if (!trigger)
		return -ENOMEM;

	kref_init(&trigger->ref);
	mutex_init(&trigger->lock);
	trigger->fwnode = fwnode_handle_get(info->fwnode);
	trigger->ops = info->ops;
	trigger->priv = info->priv;

	scoped_guard(mutex, &spi_offload_triggers_lock)
		list_add_tail(&trigger->list, &spi_offload_triggers);

	return devm_add_action_or_reset(dev, spi_offload_trigger_unregister, trigger);
}
EXPORT_SYMBOL_GPL(devm_spi_offload_trigger_register);

/**
 * spi_offload_trigger_get_priv() - Get the private data for the trigger
 *
 * @trigger: Offload trigger instance.
 *
 * Return: Private data for the trigger.
 */
void *spi_offload_trigger_get_priv(struct spi_offload_trigger *trigger)
{
	return trigger->priv;
}
EXPORT_SYMBOL_GPL(spi_offload_trigger_get_priv);
