/*
 * SPDX-License-Identifier: LGPL-2.0-or-later
 */

/**
 * CamelOperation:
 *
 * Progress reporting and cancellation for long-running tasks.
 *
 * #CamelOperation is a bi-directional rendezvous mechanism used to communicate
 * status information from long-running Camel tasks back to the calling
 * application, and to allow the application to cancel those tasks.
 *
 * The typical usage pattern is:
 *
 * 1. Create a #CamelOperation with a status callback.
 * 2. Push it onto the current thread with camel_operation_push_message() /
 *    or use it as a #GCancellable passed to async operations.
 * 3. Long-running Camel functions report progress via
 *    camel_operation_progress() and check for cancellation via the
 *    #GCancellable interface.
 * 4. The client can cancel the operation by calling g_cancellable_cancel()
 *    on the #CamelOperation (which is a #GCancellable subclass).
 *
 * Status messages follow a push/pop model: camel_operation_push_message()
 * starts a new operation description (emitting the "status" signal), and
 * camel_operation_pop_message() ends it, restoring the previous description.
 * camel_operation_progress() reports percentage-based progress within the
 * current message.
 **/

#include "evolution-data-server-config.h"

#include <stdio.h>
#include <unistd.h>
#include <sys/time.h>

#include <nspr.h>

#include "camel-msgport.h"
#include "camel-operation.h"

#define PROGRESS_DELAY		250  /* milliseconds */
#define TRANSIENT_DELAY		250  /* milliseconds */
#define POP_MESSAGE_DELAY	1    /* seconds */

typedef struct _StatusNode StatusNode;

struct _StatusNode {
	volatile gint ref_count;
	CamelOperation *operation;
	guint source_id;  /* for timeout or idle */
	gchar *message;
	gint percent;
};

struct _CamelOperationPrivate {
	GQueue status_stack;
	GCancellable *proxying;
	gulong proxying_handler_id;
};

enum {
	STATUS,
	PUSH_MESSAGE,
	POP_MESSAGE,
	PROGRESS,
	LAST_SIGNAL
};

static GRecMutex operation_lock;
#define LOCK() g_rec_mutex_lock (&operation_lock)
#define UNLOCK() g_rec_mutex_unlock (&operation_lock)

static GQueue operation_list = G_QUEUE_INIT;

static guint signals[LAST_SIGNAL];

G_DEFINE_TYPE_WITH_PRIVATE (CamelOperation, camel_operation, G_TYPE_CANCELLABLE)

static StatusNode *
status_node_new (void)
{
	StatusNode *node;

	node = g_slice_new0 (StatusNode);
	node->ref_count = 1;

	return node;
}

static StatusNode *
status_node_ref (StatusNode *node)
{
	g_return_val_if_fail (node != NULL, NULL);
	g_return_val_if_fail (node->ref_count > 0, node);

	g_atomic_int_inc (&node->ref_count);

	return node;
}

static void
status_node_unref (StatusNode *node)
{
	g_return_if_fail (node != NULL);
	g_return_if_fail (node->ref_count > 0);

	if (g_atomic_int_dec_and_test (&node->ref_count)) {

		if (node->operation != NULL)
			g_object_unref (node->operation);

		if (node->source_id > 0)
			g_source_remove (node->source_id);

		g_free (node->message);

		g_slice_free (StatusNode, node);
	}
}

static gboolean
operation_emit_status_cb (gpointer user_data)
{
	StatusNode *node = user_data;
	StatusNode *head_node;
	gboolean emit_status;

	LOCK ();

	node->source_id = 0;

	/* Check if we've been preempted by another StatusNode,
	 * or if we've been cancelled and popped off the stack. */
	head_node = g_queue_peek_head (&node->operation->priv->status_stack);
	emit_status = (node == head_node);

	UNLOCK ();

	if (emit_status)
		g_signal_emit (
			node->operation,
			signals[STATUS], 0,
			node->message,
			node->percent);

	return FALSE;
}

static void
proxying_cancellable_cancelled_cb (GCancellable *cancellable,
				   GCancellable *operation)
{
	g_return_if_fail (CAMEL_IS_OPERATION (operation));

	g_cancellable_cancel (operation);
}

