/* Copyright Philip Withnall. Licenced for documentation use only, not to be compiled. */

#include <string.h>

#include <glib.h>
#include <gio/gio.h>

#include <lib/logging.h>

#include "task.h"

/**
 * SECTION:foo-task
 * @short_description: Asynchronous task helper functions
 * @title: FooTask
 * @stability: Private
 * @include: lib/task.h
 *
 * Various helper and utility functions for operating on #GTasks.
 */


typedef struct {
	GHashTable/*<unowned GTask, gulong>*/ *remaining_tasks;
	gboolean preparing;
	GMutex mutex;
} CancelData;

static CancelData *
cancel_data_new (void)
{
	CancelData *data = g_slice_new0 (CancelData);
	data->remaining_tasks = g_hash_table_new (NULL, NULL);
	data->preparing = TRUE;
	g_mutex_init (&data->mutex);
	return data;
}

static void
cancel_data_free (CancelData *data)
{
	g_hash_table_unref (data->remaining_tasks);
	g_mutex_clear (&data->mutex);
	g_slice_free (CancelData, data);
}

/* NOTE: This may be invoked in any thread, as it’s emitted in the owning
 * context of each of the remaining tasks. */
static void
cancel_cb (GTask *cancelled_task, gpointer user_data)
{
	GTask *cancel_task = NULL;  /* owned */
	CancelData *data;  /* unowned */
	gulong id;
	gboolean finished;

	cancel_task = G_TASK (user_data);
	data = g_task_get_task_data (cancel_task);

	g_mutex_lock (&data->mutex);

	id = (gulong) g_hash_table_lookup (data->remaining_tasks,
	                                   cancelled_task);
	if (id == 0) {
		g_mutex_unlock (&data->mutex);
		g_return_if_fail (id != 0);
	}

	/* Mark @cancelled_task as complete. */
	g_signal_handler_disconnect (cancelled_task, id);
	g_hash_table_remove (data->remaining_tasks, cancelled_task);

	finished = (g_hash_table_size (data->remaining_tasks) == 0 &&
	            !data->preparing);

	g_mutex_unlock (&data->mutex);

	if (finished) {
		g_task_return_boolean (cancel_task, TRUE);
	}

	g_object_unref (cancel_task);
}

/**
 * foo_task_cancel_multiple_async:
 * @tasks: (array length=n_tasks): zero or more #GTasks
 * @n_tasks: number of elements in the @tasks array
 * @cancellable: (allow-none): a #GCancellable, or %NULL
 * @callback: completion callback
 * @user_data: data for @callback
 *
 * Asynchronously cancel the given @tasks in parallel, and call @callback once
 * all of the tasks have returned (i.e. called their own completion callbacks).
 * All of the @tasks must have a #GCancellable set. If any of the @tasks have
 * already been cancelled, they will be ignored. If @tasks is empty, @callback
 * will be invoked immediately.
 *
 * @callback is invoked in the thread default main context at the time of
 * calling this function.
 */
void
foo_task_cancel_multiple_async (GTask **tasks, guint n_tasks,
                                GCancellable *cancellable,
                                GAsyncReadyCallback callback,
                                gpointer user_data)
{
	GTask *cancel_task = NULL;  /* owned */
	CancelData *data;  /* owned */
	guint i;

	g_return_if_fail (tasks != NULL || n_tasks == 0);
	g_return_if_fail (cancellable == NULL ||
	                  G_IS_CANCELLABLE (cancellable));

	cancel_task = g_task_new (NULL, cancellable, callback, user_data);
	g_task_set_check_cancellable (cancel_task, FALSE);
	g_task_set_source_tag (cancel_task, foo_task_cancel_multiple_async);

	data = cancel_data_new ();
	g_task_set_task_data (cancel_task, data,  /* transfer */
	                      (GDestroyNotify) cancel_data_free);

	g_mutex_lock (&data->mutex);

	for (i = 0; i < n_tasks; i++) {
		GCancellable *task_cancellable;
		gulong id;

		/* We require the #GTasks to have a cancellable set. */
		task_cancellable = g_task_get_cancellable (tasks[i]);
		g_return_if_fail (task_cancellable != NULL);

		/* Already returned? */
		if (g_task_has_returned (tasks[i])) {
			continue;
		}

		/* Start the cancellation. */
		id = g_signal_connect (tasks[i], "returned",
		                       (GCallback) cancel_cb,
		                       g_object_ref (cancel_task));
		g_hash_table_insert (data->remaining_tasks, tasks[i],
		                     (gpointer) id);

		g_mutex_unlock (&data->mutex);
		g_cancellable_cancel (task_cancellable);
		g_mutex_lock (&data->mutex);
	}

	if (g_hash_table_size (data->remaining_tasks) == 0 &&
	    data->preparing) {
		g_task_return_boolean (cancel_task, TRUE);
	}

	data->preparing = FALSE;

	g_mutex_unlock (&data->mutex);

	g_object_unref (cancel_task);
}

/**
 * foo_task_cancel_multiple_finish:
 * @result: asynchronous result
 * @error: return location for a #GError, or %NULL
 *
 * Finish an asynchronous task cancellation operation started with
 * foo_task_cancel_multiple_async().
 */
void
foo_task_cancel_multiple_finish (GAsyncResult *result, GError **error)
{
	g_return_if_fail (g_task_is_valid (result, NULL));
	g_return_if_fail (error == NULL || *error == NULL);

	g_task_propagate_boolean (G_TASK (result), error);
}

/**
 * foo_task_cancel_async:
 * @task: a #GTask
 * @cancellable: (allow-none): a #GCancellable, or %NULL
 * @callback: completion callback
 * @user_data: data for @callback
 *
 * Asynchronously cancel the given @task, and call @callback once the task has
 * returned (i.e. called its own completion callback). @task must have a
 * #GCancellable set. If @task has already been cancelled, @callback will be
 * invoked immediately.
 *
 * @callback is invoked in the thread default main context at the time of
 * calling this function.
 */
void
foo_task_cancel_async (GTask *task, GCancellable *cancellable,
                       GAsyncReadyCallback callback, gpointer user_data)
{
	foo_task_cancel_multiple_async (&task, (task != NULL) ? 1 : 0,
	                                cancellable, callback, user_data);
}

/**
 * foo_task_cancel_finish:
 * @result: asynchronous result
 * @error: return location for a #GError, or %NULL
 *
 * Finish an asynchronous task cancellation operation started with
 * foo_task_cancel_async().
 */
void
foo_task_cancel_finish (GAsyncResult *result, GError **error)
{
	foo_task_cancel_multiple_finish (result, error);
}