// SPDX-License-Identifier: GPL-2.0-only
/*
 * Copyright (c) 2020-2021, The Linux Foundation. All rights reserved.
 *
 */

#include <linux/slab.h>
#include <linux/module.h>
#include <linux/spinlock.h>
#include <linux/interrupt.h>

#include <linux/gunyah/gh_dbl.h>
#include <linux/gunyah/gh_errno.h>
#include "hcall_dbl.h"

struct gh_dbl_desc {
	enum gh_dbl_label label;
};

enum gh_dbl_dir {
	GH_DBL_DIRECTION_TX,
	GH_DBL_DIRECTION_RX
};

struct gh_dbl_cap_table {
	struct gh_dbl_desc *client_desc;
	spinlock_t cap_entry_lock;
	gh_capid_t tx_cap_id;
	int tx_reg_done;

	gh_capid_t rx_cap_id;
	int rx_irq;
	int rx_reg_done;
	const char *rx_irq_name;
	dbl_rx_cb_t rx_callback;
	void *rx_priv_data;
	wait_queue_head_t cap_wq;
};

static bool gh_dbl_initialized;
static struct gh_dbl_cap_table gh_dbl_cap_table[GH_DBL_LABEL_MAX];

/**
 * gh_dbl_validate_params - Validate doorbell common parameters
 */
static int gh_dbl_validate_params(struct gh_dbl_desc *client_desc,
			enum gh_dbl_dir dir, const unsigned long flags)
{
	struct gh_dbl_cap_table *cap_table_entry;
	int ret;

	if (IS_ERR_OR_NULL(client_desc))
		return -EINVAL;

	/* Check if the client has manipulated the label */
	if (client_desc->label < 0 || client_desc->label >= GH_DBL_LABEL_MAX)
		return -EINVAL;

	cap_table_entry = &gh_dbl_cap_table[client_desc->label];

	spin_lock(&cap_table_entry->cap_entry_lock);

	if (cap_table_entry->client_desc != client_desc) {
		spin_unlock(&cap_table_entry->cap_entry_lock);
		pr_err("%s: Invalid client descriptor\n", __func__);
		return -EINVAL;
	}

	/*
	 * rx_cap_id == NULL and tx_cap_id == NULL means TWO things
	 * either "gh_dbl_populate_cap_info()" call from RM is not over
	 * or
	 * There are no doorbell setup for Tx or Rx
	 */
	if (dir == GH_DBL_DIRECTION_RX) {
		if (!cap_table_entry->rx_reg_done) {
			ret = -EINVAL;
			goto err;
		}

		if ((cap_table_entry->rx_cap_id == GH_CAPID_INVAL) &&
			(flags & GH_DBL_NONBLOCK)) {
			ret = -EAGAIN;
			goto err;
		}

		spin_unlock(&cap_table_entry->cap_entry_lock);

		if (wait_event_interruptible(cap_table_entry->cap_wq,
				cap_table_entry->rx_cap_id != GH_CAPID_INVAL))
			return -ERESTARTSYS;

	} else {
		if (!cap_table_entry->tx_reg_done) {
			ret = -EINVAL;
			goto err;
		}

		if ((cap_table_entry->tx_cap_id == GH_CAPID_INVAL) &&
			(flags & GH_DBL_NONBLOCK)) {
			ret = -EAGAIN;
			goto err;
		}

		spin_unlock(&cap_table_entry->cap_entry_lock);

		if (wait_event_interruptible(cap_table_entry->cap_wq,
				cap_table_entry->tx_cap_id != GH_CAPID_INVAL))
			return -ERESTARTSYS;

	}

	return 0;
err:
	spin_unlock(&cap_table_entry->cap_entry_lock);
	return ret;
}

/**
 * gh_dbl_read_and_clean - Automatically read and clear the flags in doorbell
 * @client_desc: client handle to indetify the doorbell object
 * @clear_flags: clear the bits mentioned in the clear_flags
 * @flags: Optional flags to pass to send the data. For the list of flags,
 *         see linux/gunyah/gh_dbl.h
 *
 * Reads and clears the flags of the Doorbell object. If there is a pending
 * bound virtual interrupt, it will be de-asserted
 *
 * Returns:
 * 0 on success, @clear_flags contains the doorbell’s previous unmasked flags
 * before the @clear_flags were removed.
 */