static void
operation_dispose (GObject *object)
{
	CamelOperationPrivate *priv;

	priv = CAMEL_OPERATION (object)->priv;

	LOCK ();

	if (priv->proxying && priv->proxying_handler_id) {
		/* Intentionally avoid g_cancellable_disconnect(), because it can lock
		   when the priv->proxying holds the last reference. */
		g_signal_handler_disconnect (priv->proxying, priv->proxying_handler_id);
		priv->proxying_handler_id = 0;
	}

	g_clear_object (&priv->proxying);

	g_queue_remove (&operation_list, object);

	/* Because each StatusNode holds a reference to its
	 * CamelOperation, the fact that we're being disposed
	 * implies the stack should be empty now. */
	g_warn_if_fail (g_queue_is_empty (&priv->status_stack));

	UNLOCK ();

	/* Chain up to parent's dispose() method. */
	G_OBJECT_CLASS (camel_operation_parent_class)->dispose (object);
}

static void
camel_operation_class_init (CamelOperationClass *class)
{
	GObjectClass *object_class;

	object_class = G_OBJECT_CLASS (class);
	object_class->dispose = operation_dispose;

	signals[STATUS] = g_signal_new (
		"status",
		G_TYPE_FROM_CLASS (class),
		G_SIGNAL_RUN_LAST,
		G_STRUCT_OFFSET (CamelOperationClass, status),
		NULL, NULL, NULL,
		G_TYPE_NONE, 2,
		G_TYPE_STRING,
		G_TYPE_INT);

	signals[PUSH_MESSAGE] = g_signal_new (
		"push-message",
		G_TYPE_FROM_CLASS (class),
		G_SIGNAL_RUN_LAST,
		0,
		NULL, NULL, NULL,
		G_TYPE_NONE, 1,
		G_TYPE_STRING);

	signals[POP_MESSAGE] = g_signal_new (
		"pop-message",
		G_TYPE_FROM_CLASS (class),
		G_SIGNAL_RUN_LAST,
		0,
		NULL, NULL, NULL,
		G_TYPE_NONE, 0);

	signals[PROGRESS] = g_signal_new (
		"progress",
		G_TYPE_FROM_CLASS (class),
		G_SIGNAL_RUN_LAST,
		0,
		NULL, NULL, NULL,
		G_TYPE_NONE, 1,
		G_TYPE_INT);
}

static void
camel_operation_init (CamelOperation *operation)
{
	operation->priv = camel_operation_get_instance_private (operation);

	g_queue_init (&operation->priv->status_stack);
	operation->priv->proxying = NULL;
	operation->priv->proxying_handler_id = 0;

	LOCK ();
	g_queue_push_tail (&operation_list, operation);
	UNLOCK ();
}

/**
 * camel_operation_new:
 *
 * Create a new camel operation handle.  Camel operation handles can
 * be used in a multithreaded application (or a single operation
 * handle can be used in a non threaded appliation) to cancel running
 * operations and to obtain notification messages of the internal
 * status of messages.
 *
 * Returns: (transfer full): A new operation handle.
 **/
GCancellable *
camel_operation_new (void)
{
	return g_object_new (CAMEL_TYPE_OPERATION, NULL);
}

/**
 * camel_operation_new_proxy:
 * @cancellable: (nullable): a #GCancellable to proxy
 *
 * Proxies the @cancellable in a way that if it is cancelled,
 * then the returned cancellable is also cancelled, but when
 * the returned cancellable is cancelled, then it doesn't
 * influence the original cancellable. Other CamelOperation
 * actions being done on the returned cancellable are also
 * propagated to the @cancellable.
 *
 * The passed-in @cancellable can be %NULL, in which case
 * a plain CamelOperation is returned.
 *
 * This is useful when some operation can be cancelled from
 * elsewhere (like by a user), but also by the code on its own,
 * when it doesn't make sense to cancel also any larger operation
 * to which the passed-in cancellable belongs.
 *
 * Returns: (transfer full): A new operation handle, proxying @cancellable.
 *
 * Since: 3.24
 **/
GCancellable *
camel_operation_new_proxy (GCancellable *cancellable)
{
	GCancellable *operation;
	CamelOperationPrivate *priv;

	operation = camel_operation_new ();

	if (!G_IS_CANCELLABLE (cancellable))
		return operation;

	priv = CAMEL_OPERATION (operation)->priv;
	g_return_val_if_fail (priv != NULL, operation);

	priv->proxying = g_object_ref (cancellable);
	/* Intentionally avoid g_cancellable_connect(), because it can lock on call
	   of g_cancellable_disconnect() when the priv->proxying holds the last
	   reference. */
	priv->proxying_handler_id = g_signal_connect_data (cancellable, "cancelled",
		G_CALLBACK (proxying_cancellable_cancelled_cb), operation, NULL, 0);

	if (g_cancellable_is_cancelled (cancellable))
		proxying_cancellable_cancelled_cb (cancellable, operation);

	return operation;
}

/**
 * camel_operation_cancel_all:
 *
 * Cancel all outstanding operations.
 **/
void
camel_operation_cancel_all (void)
{
	GList *link;

	LOCK ();

	link = g_queue_peek_head_link (&operation_list);

	while (link != NULL) {
		GCancellable *cancellable = link->data;

		g_cancellable_cancel (cancellable);

		link = g_list_next (link);
	}

	UNLOCK ();
}

/**
 * camel_operation_push_message:
 * @cancellable: a #GCancellable or %NULL
 * @format: a standard printf() format string
 * @...: the parameters to insert into the format string
 *
 * Call this function to describe an operation being performed.
 * Call camel_operation_progress() to report progress on the operation.
 * Call camel_operation_pop_message() when the operation is complete.
 *
 * This function only works if @cancellable is a #CamelOperation cast as a
 * #GCancellable.  If @cancellable is a plain #GCancellable or %NULL, the
 * function does nothing and returns silently.
 **/
void
camel_operation_push_message (GCancellable *cancellable,
                              const gchar *format, ...)
{
	CamelOperation *operation;
	StatusNode *node;
	gchar *message;
	va_list ap;

	if (cancellable == NULL)
		return;

	if (G_OBJECT_TYPE (cancellable) == G_TYPE_CANCELLABLE)
		return;

	g_return_if_fail (CAMEL_IS_OPERATION (cancellable));

	va_start (ap, format);
	message = g_strdup_vprintf (format, ap);
	va_end (ap);

	operation = CAMEL_OPERATION (cancellable);

	g_signal_emit (cancellable, signals[PUSH_MESSAGE], 0, message);

	if (operation->priv->proxying)
		camel_operation_push_message (operation->priv->proxying, "%s", message);

	LOCK ();

	node = status_node_new ();
	node->message = message; /* takes ownership */
	node->operation = g_object_ref (operation);

	if (g_queue_is_empty (&operation->priv->status_stack)) {
		node->source_id = g_idle_add_full (
			G_PRIORITY_DEFAULT_IDLE,
			operation_emit_status_cb,
			status_node_ref (node),
			(GDestroyNotify) status_node_unref);
	} else {
		GSource *source;

		source = g_timeout_source_new (TRANSIENT_DELAY);
		g_source_set_callback (source, operation_emit_status_cb, status_node_ref (node), (GDestroyNotify) status_node_unref);
		g_source_set_name (source, "[camel] operation_emit_status_cb");
		node->source_id = g_source_attach (source, NULL);
		g_source_unref (source);
	}

	g_queue_push_head (&operation->priv->status_stack, node);

	UNLOCK ();
}

/**
 * camel_operation_pop_message:
 * @cancellable: a #GCancellable
 *
 * Pops the most recently pushed message.
 *
 * This function only works if @cancellable is a #CamelOperation cast as a
 * #GCancellable.  If @cancellable is a plain #GCancellable or %NULL, the
 * function does nothing and returns silently.
 **/