int gh_dbl_read_and_clean(void *dbl_client_desc, gh_dbl_flags_t *clear_flags,
			  const unsigned long flags)
{
	struct gh_dbl_cap_table *cap_table_entry;
	struct gh_hcall_dbl_recv_resp recv_resp;
	struct gh_dbl_desc *client_desc = dbl_client_desc;
	int ret, gh_ret;

	if (!clear_flags)
		return -EINVAL;

	ret = gh_dbl_validate_params(client_desc, GH_DBL_DIRECTION_RX, flags);
	if (ret)
		return ret;

	cap_table_entry = &gh_dbl_cap_table[client_desc->label];

	gh_ret = gh_hcall_dbl_recv(cap_table_entry->rx_cap_id,
					*clear_flags, &recv_resp);

	ret = gh_remap_error(gh_ret);
	if (ret != 0)
		pr_err("%s: Hypercall failed, ret = %d\n", __func__, gh_ret);
	else
		*clear_flags = recv_resp.old_flags;

	return ret;
}
EXPORT_SYMBOL(gh_dbl_read_and_clean);

/**
 * gh_dbl_set_mask - Set doorbell object mask
 * @client_desc: client handle to indetify the doorbell object
 * @enable_mask: The mask of flags that will cause an assertion of
 *				 the doorbell's bound virtual interrupt
 * @ack_mask: Controls which flags should be automatically cleared
 *			  when the interrupt is asserted
 * @flags: Optional flags to pass to send the data. For the list of flags,
 *         see linux/gunyah/gh_dbl.h
 *
 * Sets the Doorbell object’s masks. A doorbell object has two masks
 * which are configured by the receiver to control which flags it is
 * interested in, and which flags if any should be automatically acknowledged.
 *
 * Returns:
 * 0 on success
 */
int gh_dbl_set_mask(void *dbl_client_desc, gh_dbl_flags_t enable_mask,
		    gh_dbl_flags_t ack_mask, const unsigned long flags)
{
	struct gh_dbl_cap_table *cap_table_entry;
	struct gh_dbl_desc *client_desc = dbl_client_desc;
	int ret, gh_ret;

	ret = gh_dbl_validate_params(client_desc, GH_DBL_DIRECTION_RX, flags);
	if (ret)
		return ret;

	cap_table_entry = &gh_dbl_cap_table[client_desc->label];

	gh_ret = gh_hcall_dbl_mask(cap_table_entry->rx_cap_id,
						enable_mask, ack_mask);

	ret = gh_remap_error(gh_ret);
	if (ret != 0)
		pr_err("%s: Hypercall failed ret = %d\n", __func__, gh_ret);

	return ret;
}
EXPORT_SYMBOL(gh_dbl_set_mask);

/**
 * gh_dbl_send - Set flags in the doorbell
 * @client_desc: client handle to indetify the doorbell object
 * @newflags: flags to set in the doorbell. This flag along with enable_mask
 *			  in the doorbell decide whehter to raise vIRQ are not.
 * @flags: Optional flags to pass to send the data. For the list of flags,
 *         see linux/gunyah/gh_dbl.h
 *
 * Set flags in the doorbell. If following the send, the set of enabled flags
 * as defined by the bitwise-AND of the doorbell flags with the EnableMask,
 * is non-zero, any bound virtual interrupt will be asserted.
 *
 * Returns:
 * 0 on success, @newflags contains the doorbell’s previous unmasked flags
 * before the @newflags were added.
 */