void
camel_operation_pop_message (GCancellable *cancellable)
{
	CamelOperation *operation;
	StatusNode *node;

	if (cancellable == NULL)
		return;

	if (G_OBJECT_TYPE (cancellable) == G_TYPE_CANCELLABLE)
		return;

	g_return_if_fail (CAMEL_IS_OPERATION (cancellable));

	operation = CAMEL_OPERATION (cancellable);

	g_signal_emit (cancellable, signals[POP_MESSAGE], 0);

	if (operation->priv->proxying)
		camel_operation_pop_message (operation->priv->proxying);

	LOCK ();

	node = g_queue_pop_head (&operation->priv->status_stack);

	if (node != NULL) {
		if (node->source_id > 0) {
			g_source_remove (node->source_id);
			node->source_id = 0;
		}
		status_node_unref (node);
	}

	node = g_queue_peek_head (&operation->priv->status_stack);

	if (node != NULL) {
		GSource *source;

		if (node->source_id != 0)
			g_source_remove (node->source_id);

		source = g_timeout_source_new_seconds (POP_MESSAGE_DELAY);
		g_source_set_callback (source, operation_emit_status_cb, status_node_ref (node), (GDestroyNotify) status_node_unref);
		g_source_set_name (source, "[camel] operation_emit_status_cb");
		node->source_id = g_source_attach (source, NULL);
		g_source_unref (source);
	}

	UNLOCK ();
}

/**
 * camel_operation_dup_message:
 * @cancellable: a #GCancellable or %NULL
 *
 * Duplicates current operation message, or returns %NULL, if no such is available.
 * The message as the last text set by camel_operation_push_message().
 *
 * Free the returned text with g_free(), when no longer needed.
 *
 * This function only works if @cancellable is a #CamelOperation cast as a
 * #GCancellable.  If @cancellable is a plain #GCancellable or %NULL, the
 * function does nothing and returns silently.
 *
 * Returns: (transfer full) (nullable): a copy of the last text set by camel_operation_push_message(),
 *    or %NULL, when none is set.
 *
 * Since: 3.52
 **/
gchar *
camel_operation_dup_message (GCancellable *cancellable)
{
	CamelOperation *operation;
	gchar *res = NULL;

	if (cancellable == NULL)
		return NULL;

	if (G_OBJECT_TYPE (cancellable) == G_TYPE_CANCELLABLE)
		return NULL;

	g_return_val_if_fail (CAMEL_IS_OPERATION (cancellable), NULL);

	operation = CAMEL_OPERATION (cancellable);

	if (operation->priv->proxying) {
		res = camel_operation_dup_message (operation->priv->proxying);
	} else {
		StatusNode *node;

		LOCK ();

		node = g_queue_peek_head (&operation->priv->status_stack);
		if (node)
			res = g_strdup (node->message);

		UNLOCK ();
	}

	return res;
}

/**
 * camel_operation_progress:
 * @cancellable: a #GCancellable or %NULL
 * @percent: percent complete, 0 to 100.
 *
 * Report progress on the current operation.  @percent reports the current
 * percentage of completion, which should be in the range of 0 to 100.
 *
 * This function only works if @cancellable is a #CamelOperation cast as a
 * #GCancellable.  If @cancellable is a plain #GCancellable or %NULL, the
 * function does nothing and returns silently.
 **/
void
camel_operation_progress (GCancellable *cancellable,
                          gint percent)
{
	CamelOperation *operation;
	StatusNode *node;

	if (cancellable == NULL)
		return;

	if (G_OBJECT_TYPE (cancellable) == G_TYPE_CANCELLABLE)
		return;

	g_return_if_fail (CAMEL_IS_OPERATION (cancellable));

	operation = CAMEL_OPERATION (cancellable);

	g_signal_emit (cancellable, signals[PROGRESS], 0, percent);

	if (operation->priv->proxying)
		camel_operation_progress (operation->priv->proxying, percent);

	LOCK ();

	node = g_queue_peek_head (&operation->priv->status_stack);

	if (node != NULL) {
		node->percent = percent;

		/* Rate limit progress updates. */
		if (node->source_id == 0) {
			GSource *source;

			source = g_timeout_source_new (PROGRESS_DELAY);
			g_source_set_callback (source, operation_emit_status_cb, status_node_ref (node), (GDestroyNotify) status_node_unref);
			g_source_set_name (source, "[camel] operation_emit_status_cb");
			node->source_id = g_source_attach (source, NULL);
			g_source_unref (source);
		}
	}

	UNLOCK ();
}