int gh_dbl_send(void *dbl_client_desc, gh_dbl_flags_t *newflags,
		unsigned long flags)
{
	struct gh_dbl_cap_table *cap_table_entry;
	struct gh_hcall_dbl_send_resp send_resp;
	struct gh_dbl_desc *client_desc = dbl_client_desc;
	int ret, gh_ret;

	if (!newflags)
		return -EINVAL;

	ret = gh_dbl_validate_params(client_desc, GH_DBL_DIRECTION_TX, flags);
	if (ret)
		return ret;

	cap_table_entry = &gh_dbl_cap_table[client_desc->label];

	gh_ret = gh_hcall_dbl_send(cap_table_entry->tx_cap_id, *newflags,
								&send_resp);

	ret = gh_remap_error(gh_ret);
	if (ret != 0)
		pr_err("%s: Hypercall failed ret = %d\n", __func__, gh_ret);
	else
		*newflags = send_resp.old_flags;

	return ret;
}
EXPORT_SYMBOL(gh_dbl_send);

/**
 * gh_dbl_reset - clear all the flags of the doorbell and sets all bits in
 *				  the Doorbell’s mask.
 * @client_desc: client handle to indetify the doorbell object
 * @flags: Optional flags to pass to send the data. For the list of flags,
 *         see linux/gunyah/gh_dbl.h
 *
 * Clears all the flags of the doorbell and sets all bits in the doorbell’s
 * mask. If there is a pending bound virtual interrupt, it will be de-asserted.
 *
 * Returns:
 * 0 on success
 */
int gh_dbl_reset(void *dbl_client_desc, const unsigned long flags)
{
	struct gh_dbl_cap_table *cap_table_entry;
	struct gh_dbl_desc *client_desc = dbl_client_desc;
	int ret, gh_ret;

	ret = gh_dbl_validate_params(client_desc, GH_DBL_DIRECTION_RX, flags);
	if (ret)
		return ret;

	cap_table_entry = &gh_dbl_cap_table[client_desc->label];

	gh_ret = gh_hcall_dbl_reset(cap_table_entry->rx_cap_id);

	ret = gh_remap_error(gh_ret);
	if (ret != 0)
		pr_err("%s: Hypercall failed ret = %d\n", __func__, gh_ret);

	return ret;
}
EXPORT_SYMBOL(gh_dbl_reset);

static irqreturn_t gh_dbl_rx_callback_thread(int irq, void *data)
{
	struct gh_dbl_cap_table *cap_table_entry = data;

	if (!cap_table_entry->rx_callback)
		return IRQ_HANDLED;

	cap_table_entry->rx_callback(irq, cap_table_entry->rx_priv_data);
	return IRQ_HANDLED;
}

/**
 * gh_dbl_tx_register: Register as a Tx client to use the doorbell
 * @label: The label associated to the doorbell that the client wants
 *	   to send a message to other VM.
 *
 * The function returns a descriptor for the clients to send a message.
 * Else, returns -EBUSY if some other client is already registered
 * to this label, and -EINVAL for invalid arguments. The caller should check
 * the return value using IS_ERR_OR_NULL() and PTR_ERR() to extract the error
 * code.
 */
void *gh_dbl_tx_register(enum gh_dbl_label label)
{
	struct gh_dbl_cap_table *cap_table_entry;
	struct gh_dbl_desc *client_desc;
	int ret;

	if (label < 0 || label >= GH_DBL_LABEL_MAX)
		return ERR_PTR(-EINVAL);

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

	cap_table_entry = &gh_dbl_cap_table[label];

	spin_lock(&cap_table_entry->cap_entry_lock);

	/* Avoid multiple client Tx registration for the same doorbell */
	if (cap_table_entry->tx_reg_done) {
		ret = -EBUSY;
		goto err;
	}

	if (cap_table_entry->client_desc) {
		client_desc = cap_table_entry->client_desc;
	} else {
		client_desc = kzalloc(sizeof(*client_desc), GFP_ATOMIC);
		if (!client_desc) {
			ret = -ENOMEM;
			goto err;
		}

		client_desc->label = label;
		cap_table_entry->client_desc = client_desc;
	}

	cap_table_entry->tx_reg_done = 1;

	pr_debug("%s: Registered Tx client for label: %d\n", __func__, label);

	spin_unlock(&cap_table_entry->cap_entry_lock);
	return client_desc;

err:
	spin_unlock(&cap_table_entry->cap_entry_lock);
	return ERR_PTR(ret);
}
EXPORT_SYMBOL(gh_dbl_tx_register);

/**
 * gh_dbl_rx_register: Register as a Rx client to use the doorbell
 * @label: The label associated to the doorbell that the client wants
 *	   to read a message.
 * @rx_cb: Callback of the client when there is a vIRQ on doorbell
 * @priv: Private data of the driver
 *
 * The function returns a descriptor for the clients to receive a message.
 * Else, returns -EBUSY if some other client is already registered
 * to this label, and -EINVAL for invalid arguments. The caller should check
 * the return value using IS_ERR_OR_NULL() and PTR_ERR() to extract the error
 * code.
 */

void *gh_dbl_rx_register(enum gh_dbl_label label, dbl_rx_cb_t rx_cb, void *priv)
{
	struct gh_dbl_cap_table *cap_table_entry;
	struct gh_dbl_desc *client_desc;
	int ret;

	if (label < 0 || label >= GH_DBL_LABEL_MAX)
		return ERR_PTR(-EINVAL);

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

	cap_table_entry = &gh_dbl_cap_table[label];

	spin_lock(&cap_table_entry->cap_entry_lock);

	/* Avoid multiple client Rx registration for the same doorbell */
	if (cap_table_entry->rx_reg_done) {
		ret = -EBUSY;
		goto err;
	}

	if (cap_table_entry->client_desc) {
		client_desc = cap_table_entry->client_desc;
	} else {
		client_desc = kzalloc(sizeof(*client_desc), GFP_ATOMIC);
		if (!client_desc) {
			ret = -ENOMEM;
			goto err;
		}

		client_desc->label = label;
		cap_table_entry->client_desc = client_desc;
	}

	cap_table_entry->rx_callback = rx_cb;
	cap_table_entry->rx_priv_data = priv;

	cap_table_entry->rx_reg_done = 1;

	pr_debug("%s: Registered Rx client for label: %d\n", __func__, label);

	spin_unlock(&cap_table_entry->cap_entry_lock);
	return client_desc;

err:
	pr_debug("%s: Registration for Rx client for label failed: %d\n",
		__func__, label);
	spin_unlock(&cap_table_entry->cap_entry_lock);
	return ERR_PTR(ret);
}
EXPORT_SYMBOL(gh_dbl_rx_register);

/**
 * gh_dbl_tx_unregister: Unregister Tx client to use the doorbell
 * @client_desc: The descriptor that was passed via gh_dbl_tx_register() or
 *		 gh_dbl_rx_register()
 *
 * The function returns 0 is the client was unregistered successfully. Else,
 * -EINVAL for invalid arguments.
 */
int gh_dbl_tx_unregister(void *dbl_client_desc)
{
	struct gh_dbl_desc *client_desc = dbl_client_desc;
	struct gh_dbl_cap_table *cap_table_entry;

	if (IS_ERR_OR_NULL(client_desc))
		return -EINVAL;

	/* Check if the client has manipulated the label */
	if (client_desc->label < 0 || client_desc->label >= GH_DBL_LABEL_MAX)
		return -EINVAL;

	cap_table_entry = &gh_dbl_cap_table[client_desc->label];

	spin_lock(&cap_table_entry->cap_entry_lock);

	/* Is the client trying to free someone else's doorbell? */
	if (cap_table_entry->client_desc != client_desc) {
		pr_err("%s: Trying to free invalid client descriptor!\n",
			__func__);
		spin_unlock(&cap_table_entry->cap_entry_lock);
		return -EINVAL;
	}

	pr_debug("%s: Unregistering client for label: %d\n",
			__func__, client_desc->label);

	/* Rx client still holding the "client_desc". Do not remove now. */
	if (!cap_table_entry->rx_reg_done) {
		cap_table_entry->client_desc = NULL;
		kfree(client_desc);
	} else {
		pr_debug("%s: Rx client holding the client_desc.\n", __func__);
	}

	cap_table_entry->tx_reg_done = 0;
	spin_unlock(&cap_table_entry->cap_entry_lock);

	return 0;
}
EXPORT_SYMBOL(gh_dbl_tx_unregister);

/**
 * gh_dbl_rx_unregister: Unregister Rx client to use the doorbell
 * @client_desc: The descriptor that was passed via gh_dbl_tx_register() or
 *				 gh_dbl_rx_register()
 *
 * The function returns 0 is the client was unregistered successfully. Else,
 * -EINVAL for invalid arguments.
 */
int gh_dbl_rx_unregister(void *dbl_client_desc)
{
	struct gh_dbl_desc *client_desc = dbl_client_desc;
	struct gh_dbl_cap_table *cap_table_entry;

	if (IS_ERR_OR_NULL(client_desc))
		return -EINVAL;

	/* Check if the client has manipulated the label */
	if (client_desc->label < 0 || client_desc->label >= GH_DBL_LABEL_MAX)
		return -EINVAL;

	cap_table_entry = &gh_dbl_cap_table[client_desc->label];

	spin_lock(&cap_table_entry->cap_entry_lock);

	/* Is the client trying to free someone else's doorbell? */
	if (cap_table_entry->client_desc != client_desc) {
		pr_err("%s: Trying to free invalid client descriptor!\n",
			__func__);
		spin_unlock(&cap_table_entry->cap_entry_lock);
		return -EINVAL;
	}

	pr_debug("%s: Unregistering client for label: %d\n", __func__,
			 client_desc->label);

	/* Tx client still holding the "client_desc". Do not remove now.*/
	if (!cap_table_entry->tx_reg_done) {
		cap_table_entry->client_desc = NULL;
		kfree(client_desc);
	} else {
		pr_debug("%s: Tx client holding the client_desc.\n", __func__);
	}

	cap_table_entry->rx_callback = NULL;
	cap_table_entry->rx_priv_data = NULL;
	cap_table_entry->rx_reg_done = 0;

	spin_unlock(&cap_table_entry->cap_entry_lock);

	return 0;
}
EXPORT_SYMBOL(gh_dbl_rx_unregister);

/**
 * This API is called by RM driver to populate doorbell objects
 */
int gh_dbl_populate_cap_info(enum gh_dbl_label label, u64 cap_id,
				int direction, int rx_irq)
{
	struct gh_dbl_cap_table *cap_table_entry;
	int ret = 0;

	if (!gh_dbl_initialized)
		return -EAGAIN;

	if (label < 0 || label >= GH_DBL_LABEL_MAX) {
		pr_err("%s: Invalid label passed\n", __func__);
		return -EINVAL;
	}

	cap_table_entry = &gh_dbl_cap_table[label];

	switch (direction) {
	case GH_DBL_DIRECTION_TX:
		/* No interrupt should associated with Tx doorbell*/
		if (rx_irq > 0) {
			pr_err("%s: No IRQ associated for Tx doorbell!\n",
				__func__);
			ret = -ENXIO;
			goto err;
		}

		spin_lock(&cap_table_entry->cap_entry_lock);
		cap_table_entry->tx_cap_id = cap_id;
		spin_unlock(&cap_table_entry->cap_entry_lock);

		wake_up_interruptible(&cap_table_entry->cap_wq);

		pr_debug("%s: label: %d; tx_cap_id: %llu; dir: %d; rx_irq: %d\n",
			__func__, label, cap_id, direction, rx_irq);
		break;
	case GH_DBL_DIRECTION_RX:
		if (rx_irq <= 0) {
			pr_err("%s: Invalid IRQ number for Rx doorbell\n",
				__func__);
			ret = -ENXIO;
			goto err;
		}

		cap_table_entry->rx_irq = rx_irq;
		ret = request_threaded_irq(cap_table_entry->rx_irq,
				   NULL,
				   gh_dbl_rx_callback_thread,
				   IRQF_ONESHOT | IRQF_TRIGGER_RISING,
				   cap_table_entry->rx_irq_name,
				   cap_table_entry);

		if (ret < 0) {
			pr_err("%s: IRQ registration failed\n", __func__);
			goto err;
		}

		irq_set_irq_wake(rx_irq, 1);

		spin_lock(&cap_table_entry->cap_entry_lock);
		cap_table_entry->rx_cap_id = cap_id;
		spin_unlock(&cap_table_entry->cap_entry_lock);

		wake_up_interruptible(&cap_table_entry->cap_wq);

		pr_debug("%s: label: %d; rx_cap_id: %llu; dir: %d; rx_irq: %d\n",
			__func__, label, cap_id, direction, rx_irq);
		break;
	default:
		pr_err("%s: Invalid direction(%d) for doorbell\n",
			__func__, direction);
		ret = -EINVAL;
	}

err:
	return ret;
}
EXPORT_SYMBOL(gh_dbl_populate_cap_info);

/**
 * This API is called by RM driver to free up doorbell objects
 */
int gh_dbl_reset_cap_info(enum gh_dbl_label label, int direction, int *irq)
{
	struct gh_dbl_cap_table *cap_table_entry;
	int ret = 0;

	if (!gh_dbl_initialized)
		return -EAGAIN;

	if (label < 0 || label >= GH_DBL_LABEL_MAX) {
		pr_err("%s: Invalid label passed\n", __func__);
		return -EINVAL;
	}

	if (!irq)
		return -EINVAL;

	cap_table_entry = &gh_dbl_cap_table[label];

	spin_lock(&cap_table_entry->cap_entry_lock);

	switch (direction) {
	case GH_DBL_DIRECTION_TX:
		*irq = 0;
		cap_table_entry->tx_cap_id = GH_CAPID_INVAL;
		break;
	case GH_DBL_DIRECTION_RX:
		if (!cap_table_entry->rx_irq) {
			pr_err("%s: Rx IRQ not setup\n", __func__);
			ret = -ENXIO;
			goto err_unlock;
		}

		*irq = cap_table_entry->rx_irq;

		cap_table_entry->rx_irq = 0;
		cap_table_entry->rx_cap_id = GH_CAPID_INVAL;
		break;
	default:
		pr_err("%s: Invalid direction(%d) for doorbell\n",
			__func__, direction);
		ret = -EINVAL;
	}

err_unlock:
	spin_unlock(&cap_table_entry->cap_entry_lock);

	if (*irq && !ret)
		free_irq(*irq, cap_table_entry);

	return ret;
}
EXPORT_SYMBOL(gh_dbl_reset_cap_info);

static void gh_dbl_cleanup(int begin_idx)
{
	struct gh_dbl_cap_table *cap_table_entry;
	int i;

	if (begin_idx >= GH_DBL_LABEL_MAX)
		begin_idx = GH_DBL_LABEL_MAX - 1;

	for (i = begin_idx; i >= 0; i--) {
		cap_table_entry = &gh_dbl_cap_table[i];
		kfree(cap_table_entry->rx_irq_name);
	}
}

static int __init gh_dbl_init(void)
{
	struct gh_dbl_cap_table *entry;
	int ret;
	int i;

	for (i = 0; i < GH_DBL_LABEL_MAX; i++) {
		entry = &gh_dbl_cap_table[i];
		spin_lock_init(&entry->cap_entry_lock);
		init_waitqueue_head(&entry->cap_wq);
		entry->tx_cap_id = GH_CAPID_INVAL;
		entry->rx_cap_id = GH_CAPID_INVAL;
		entry->rx_irq_name = kasprintf(GFP_KERNEL, "gh_dbl_rx_%d", i);
		if (!entry->rx_irq_name) {
			ret = -ENOMEM;
			goto err;
		}
	}

	gh_dbl_initialized = true;

	return 0;

err:
	gh_dbl_cleanup(i);
	return ret;
}
module_init(gh_dbl_init);

static void __exit gh_dbl_exit(void)
{
	gh_dbl_cleanup(GH_DBL_LABEL_MAX - 1);
}
module_exit(gh_dbl_exit);

MODULE_DESCRIPTION("Qualcomm Technologies, Inc. Gunyah Doorbell Driver");
MODULE_LICENSE("GPL v2");
