/* ide-task.c
 *
 * Copyright 2018 Christian Hergert
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

#define G_LOG_DOMAIN "ide-task"

#include "config.h"

#include <libide-core.h>

#include "ide-task.h"
#include "ide-thread-pool.h"
#include "ide-thread-private.h"

/* From GDK_PRIORITY_REDRAW */
#define PRIORITY_REDRAW (G_PRIORITY_HIGH_IDLE + 20)

#if 0
# define ENABLE_TIME_CHART
#endif

/**
 * SECTION:ide-task
 * @title: IdeTask
 * @short_description: asynchronous task management
 *
 * #IdeTask is meant to be an improved form of #GTask. There are a few
 * deficiencies in #GTask that have made it unsuitable for certain use cases.
 *
 * #GTask does not provide a way to guarantee that the source object,
 * task data, and unused results are freed with in a given #GMainContext.
 * #IdeTask addresses this by having a more flexible result and object
 * ownership control.
 *
 * Furthermore, #IdeTask allows consumers to force disposal from a given
 * thread so that the data is released there.
 *
 * #IdeTask also supports chaining tasks together which makes it simpler
 * to avoid doing duplicate work by instead simply chaining the tasks.
 *
 * There are some costs to this design. It uses the main context a bit
 * more than #GTask may use it.
 *
 * #IdeTask allows setting a task kind which determines which thread pool
 * the task will be executed (and throttled) on.
 *
 * Because #IdeTask needs more control over result life-cycles (for chaining
 * results), additional return methods have been provided. Consumers should
 * use ide_task_return_boxed() when working with boxed types as it allows us
 * to copy the result to another task. Additionally, ide_task_return_object()
 * provides a simplified API over ide_task_return_pointer() which also allows
 * copying the result to chained tasks.
 *
 * Since: 3.32
 */

typedef struct
{
  /*
   * The pointer we were provided.
   */
  gpointer data;

  /*
   * The destroy notify for @data. We should only call this from the
   * main context associated with the task.
   */
  GDestroyNotify data_destroy;
} IdeTaskData;

typedef enum
{
  IDE_TASK_RESULT_NONE,
  IDE_TASK_RESULT_CANCELLED,
  IDE_TASK_RESULT_BOOLEAN,
  IDE_TASK_RESULT_INT,
  IDE_TASK_RESULT_ERROR,
  IDE_TASK_RESULT_OBJECT,
  IDE_TASK_RESULT_BOXED,
  IDE_TASK_RESULT_POINTER,
} IdeTaskResultType;

typedef struct
{
  /*
   * The type of result stored in our union @u.
   */
  IdeTaskResultType type;

  /*
   * To ensure that we can pass ownership back to the main context
   * from our worker thread, we need to be able to stash the reference
   * here in our result. It is also convenient as we need access to it
   * from the main context callback anyway.
   */
  IdeTask *task;

  /*
   * Additionally, we need to allow passing our main context reference
   * back so that it cannot be finalized in our thread.
   */
  GMainContext *main_context;

  /*
   * Priority for our GSource attached to @main_context.
   */
  gint complete_priority;

  /*
   * The actual result information, broken down by result @type.
   */
  union {
    gboolean  v_bool;
    gssize    v_int;
    GError   *v_error;
    GObject  *v_object;
    struct {
      GType    type;
      gpointer pointer;
    } v_boxed;
    struct {
      gpointer       pointer;
      GDestroyNotify destroy;
    } v_pointer;
  } u;
} IdeTaskResult;

typedef struct
{
  IdeTask      *task;
  GMainContext *main_context;
  gint          priority;
} IdeTaskCancel;

typedef struct
{
  /*
   * @global_link is used to store a pointer to the task in the global
   * queue during the lifetime of the task. This is a debugging feature
   * so that we can dump the list of active tasks from the debugger.
   */
  GList global_link;

  /*
   * Controls access to our private data. We only access structure
   * data while holding this mutex to ensure that we have consistency
   * between threads which could be accessing internals.
   */
  GMutex mutex;

  /*
   * The source object for the GAsyncResult interface. If we have set
   * release_on_propagate, this will be released when the task propagate
   * function is called.
   */
  gpointer source_object;

  /*
   * The cancellable that we're monitoring for task cancellation.
   */
  GCancellable *cancellable;

  /*
   * If ide_task_set_return_on_cancel() has been set, then we might be
   * listening for changes. Handling this will queue a completion
   */
  gulong cancel_handler;

  /*
   * The callback to execute upon completion of the operation. It will
   * be called from @main_contect after the operation completes.
   */
  GAsyncReadyCallback callback;
  gpointer user_data;

  /*
   * The name for the task. This string is interned so you should not
   * use dynamic names. They are meant to simplify the process of
   * debugging what task failed.
   */
  const gchar *name;

  /*
   * The GMainContext that was the thread default when the task was
   * created. Most operations are proxied back to this context so that
   * the consumer does not need to worry about thread safety.
   */
  GMainContext *main_context;

  /*
   * The task data that has been set for the task. Task data is released
   * from a callback in the #GMainContext if changed outside the main
   * context.
   */
  IdeTaskData *task_data;

  /*
   * The result for the task. If release_on_propagate as set to %FALSE,
   * then this may be kept around so that ide_task_chain() can be used to
   * duplicate the result to another task. This is convenient when multiple
   * async funcs race to do some work, allowing just a single winner with all
   * the callers getting the same result.
   */
  IdeTaskResult *result;

  /*
   * ide_task_chain() allows us to propagate the result of this task to
   * another task (for a limited number of result types). This is the
   * list of those tasks.
   */
  GPtrArray *chained;

  /*
   * If ide_task_run_in_thread() is called, this will be set to the func
   * that should be called from within the thread.
   */
  IdeTaskThreadFunc thread_func;

  /*
   * If we're running in a thread, we'll stash the value here until we
   * can complete things cleanly and pass ownership back as one operation.
   */
  IdeTaskResult *thread_result;

  /*
   * The source tag for the task, which can be used to determine what
   * the task is from a debugger as well as to verify correctness
   * in async finish functions.
   */
  gpointer source_tag;

#ifdef ENABLE_TIME_CHART
  /* The time the task was created */
  gint64 begin_time;
#endif

  /*
   * Our priority for scheduling tasks in the particular workqueue.
   */
  gint priority;

  /*
   * The priority for completing the result back on the main context. This
   * defaults to a value lower than gtk redraw priority to ensure that gtk
   * has higher priority than task completion.
   */
  gint complete_priority;

  /*
   * While we're waiting for our return callback, this is set to our
   * source id. We use that to know we need to block on the main loop
   * in case the user calls ide_task_propagate_*() synchronously without
   * round-triping to the main loop.
   */
  guint return_source;

  /*
   * Our kind of task, which is used to determine what thread pool we
   * can use when running threaded work. This can be used to help choke
   * lots of work down to a relatively small number of threads.
   */
  IdeTaskKind kind : 8;

  /*
   * If the task has been completed, which is to say that the callback
   * dispatch has occurred in @main_context.
   */
  guint completed : 1;

  /*
   * If we should check @cancellable before returning the result. If set
   * to true, and the cancellable was cancelled, an error will be returned
   * even if the task completed successfully.
   */
  guint check_cancellable : 1;

  /*
   * If we should synthesize completion from a GCancellable::cancelled
   * event instead of waiting for the task to complete normally.
   */
  guint return_on_cancel : 1;

  /*
   * If we should release the source object and task data after we've
   * dispatched the callback (or the callback was NULL). This allows us
   * to ensure that various dependent data are released in the main
   * context. This is the default and helps ensure thread-safety.
   */
  guint release_on_propagate : 1;

  /*
   * Protect against multiple return calls, and given the developer a good
   * warning so they catch this early.
   */
  guint return_called : 1;

  /*
   * If we got a result that was a cancellation, then we mark it here so
   * that we can deal with it cleanly later.
   */
  guint got_cancel : 1;

  /*
   * If we have dispatched to a thread already.
   */
  guint thread_called : 1;

} IdeTaskPrivate;

static void     async_result_init_iface (GAsyncResultIface *iface);
static void     ide_task_data_free      (IdeTaskData       *task_data);
static void     ide_task_result_free    (IdeTaskResult     *result);
static gboolean ide_task_return_cb      (gpointer           user_data);
static void     ide_task_release        (IdeTask           *self,
                                         gboolean           force);

G_DEFINE_AUTOPTR_CLEANUP_FUNC (IdeTaskData, ide_task_data_free);
G_DEFINE_AUTOPTR_CLEANUP_FUNC (IdeTaskResult, ide_task_result_free);

G_DEFINE_TYPE_WITH_CODE (IdeTask, ide_task, G_TYPE_OBJECT,
                         G_ADD_PRIVATE (IdeTask)
                         G_IMPLEMENT_INTERFACE (G_TYPE_ASYNC_RESULT, async_result_init_iface))

enum {
  PROP_0,
  PROP_COMPLETED,
  N_PROPS
};

static GParamSpec *properties [N_PROPS];
static GQueue global_task_list = G_QUEUE_INIT;

G_LOCK_DEFINE (global_task_list);

static void
ide_task_cancel_free (IdeTaskCancel *cancel)
{
  g_clear_pointer (&cancel->main_context, g_main_context_unref);
  g_clear_object (&cancel->task);
  g_slice_free (IdeTaskCancel, cancel);
}

static const gchar *
result_type_name (IdeTaskResultType type)
{
  switch (type)
    {
    case IDE_TASK_RESULT_NONE:
      return "none";

    case IDE_TASK_RESULT_CANCELLED:
      return "cancelled";

    case IDE_TASK_RESULT_INT:
      return "int";

    case IDE_TASK_RESULT_POINTER:
      return "pointer";

    case IDE_TASK_RESULT_OBJECT:
      return "object";

    case IDE_TASK_RESULT_BOXED:
      return "boxed";

    case IDE_TASK_RESULT_BOOLEAN:
      return "boolean";

    case IDE_TASK_RESULT_ERROR:
      return "error";

    default:
      return NULL;
    }
}

static void
ide_task_data_free (IdeTaskData *task_data)
{
  if (task_data->data_destroy != NULL)
    task_data->data_destroy (task_data->data);
  g_slice_free (IdeTaskData, task_data);
}

static IdeTaskResult *
ide_task_result_copy (const IdeTaskResult *src)
{
  IdeTaskResult *dst;

  dst = g_slice_new0 (IdeTaskResult);
  dst->type = src->type;

  switch (src->type)
    {
    case IDE_TASK_RESULT_INT:
      dst->u.v_int = src->u.v_int;
      break;

    case IDE_TASK_RESULT_BOOLEAN:
      dst->u.v_bool = src->u.v_bool;
      break;

    case IDE_TASK_RESULT_ERROR:
      dst->u.v_error = g_error_copy (src->u.v_error);
      break;

    case IDE_TASK_RESULT_OBJECT:
      dst->u.v_object = src->u.v_object ? g_object_ref (src->u.v_object) : NULL;
      break;

    case IDE_TASK_RESULT_BOXED:
      dst->u.v_boxed.type = src->u.v_boxed.type;
      dst->u.v_boxed.pointer = g_boxed_copy (src->u.v_boxed.type, src->u.v_boxed.pointer);
      break;

    case IDE_TASK_RESULT_POINTER:
      g_critical ("Cannot proxy raw pointers for task results");
      break;

    case IDE_TASK_RESULT_CANCELLED:
    case IDE_TASK_RESULT_NONE:
    default:
      break;
    }

  return g_steal_pointer (&dst);
}

static void
ide_task_result_free (IdeTaskResult *result)
{
  if (result == NULL)
    return;

  switch (result->type)
    {
    case IDE_TASK_RESULT_POINTER:
      if (result->u.v_pointer.destroy)
        result->u.v_pointer.destroy (result->u.v_pointer.pointer);
      break;

    case IDE_TASK_RESULT_ERROR:
      g_error_free (result->u.v_error);
      break;

    case IDE_TASK_RESULT_BOXED:
      if (result->u.v_boxed.pointer)
        g_boxed_free (result->u.v_boxed.type, result->u.v_boxed.pointer);
      break;

    case IDE_TASK_RESULT_OBJECT:
      g_clear_object (&result->u.v_object);
      break;

    case IDE_TASK_RESULT_BOOLEAN:
    case IDE_TASK_RESULT_INT:
    case IDE_TASK_RESULT_NONE:
    case IDE_TASK_RESULT_CANCELLED:
    default:
      break;
    }

  g_clear_object (&result->task);
  g_clear_pointer (&result->main_context, g_main_context_unref);
  g_slice_free (IdeTaskResult, result);
}

/*
 * ide_task_complete:
 * @result: (transfer full): the result to complete
 *
 * queues the completion for the task. make sure that you've
 * set the result->task, main_context, and priority first.
 *
 * This is designed to allow stealing the last reference from
 * a worker thread and pass it back to the main context.
 *
 * Returns: a gsource identifier
 */
static guint
ide_task_complete (IdeTaskResult *result)
{
  GSource *source;
  guint ret;

  g_assert (result != NULL);
  g_assert (IDE_IS_TASK (result->task));
  g_assert (result->main_context);

  source = g_idle_source_new ();
  g_source_set_name (source, "[ide-task] complete result");
  g_source_set_ready_time (source, -1);
  g_source_set_callback (source, ide_task_return_cb, result, NULL);
  g_source_set_priority (source, result->complete_priority);
  ret = g_source_attach (source, result->main_context);
  g_source_unref (source);

  return ret;
}

static void
ide_task_thread_func (gpointer data)
{
  g_autoptr(GObject) source_object = NULL;
  g_autoptr(GCancellable) cancellable = NULL;
  g_autoptr(IdeTask) task = data;
  IdeTaskPrivate *priv = ide_task_get_instance_private (task);
  gpointer task_data = NULL;
  IdeTaskThreadFunc thread_func;

  g_assert (IDE_IS_TASK (task));

  g_mutex_lock (&priv->mutex);
  source_object = priv->source_object ? g_object_ref (priv->source_object) : NULL;
  cancellable = priv->cancellable ? g_object_ref (priv->cancellable) : NULL;
  if (priv->task_data)
    task_data = priv->task_data->data;
  thread_func = priv->thread_func;
  priv->thread_func = NULL;
  g_mutex_unlock (&priv->mutex);

  g_assert (thread_func != NULL);

  thread_func (task, source_object, task_data, cancellable);

  g_clear_object (&source_object);
  g_clear_object (&cancellable);

  g_mutex_lock (&priv->mutex);

  /*
   * We've delayed our ide_task_return() until we reach here, so now
   * we can steal our object instance and complete the task along with
   * ensuring the object wont be finalized from this thread.
   */
  if (priv->thread_result)
    {
      IdeTaskResult *result = g_steal_pointer (&priv->thread_result);

      g_assert (result->task == task);
      g_clear_object (&result->task);
      result->task = g_steal_pointer (&task);

      priv->return_source = ide_task_complete (g_steal_pointer (&result));

      g_assert (source_object == NULL);
      g_assert (cancellable == NULL);
      g_assert (task == NULL);
    }
  else
    {
      /* The task did not return a value while in the thread func!  GTask
       * doesn't support this, but its useful to us in a number of ways, so
       * we'll begrudgingly support it but the best we can do is drop our
       * reference from the thread.
       */
    }

  g_mutex_unlock (&priv->mutex);

  g_assert (source_object == NULL);
  g_assert (cancellable == NULL);
}

static void
ide_task_dispose (GObject *object)
{
  IdeTask *self = (IdeTask *)object;
  IdeTaskPrivate *priv = ide_task_get_instance_private (self);

  g_assert (IDE_IS_TASK (self));

  ide_task_release (self, TRUE);

  g_mutex_lock (&priv->mutex);
  g_clear_pointer (&priv->result, ide_task_result_free);
  g_mutex_unlock (&priv->mutex);

  G_OBJECT_CLASS (ide_task_parent_class)->dispose (object);
}

static void
ide_task_finalize (GObject *object)
{
  IdeTask *self = (IdeTask *)object;
  IdeTaskPrivate *priv = ide_task_get_instance_private (self);

  G_LOCK (global_task_list);
  g_queue_unlink (&global_task_list, &priv->global_link);
  G_UNLOCK (global_task_list);

  if (!priv->return_called)
    g_critical ("%s [%s] finalized before completing",
                G_OBJECT_TYPE_NAME (self),
                priv->name ?: "unnamed");
  else if (priv->chained && priv->chained->len)
    g_critical ("%s [%s] finalized before dependents were notified",
                G_OBJECT_TYPE_NAME (self),
                priv->name ?: "unnamed");
  else if (priv->thread_func)
    g_critical ("%s [%s] finalized while thread_func is active",
                G_OBJECT_TYPE_NAME (self),
                priv->name ?: "unnamed");
  else if (!priv->completed)
    g_critical ("%s [%s] finalized before completion",
                G_OBJECT_TYPE_NAME (self),
                priv->name ?: "unnamed");

  g_assert (priv->return_source == 0);
  g_assert (priv->result == NULL);
  g_assert (priv->task_data == NULL);
  g_assert (priv->source_object == NULL);
  g_assert (priv->chained == NULL);
  g_assert (priv->thread_result == NULL);

  g_clear_pointer (&priv->main_context, g_main_context_unref);
  g_clear_object (&priv->cancellable);
  g_mutex_clear (&priv->mutex);

  G_OBJECT_CLASS (ide_task_parent_class)->finalize (object);
}

static void
ide_task_get_property (GObject    *object,
                       guint       prop_id,
                       GValue     *value,
                       GParamSpec *pspec)
{
  IdeTask *self = IDE_TASK (object);

  switch (prop_id)
    {
    case PROP_COMPLETED:
      g_value_set_boolean (value, ide_task_get_completed (self));
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
ide_task_class_init (IdeTaskClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->dispose = ide_task_dispose;
  object_class->finalize = ide_task_finalize;
  object_class->get_property = ide_task_get_property;

  properties [PROP_COMPLETED] =
    g_param_spec_boolean ("completed",
                          "Completed",
                          "If the task has completed",
                          FALSE,
                          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

  g_object_class_install_properties (object_class, N_PROPS, properties);

  /* This can be called multiple times, so we use this to allow
   * unit tests to work without having to expose the function as
   * public API.
   */
  _ide_thread_pool_init (FALSE);
}

static void
ide_task_init (IdeTask *self)
{
  IdeTaskPrivate *priv = ide_task_get_instance_private (self);

  g_mutex_init (&priv->mutex);

  priv->check_cancellable = TRUE;
  priv->release_on_propagate = TRUE;
  priv->priority = G_PRIORITY_DEFAULT;
  priv->complete_priority = PRIORITY_REDRAW + 1;
  priv->main_context = g_main_context_ref_thread_default ();
  priv->global_link.data = self;

  G_LOCK (global_task_list);
  g_queue_push_tail_link (&global_task_list, &priv->global_link);
  G_UNLOCK (global_task_list);
}

/**
 * ide_task_get_source_object: (skip)
 * @self: a #IdeTask
 *
 * Gets the #GObject used when creating the source object.
 *
 * As this does not provide ownership transfer of the #GObject, it is a
 * programmer error to call this function outside of a thread worker called
 * from ide_task_run_in_thread() or outside the #GMainContext that is
 * associated with the task.
 *
 * If you need to access the object in other scenarios, you must use the
 * g_async_result_get_source_object() which provides a full reference to the
 * source object, safely. You are responsible for ensuring that you do not
 * release the object in a manner that is unsafe for the source object.
 *
 * Returns: (transfer none) (nullable) (type GObject.Object): a #GObject or %NULL
 *
 * Since: 3.32
 */
gpointer
ide_task_get_source_object (IdeTask *self)
{
  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
  gpointer ret;

  g_return_val_if_fail (IDE_IS_TASK (self), NULL);

  g_mutex_lock (&priv->mutex);
  ret = priv->source_object;
  g_mutex_unlock (&priv->mutex);

  return ret;
}

/**
 * ide_task_new:
 * @source_object: (type GObject.Object) (nullable): a #GObject or %NULL
 * @cancellable: (nullable): a #GCancellable or %NULL
 * @callback: (scope async) (nullable): a #GAsyncReadyCallback or %NULL
 * @user_data: closure data for @callback
 *
 * Creates a new #IdeTask.
 *
 * #IdeTask is similar to #GTask but provides some additional guarantees
 * such that by default, the source object, task data, and unused results
 * are guaranteed to be finalized in the #GMainContext associated with
 * the task itself.
 *
 * Returns: (transfer full): an #IdeTask
 *
 * Since: 3.32
 */
IdeTask *
(ide_task_new) (gpointer             source_object,
                GCancellable        *cancellable,
                GAsyncReadyCallback  callback,
                gpointer             user_data)
{
  g_autoptr(IdeTask) self = NULL;
  IdeTaskPrivate *priv;

  g_return_val_if_fail (!source_object || G_IS_OBJECT (source_object), NULL);
  g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), NULL);

  self = g_object_new (IDE_TYPE_TASK, NULL);
  priv = ide_task_get_instance_private (self);

  priv->source_object = source_object ? g_object_ref (source_object) : NULL;
  priv->cancellable = cancellable ? g_object_ref (cancellable) : NULL;
  priv->callback = callback;
  priv->user_data = user_data;
#ifdef ENABLE_TIME_CHART
  priv->begin_time = g_get_monotonic_time ();
#endif

  return g_steal_pointer (&self);
}

/**
 * ide_task_is_valid:
 * @self: (nullable) (type IdeTask): a #IdeTask
 * @source_object: (nullable): a #GObject or %NULL
 *
 * Checks if @source_object matches the object the task was created with.
 *
 * Returns: %TRUE is source_object matches
 *
 * Since: 3.32
 */
gboolean
ide_task_is_valid (gpointer self,
                   gpointer source_object)
{
  IdeTaskPrivate *priv = ide_task_get_instance_private (self);

  return IDE_IS_TASK (self) && priv->source_object == source_object;
}

/**
 * ide_task_get_completed:
 * @self: a #IdeTask
 *
 * Gets the "completed" property. This is %TRUE after the callback used when
 * creating the task has been executed.
 *
 * The property will be notified using g_object_notify() exactly once in the
 * same #GMainContext as the callback.
 *
 * Returns: %TRUE if the task has completed
 *
 * Since: 3.32
 */
gboolean
ide_task_get_completed (IdeTask *self)
{
  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
  gboolean ret;

  g_return_val_if_fail (IDE_IS_TASK (self), FALSE);

  g_mutex_lock (&priv->mutex);
  ret = priv->completed;
  g_mutex_unlock (&priv->mutex);

  return ret;
}

gint
ide_task_get_priority (IdeTask *self)
{
  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
  gint ret;

  g_return_val_if_fail (IDE_IS_TASK (self), 0);

  g_mutex_lock (&priv->mutex);
  ret = priv->priority;
  g_mutex_unlock (&priv->mutex);

  return ret;
}

void
ide_task_set_priority (IdeTask *self,
                       gint     priority)
{
  IdeTaskPrivate *priv = ide_task_get_instance_private (self);

  g_return_if_fail (IDE_IS_TASK (self));

  g_mutex_lock (&priv->mutex);
  priv->priority = priority;
  g_mutex_unlock (&priv->mutex);
}

gint
ide_task_get_complete_priority (IdeTask *self)
{
  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
  gint ret;

  g_return_val_if_fail (IDE_IS_TASK (self), 0);

  g_mutex_lock (&priv->mutex);
  ret = priv->complete_priority;
  g_mutex_unlock (&priv->mutex);

  return ret;
}

void
ide_task_set_complete_priority (IdeTask *self,
                                gint     complete_priority)
{
  IdeTaskPrivate *priv = ide_task_get_instance_private (self);

  g_return_if_fail (IDE_IS_TASK (self));

  g_mutex_lock (&priv->mutex);
  priv->complete_priority = complete_priority;
  g_mutex_unlock (&priv->mutex);
}

/**
 * ide_task_get_cancellable:
 * @self: a #IdeTask
 *
 * Gets the #GCancellable for the task.
 *
 * Returns: (transfer none) (nullable): a #GCancellable or %NULL
 *
 * Since: 3.32
 */
GCancellable *
ide_task_get_cancellable (IdeTask *self)
{
  IdeTaskPrivate *priv = ide_task_get_instance_private (self);

  g_return_val_if_fail (IDE_IS_TASK (self), NULL);

  return priv->cancellable;
}

static void
ide_task_deliver_result (IdeTask       *self,
                         IdeTaskResult *result)
{
  IdeTaskPrivate *priv = ide_task_get_instance_private (self);

  g_assert (IDE_IS_TASK (self));
  g_assert (result != NULL);
  g_assert (result->task == NULL);
  g_assert (result->main_context == NULL);

  /* This task was chained from another task. This completes the result
   * and we should dispatch the callback. To simplify the dispatching and
   * help prevent any re-entrancy issues, we defer back to the main context
   * to complete the operation.
   */

  result->task = g_object_ref (self);
  result->main_context = g_main_context_ref (priv->main_context);
  result->complete_priority = priv->complete_priority;

  g_mutex_lock (&priv->mutex);

  priv->return_called = TRUE;
  priv->return_source = ide_task_complete (g_steal_pointer (&result));

  g_mutex_unlock (&priv->mutex);
}

static void
ide_task_release (IdeTask  *self,
                  gboolean  force)
{
  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
  g_autoptr(IdeTaskData) task_data = NULL;
  g_autoptr(GObject) source_object = NULL;
  g_autoptr(GPtrArray) chained = NULL;

  g_assert (IDE_IS_TASK (self));

  g_mutex_lock (&priv->mutex);
  if (force || priv->release_on_propagate)
    {
      source_object = g_steal_pointer (&priv->source_object);
      task_data = g_steal_pointer (&priv->task_data);
      chained = g_steal_pointer (&priv->chained);
    }
  g_mutex_unlock (&priv->mutex);

  if (chained)
    {
      for (guint i = 0; i < chained->len; i++)
        {
          IdeTask *task = g_ptr_array_index (chained, i);

          ide_task_return_new_error (task,
                                     G_IO_ERROR,
                                     G_IO_ERROR_FAILED,
                                     "Error synthesized for task, parent task disposed");
        }
    }
}

static gboolean
ide_task_return_cb (gpointer user_data)
{
  g_autoptr(IdeTask) self = NULL;
  g_autoptr(IdeTaskResult) result = user_data;
  g_autoptr(IdeTaskResult) result_copy = NULL;
  g_autoptr(GCancellable) cancellable = NULL;
  g_autoptr(GObject) source_object = NULL;
  g_autoptr(GPtrArray) chained = NULL;
  GAsyncReadyCallback callback = NULL;
  gpointer callback_data = NULL;
  IdeTaskPrivate *priv;

  g_assert (result != NULL);
  g_assert (IDE_IS_TASK (result->task));

  /* We steal the task object, because we only stash it in the result
   * structure to get it here. And if we held onto it, we would have
   * a reference cycle.
   */
  self = g_steal_pointer (&result->task);
  priv = ide_task_get_instance_private (self);

#ifdef ENABLE_TIME_CHART
  g_message ("TASK-END: %s: duration=%lf",
             priv->name,
             (g_get_monotonic_time () - priv->begin_time) / (gdouble)G_USEC_PER_SEC);
#endif

  g_mutex_lock (&priv->mutex);

  g_assert (priv->return_source != 0);

  priv->return_source = 0;

  if (priv->got_cancel && priv->result != NULL)
    {
      /* We can discard this since we already handled a result for the
       * task. We delivered this here just so that we could finalize
       * any objects back inside them main context.
       */
      g_mutex_unlock (&priv->mutex);
      return G_SOURCE_REMOVE;
    }

  g_assert (priv->result == NULL);
  g_assert (priv->return_called == TRUE);

  priv->result = g_steal_pointer (&result);

  callback = priv->callback;
  callback_data = priv->user_data;

  priv->callback = NULL;
  priv->user_data = NULL;

  source_object = priv->source_object ? g_object_ref (priv->source_object) : NULL;
  cancellable = priv->cancellable ? g_object_ref (priv->cancellable) : NULL;

  chained = g_steal_pointer (&priv->chained);

  /* Make a private copy of the result data if we're going to need to notify
   * other tasks of our result. We can't guarantee the result in @task will
   * stay alive during our dispatch callbacks, so we need to have a copy.
   */
  if (chained != NULL && chained->len > 0)
    result_copy = ide_task_result_copy (priv->result);

  g_mutex_unlock (&priv->mutex);

  if (callback)
    callback (source_object, G_ASYNC_RESULT (self), callback_data);

  if (chained)
    {
      for (guint i = 0; i < chained->len; i++)
        {
          IdeTask *other = g_ptr_array_index (chained, i);
          g_autoptr(IdeTaskResult) other_result = ide_task_result_copy (result_copy);

          ide_task_deliver_result (other, g_steal_pointer (&other_result));
        }
    }

  g_mutex_lock (&priv->mutex);
  priv->completed = TRUE;
  g_mutex_unlock (&priv->mutex);

  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_COMPLETED]);

  ide_task_release (self, FALSE);

  return G_SOURCE_REMOVE;
}

static gboolean
ide_task_return_dummy_cb (gpointer data)
{
  return G_SOURCE_REMOVE;
}

static void
ide_task_return (IdeTask       *self,
                 IdeTaskResult *result)
{
  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
  g_autoptr(GMutexLocker) locker = NULL;

  g_assert (IDE_IS_TASK (self));
  g_assert (result != NULL);
  g_assert (result->task == NULL);

  locker = g_mutex_locker_new (&priv->mutex);

  if (priv->cancel_handler && priv->cancellable)
    {
      g_cancellable_disconnect (priv->cancellable, priv->cancel_handler);
      priv->cancel_handler = 0;
    }

  if (priv->return_called)
    {
      GSource *source;

      if (result->type == IDE_TASK_RESULT_CANCELLED)
        {
          /* We already had a result, and now raced to be notified of
           * cancellation. We can safely free this result even if we're
           * currently in a worker thread.
           */
          ide_task_result_free (result);
          return;
        }

      /* If we haven't been cancelled, then we reached this path multiple
       * times by programmer error.
       */
      if (!priv->got_cancel)
        g_critical ("Attempted to set result on task [%s] multiple times", priv->name);

      /*
       * This task has already returned, but we need to ensure that we pass
       * the data back to the main context so that it is freed appropriately.
       */

      source = g_idle_source_new ();
      g_source_set_name (source, "[ide-task] finalize task result");
      g_source_set_ready_time (source, -1);
      g_source_set_callback (source,
                             ide_task_return_dummy_cb,
                             result,
                             (GDestroyNotify)ide_task_result_free);
      g_source_attach (source, priv->main_context);
      g_source_unref (source);

      return;
    }

  priv->return_called = TRUE;

  if (result->type == IDE_TASK_RESULT_CANCELLED)
    priv->got_cancel = TRUE;

  result->task = g_object_ref (self);
  result->main_context = g_main_context_ref (priv->main_context);
  result->complete_priority = priv->complete_priority;

  /* We can queue the result immediately if we're not being called
   * while we're inside of a ide_task_run_in_thread() callback. Otherwise,
   * that thread cleanup must complete this to ensure objects cannot
   * be finalized in that thread.
   */
  if (!priv->thread_called || IDE_IS_MAIN_THREAD ())
    priv->return_source = ide_task_complete (result);
  else if (priv->return_on_cancel && result->type == IDE_TASK_RESULT_CANCELLED)
    priv->return_source = ide_task_complete (result);
  else
    priv->thread_result = result;
}

/**
 * ide_task_return_int:
 * @self: a #IdeTask
 * @result: the result for the task
 *
 * Sets the result of the task to @result.
 *
 * Other tasks depending on the result will be notified after returning
 * to the #GMainContext of the task.
 *
 * Since: 3.32
 */
void
ide_task_return_int (IdeTask *self,
                     gssize   result)
{
  IdeTaskResult *ret;

  g_return_if_fail (IDE_IS_TASK (self));

  ret = g_slice_new0 (IdeTaskResult);
  ret->type = IDE_TASK_RESULT_INT;
  ret->u.v_int = result;

  ide_task_return (self, g_steal_pointer (&ret));
}

/**
 * ide_task_return_boolean:
 * @self: a #IdeTask
 * @result: the result for the task
 *
 * Sets the result of the task to @result.
 *
 * Other tasks depending on the result will be notified after returning
 * to the #GMainContext of the task.
 *
 * Since: 3.32
 */
void
ide_task_return_boolean (IdeTask  *self,
                         gboolean  result)
{
  IdeTaskResult *ret;

  g_return_if_fail (IDE_IS_TASK (self));

  ret = g_slice_new0 (IdeTaskResult);
  ret->type = IDE_TASK_RESULT_BOOLEAN;
  ret->u.v_bool = !!result;

  ide_task_return (self, g_steal_pointer (&ret));
}

/**
 * ide_task_return_boxed: (skip)
 * @self: a #IdeTask
 * @result_type: the #GType of the boxed type
 * @result: (transfer full): the result to be returned
 *
 * This is similar to ide_task_return_pointer(), but allows the task to
 * know the boxed #GType so that the result may be propagated to chained
 * tasks.
 *
 * Since: 3.32
 */
void
ide_task_return_boxed (IdeTask  *self,
                       GType     result_type,
                       gpointer  result)
{
  IdeTaskResult *ret;

  g_return_if_fail (IDE_IS_TASK (self));
  g_return_if_fail (result_type != G_TYPE_INVALID);
  g_return_if_fail (G_TYPE_IS_BOXED (result_type));

  ret = g_slice_new0 (IdeTaskResult);
  ret->type = IDE_TASK_RESULT_BOXED;
  ret->u.v_boxed.type = result_type;
  ret->u.v_boxed.pointer = result;

  ide_task_return (self, g_steal_pointer (&ret));
}

/**
 * ide_task_return_object:
 * @self: a #IdeTask
 * @instance: (transfer full) (type GObject.Object): a #GObject instance
 *
 * Returns a new object instance.
 *
 * Takes ownership of @instance to allow saving a reference increment and
 * decrement by the caller.
 *
 * Since: 3.32
 */
void
ide_task_return_object (IdeTask  *self,
                        gpointer  instance)
{
  IdeTaskResult *ret;

  g_return_if_fail (IDE_IS_TASK (self));
  g_return_if_fail (!instance || G_IS_OBJECT (instance));

  ret = g_slice_new0 (IdeTaskResult);
  ret->type = IDE_TASK_RESULT_OBJECT;
  ret->u.v_object = instance;

  ide_task_return (self, g_steal_pointer (&ret));
}

/**
 * ide_task_return_pointer: (skip)
 * @self: a #IdeTask
 * @data: the data to return
 * @destroy: an optional #GDestroyNotify to cleanup data if no handler
 *   propagates the result
 *
 * Returns a new raw pointer.
 *
 * Note that pointers cannot be chained to other tasks, so you may not
 * use ide_task_chain() in conjunction with a task returning a pointer
 * using ide_task_return_pointer().
 *
 * If you need task chaining with pointers, see ide_task_return_boxed()
 * or ide_task_return_object().
 *
 * Since: 3.32
 */
void
(ide_task_return_pointer) (IdeTask        *self,
                           gpointer        data,
                           GDestroyNotify  destroy)
{
  IdeTaskResult *ret;

  g_return_if_fail (IDE_IS_TASK (self));

  ret = g_slice_new0 (IdeTaskResult);
  ret->type = IDE_TASK_RESULT_POINTER;
  ret->u.v_pointer.pointer = data;
  ret->u.v_pointer.destroy = destroy;

  ide_task_return (self, g_steal_pointer (&ret));
}

/**
 * ide_task_return_error:
 * @self: a #IdeTask
 * @error: (transfer full): a #GError
 *
 * Sets @error as the result of the #IdeTask
 *
 * Since: 3.32
 */
void
ide_task_return_error (IdeTask *self,
                       GError  *error)
{
  IdeTaskResult *ret;

  g_return_if_fail (IDE_IS_TASK (self));

  ret = g_slice_new0 (IdeTaskResult);
  ret->type = IDE_TASK_RESULT_ERROR;
  ret->u.v_error = error;

  ide_task_return (self, g_steal_pointer (&ret));
}

/**
 * ide_task_return_new_error:
 * @self: a #IdeTask
 * @error_domain: the error domain of the #GError
 * @error_code: the error code for the #GError
 * @format: the printf-style format string
 *
 * Creates a new #GError and sets it as the result for the task.
 *
 * Since: 3.32
 */
void
ide_task_return_new_error (IdeTask     *self,
                           GQuark       error_domain,
                           gint         error_code,
                           const gchar *format,
                           ...)
{
  GError *error;
  va_list args;

  g_return_if_fail (IDE_IS_TASK (self));

  va_start (args, format);
  error = g_error_new_valist (error_domain, error_code, format, args);
  va_end (args);

  ide_task_return_error (self, g_steal_pointer (&error));
}

/**
 * ide_task_return_error_if_cancelled:
 * @self: a #IdeTask
 *
 * Returns a new #GError if the cancellable associated with the task
 * has been cancelled. If so, %TRUE is returned, otherwise %FALSE.
 *
 * If the source object related to the task is an #IdeObject and that
 * object has had been requested to destroy, it too will be considered
 * a cancellation state.
 *
 * Returns: %TRUE if the task was cancelled and error returned.
 *
 * Since: 3.32
 */
gboolean
ide_task_return_error_if_cancelled (IdeTask *self)
{
  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
  gboolean failed;

  g_return_val_if_fail (IDE_IS_TASK (self), FALSE);

  g_mutex_lock (&priv->mutex);
  failed = g_cancellable_is_cancelled (priv->cancellable) ||
    (IDE_IS_OBJECT (priv->source_object) &&
     ide_object_in_destruction (IDE_OBJECT (priv->source_object)));
  g_mutex_unlock (&priv->mutex);

  if (failed)
    ide_task_return_new_error (self,
                               G_IO_ERROR,
                               G_IO_ERROR_CANCELLED,
                               "The task was cancelled");

  return failed;
}

/**
 * ide_task_set_release_on_propagate:
 * @self: a #IdeTask
 * @release_on_propagate: if data should be released on propagate
 *
 * Setting this to %TRUE (the default) ensures that the task will release all
 * task data and source_object references after executing the configured
 * callback. This is useful to ensure that dependent objects are finalized
 * in the thread-default #GMainContext the task was created in.
 *
 * Generally, you want to leave this as %TRUE to ensure thread-safety on the
 * dependent objects and task data.
 *
 * Since: 3.32
 */
void
ide_task_set_release_on_propagate (IdeTask  *self,
                                   gboolean  release_on_propagate)
{
  IdeTaskPrivate *priv = ide_task_get_instance_private (self);

  g_return_if_fail (IDE_IS_TASK (self));

  release_on_propagate = !!release_on_propagate;

  g_mutex_lock (&priv->mutex);
  priv->release_on_propagate = release_on_propagate;
  g_mutex_unlock (&priv->mutex);
}

/**
 * ide_task_set_source_tag:
 * @self: a #IdeTask
 * @source_tag: a tag to identify the task, usual a function pointer
 *
 * Sets the source tag for the task. Generally this is a function pointer
 * of the function that created the task.
 *
 * Since: 3.32
 */
void
ide_task_set_source_tag (IdeTask  *self,
                         gpointer  source_tag)
{
  IdeTaskPrivate *priv = ide_task_get_instance_private (self);

  g_return_if_fail (IDE_IS_TASK (self));

  g_mutex_lock (&priv->mutex);
  priv->source_tag = source_tag;
  g_mutex_unlock (&priv->mutex);
}

/**
 * ide_task_set_check_cancellable:
 * @self: a #IdeTask
 * @check_cancellable: %TRUE if the cancellable should be checked
 *
 * Setting @check_cancellable to %TRUE (the default) ensures that the
 * #GCancellable used when creating the #IdeTask is checked for cancellation
 * before propagating a result. If cancelled, an error will be returned
 * instead of the result.
 *
 * Since: 3.32
 */
void
ide_task_set_check_cancellable (IdeTask  *self,
                                gboolean  check_cancellable)
{
  IdeTaskPrivate *priv = ide_task_get_instance_private (self);

  g_return_if_fail (IDE_IS_TASK (self));

  check_cancellable = !!check_cancellable;

  g_mutex_lock (&priv->mutex);
  priv->check_cancellable = check_cancellable;
  g_mutex_unlock (&priv->mutex);
}

/**
 * ide_task_run_in_thread: (skip)
 * @self: a #IdeTask
 * @thread_func: a function to execute on a worker thread
 *
 * Scheules @thread_func to be executed on a worker thread.
 *
 * @thread_func must complete the task from the worker thread using one of
 * ide_task_return_boolean(), ide_task_return_int(), or
 * ide_task_return_pointer().
 *
 * Since: 3.32
 */
void
ide_task_run_in_thread (IdeTask           *self,
                        IdeTaskThreadFunc  thread_func)
{
  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
  g_autoptr(GError) error = NULL;

  g_return_if_fail (IDE_IS_TASK (self));
  g_return_if_fail (thread_func != NULL);

  g_mutex_lock (&priv->mutex);

  if (priv->completed == TRUE)
    {
      g_critical ("Task already completed, cannot run in thread");
      goto unlock;
    }

  if (priv->thread_called)
    {
      g_critical ("Run in thread already called, cannot run again");
      goto unlock;
    }

  priv->thread_called = TRUE;
  priv->thread_func = thread_func;

  ide_thread_pool_push_with_priority ((IdeThreadPoolKind)priv->kind,
                                      priv->priority,
                                      ide_task_thread_func,
                                      g_object_ref (self));

unlock:
  g_mutex_unlock (&priv->mutex);

  if (error != NULL)
    ide_task_return_error (self, g_steal_pointer (&error));
}

static IdeTaskResult *
ide_task_propagate_locked (IdeTask            *self,
                           IdeTaskResultType   expected_type,
                           GError            **error)
{
  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
  IdeTaskResult *ret = NULL;

  g_assert (IDE_IS_TASK (self));
  g_assert (expected_type > IDE_TASK_RESULT_NONE);

  if (priv->result == NULL)
    {
      g_autoptr(GMainContext) context = g_main_context_ref (priv->main_context);

      while (priv->return_source)
        {
          g_mutex_unlock (&priv->mutex);
          g_main_context_iteration (context, FALSE);
          g_mutex_lock (&priv->mutex);
        }
    }

  if (priv->result == NULL)
    {
      g_set_error_literal (error,
                           G_IO_ERROR,
                           G_IO_ERROR_FAILED,
                           "No result available for task");
    }
  else if (priv->result->type == IDE_TASK_RESULT_ERROR)
    {
      if (error != NULL)
        *error = g_error_copy (priv->result->u.v_error);
    }
  else if ((priv->check_cancellable && g_cancellable_is_cancelled (priv->cancellable)) ||
           priv->result->type == IDE_TASK_RESULT_CANCELLED)
    {
      g_set_error (error,
                   G_IO_ERROR,
                   G_IO_ERROR_CANCELLED,
                   "The operation was cancelled");
    }
  else if (IDE_IS_OBJECT (priv->source_object) &&
           ide_object_in_destruction (IDE_OBJECT (priv->source_object)))
    {
      g_set_error (error,
                   G_IO_ERROR,
                   G_IO_ERROR_CANCELLED,
                   "The object was destroyed while the task executed");
    }
  else if (priv->result->type != expected_type)
    {
      g_set_error (error,
                   G_IO_ERROR,
                   G_IO_ERROR_FAILED,
                   "Task expected result of %s got %s",
                   result_type_name (expected_type),
                   result_type_name (priv->result->type));
    }
  else
    {
      g_assert (priv->result != NULL);
      g_assert (priv->result->type == expected_type);

      if (priv->release_on_propagate)
        ret = g_steal_pointer (&priv->result);
      else if (priv->result->type == IDE_TASK_RESULT_POINTER)
        ret = g_steal_pointer (&priv->result);
      else
        ret = ide_task_result_copy (priv->result);

      g_assert (ret != NULL);
    }

  return ret;
}

gboolean
ide_task_propagate_boolean (IdeTask  *self,
                            GError  **error)
{
  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
  g_autoptr(GMutexLocker) locker = NULL;
  g_autoptr(IdeTaskResult) res = NULL;

  g_return_val_if_fail (IDE_IS_TASK (self), FALSE);

  locker = g_mutex_locker_new (&priv->mutex);

  if (!(res = ide_task_propagate_locked (self, IDE_TASK_RESULT_BOOLEAN, error)))
    return FALSE;

  return res->u.v_bool;
}

gpointer
ide_task_propagate_boxed (IdeTask  *self,
                          GError  **error)
{
  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
  g_autoptr(GMutexLocker) locker = NULL;
  g_autoptr(IdeTaskResult) res = NULL;

  g_return_val_if_fail (IDE_IS_TASK (self), NULL);

  locker = g_mutex_locker_new (&priv->mutex);

  if (!(res = ide_task_propagate_locked (self, IDE_TASK_RESULT_BOXED, error)))
    return NULL;

  return g_boxed_copy (res->u.v_boxed.type, res->u.v_boxed.pointer);
}

gssize
ide_task_propagate_int (IdeTask  *self,
                        GError  **error)
{
  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
  g_autoptr(GMutexLocker) locker = NULL;
  g_autoptr(IdeTaskResult) res = NULL;

  g_return_val_if_fail (IDE_IS_TASK (self), 0);

  locker = g_mutex_locker_new (&priv->mutex);

  if (!(res = ide_task_propagate_locked (self, IDE_TASK_RESULT_INT, error)))
    return 0;

  return res->u.v_int;
}

/**
 * ide_task_propagate_object:
 * @self: a #IdeTask
 * @error: a location for a #GError, or %NULL
 *
 * Returns an object if the task completed with an object. Otherwise, %NULL
 * is returned.
 *
 * @error is set if the task completed with an error.
 *
 * Returns: (transfer full) (type GObject.Object): a #GObject or %NULL
 *   and @error may be set.
 *
 * Since: 3.32
 */
gpointer
ide_task_propagate_object (IdeTask  *self,
                           GError  **error)
{
  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
  g_autoptr(GMutexLocker) locker = NULL;
  g_autoptr(IdeTaskResult) res = NULL;

  g_return_val_if_fail (IDE_IS_TASK (self), NULL);

  locker = g_mutex_locker_new (&priv->mutex);

  if (!(res = ide_task_propagate_locked (self, IDE_TASK_RESULT_OBJECT, error)))
    return NULL;

  return g_steal_pointer (&res->u.v_object);
}

gpointer
ide_task_propagate_pointer (IdeTask  *self,
                            GError  **error)
{
  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
  g_autoptr(GMutexLocker) locker = NULL;
  g_autoptr(IdeTaskResult) res = NULL;
  gpointer ret;

  g_return_val_if_fail (IDE_IS_TASK (self), NULL);

  locker = g_mutex_locker_new (&priv->mutex);

  if (!(res = ide_task_propagate_locked (self, IDE_TASK_RESULT_POINTER, error)))
    return NULL;

  ret = g_steal_pointer (&res->u.v_pointer.pointer);
  res->u.v_pointer.destroy = NULL;

  return ret;
}

/**
 * ide_task_chain:
 * @self: a #IdeTask
 * @other_task: a #IdeTask
 *
 * Causes the result of @self to also be delivered to @other_task.
 *
 * This API is useful in situations when you want to avoid doing the same
 * work multiple times, and can share the result between mutliple async
 * operations requesting the same work.
 *
 * Users of this API must make sure one of two things is true. Either they
 * have called ide_task_set_release_on_propagate() with @self and set
 * release_on_propagate to %FALSE, or @self has not yet completed.
 *
 * Since: 3.32
 */
void
ide_task_chain (IdeTask *self,
                IdeTask *other_task)
{
  IdeTaskPrivate *self_priv = ide_task_get_instance_private (self);

  g_return_if_fail (IDE_IS_TASK (self));
  g_return_if_fail (IDE_IS_TASK (other_task));
  g_return_if_fail (self != other_task);

  g_mutex_lock (&self_priv->mutex);

  if (self_priv->result)
    {
      IdeTaskResult *copy = ide_task_result_copy (self_priv->result);

      if (copy != NULL)
        ide_task_deliver_result (other_task, g_steal_pointer (&copy));
      else
        ide_task_return_new_error (other_task,
                                   G_IO_ERROR,
                                   G_IO_ERROR_FAILED,
                                   "Result could not be copied to task");
    }
  else
    {
      if (self_priv->chained == NULL)
        self_priv->chained = g_ptr_array_new_with_free_func (g_object_unref);
      g_ptr_array_add (self_priv->chained, g_object_ref (other_task));
    }

  g_mutex_unlock (&self_priv->mutex);
}

gpointer
ide_task_get_source_tag (IdeTask *self)
{
  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
  gpointer ret;

  g_return_val_if_fail (IDE_IS_TASK (self), NULL);

  g_mutex_lock (&priv->mutex);
  ret = priv->source_tag;
  g_mutex_unlock (&priv->mutex);

  return ret;
}

IdeTaskKind
ide_task_get_kind (IdeTask *self)
{
  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
  IdeTaskKind kind;

  g_return_val_if_fail (IDE_IS_TASK (self), 0);

  g_mutex_lock (&priv->mutex);
  kind = priv->kind;
  g_mutex_unlock (&priv->mutex);

  return kind;
}

void
ide_task_set_kind (IdeTask     *self,
                   IdeTaskKind  kind)
{
  IdeTaskPrivate *priv = ide_task_get_instance_private (self);

  g_return_if_fail (IDE_IS_TASK (self));
  g_return_if_fail (kind >= IDE_TASK_KIND_DEFAULT);
  g_return_if_fail (kind < IDE_TASK_KIND_LAST);

  g_mutex_lock (&priv->mutex);
  priv->kind = kind;
  g_mutex_unlock (&priv->mutex);
}

/**
 * ide_task_get_task_data: (skip)
 * @self: a #IdeTask
 *
 * Gets the task data previously set with ide_task_set_task_data().
 *
 * Returns: (transfer none): previously registered task data or %NULL
 *
 * Since: 3.32
 */
gpointer
ide_task_get_task_data (IdeTask *self)
{
  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
  gpointer task_data = NULL;

  g_assert (IDE_IS_TASK (self));

  g_mutex_lock (&priv->mutex);
  if (priv->task_data)
    task_data = priv->task_data->data;
  g_mutex_unlock (&priv->mutex);

  return task_data;
}

static gboolean
ide_task_set_task_data_cb (gpointer data)
{
  IdeTaskData *task_data = data;
  ide_task_data_free (task_data);
  return G_SOURCE_REMOVE;
}

void
(ide_task_set_task_data) (IdeTask        *self,
                          gpointer        task_data,
                          GDestroyNotify  task_data_destroy)
{
  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
  g_autoptr(IdeTaskData) old_task_data = NULL;
  g_autoptr(IdeTaskData) new_task_data = NULL;

  g_return_if_fail (IDE_IS_TASK (self));

  new_task_data = g_slice_new0 (IdeTaskData);
  new_task_data->data = task_data;
  new_task_data->data_destroy = task_data_destroy;

  g_mutex_lock (&priv->mutex);

  if (priv->return_called)
    {
      g_critical ("Cannot set task data after returning value");
      goto unlock;
    }

  old_task_data = g_steal_pointer (&priv->task_data);
  priv->task_data = g_steal_pointer (&new_task_data);

  if (priv->thread_called && old_task_data)
    {
      GSource *source;

      source = g_idle_source_new ();
      g_source_set_name (source, "[ide-task] finalize task data");
      g_source_set_ready_time (source, -1);
      g_source_set_callback (source,
                             ide_task_set_task_data_cb,
                             NULL, NULL);
      g_source_set_priority (source, priv->priority);
      g_source_attach (source, priv->main_context);
      g_source_unref (source);
    }

unlock:
  g_mutex_unlock (&priv->mutex);
}

static gboolean
ide_task_cancel_cb (gpointer user_data)
{
  IdeTask *self = user_data;
  IdeTaskResult *ret;

  g_assert (IDE_IS_TASK (self));

  ret = g_slice_new0 (IdeTaskResult);
  ret->type = IDE_TASK_RESULT_CANCELLED;

  ide_task_return (self, g_steal_pointer (&ret));

  return G_SOURCE_REMOVE;
}

static void
ide_task_cancellable_cancelled_cb (GCancellable  *cancellable,
                                   IdeTaskCancel *cancel)
{
  GSource *source;

  g_assert (G_IS_CANCELLABLE (cancellable));
  g_assert (cancel != NULL);
  g_assert (IDE_IS_TASK (cancel->task));
  g_assert (cancel->main_context != NULL);

  /*
   * This can be called synchronously from g_cancellable_connect(), which
   * could still be holding priv->mutex. So we need to queue the cancellation
   * request back through the main context.
   */

  source = g_idle_source_new ();
  g_source_set_name (source, "[ide-task] cancel task");
  g_source_set_ready_time (source, -1);
  g_source_set_callback (source, ide_task_cancel_cb, g_object_ref (cancel->task), g_object_unref);
  g_source_set_priority (source, cancel->priority);
  g_source_attach (source, cancel->main_context);
  g_source_unref (source);
}

/**
 * ide_task_get_return_on_cancel:
 * @self: a #IdeTask
 *
 * Gets the return_on_cancel value, which means the task will return
 * immediately when the #GCancellable is cancelled.
 *
 * Since: 3.32
 */
gboolean
ide_task_get_return_on_cancel (IdeTask *self)
{
  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
  gboolean ret;

  g_return_val_if_fail (IDE_IS_TASK (self), FALSE);

  g_mutex_lock (&priv->mutex);
  ret = priv->return_on_cancel;
  g_mutex_unlock (&priv->mutex);

  return ret;
}

/**
 * ide_task_set_return_on_cancel:
 * @self: a #IdeTask
 * @return_on_cancel: if the task should return immediately when the
 *   #GCancellable has been cancelled.
 *
 * Setting @return_on_cancel to %TRUE ensures that the task will cancel
 * immediately when #GCancellable::cancelled is emitted by the configured
 * cancellable.
 *
 * Setting this requires that the caller can ensure the configured #GMainContext
 * will outlive the threaded worker so that task state can be freed in a delayed
 * fashion.
 *
 * Since: 3.32
 */
void
ide_task_set_return_on_cancel (IdeTask  *self,
                               gboolean  return_on_cancel)
{
  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
  g_autoptr(GMutexLocker) locker = NULL;

  g_return_if_fail (IDE_IS_TASK (self));

  locker = g_mutex_locker_new (&priv->mutex);

  if (priv->cancellable == NULL)
    return;

  return_on_cancel = !!return_on_cancel;

  if (priv->return_on_cancel != return_on_cancel)
    {
      priv->return_on_cancel = return_on_cancel;

      if (return_on_cancel)
        {
          IdeTaskCancel *cancel;

          /* This creates a reference cycle, but it gets destroyed when the
           * appropriate ide_task_return() API is called.
           */
          cancel = g_slice_new0 (IdeTaskCancel);
          cancel->main_context = g_main_context_ref (priv->main_context);
          cancel->task = g_object_ref (self);
          cancel->priority = priv->priority;

          priv->cancel_handler =
            g_cancellable_connect (priv->cancellable,
                                   G_CALLBACK (ide_task_cancellable_cancelled_cb),
                                   g_steal_pointer (&cancel),
                                   (GDestroyNotify)ide_task_cancel_free);

        }
      else
        {
          if (priv->cancel_handler)
            {
              g_cancellable_disconnect (priv->cancellable, priv->cancel_handler);
              priv->cancel_handler = 0;
            }
        }
    }
}

void
ide_task_report_new_error (gpointer              source_object,
                           GAsyncReadyCallback   callback,
                           gpointer              callback_data,
                           gpointer              source_tag,
                           GQuark                domain,
                           gint                  code,
                           const gchar          *format,
                           ...)
{
  g_autoptr(IdeTask) task = NULL;
  GError *error;
  va_list args;

  va_start (args, format);
  error = g_error_new_valist (domain, code, format, args);
  va_end (args);

  task = ide_task_new (source_object, NULL, callback, callback_data);
  ide_task_set_source_tag (task, source_tag);
  ide_task_return_error (task, g_steal_pointer (&error));
}

/**
 * ide_task_get_name:
 * @self: a #IdeTask
 *
 * Gets the name assigned for the task.
 *
 * Returns: (nullable): a string or %NULL
 *
 * Since: 3.32
 */
const gchar *
ide_task_get_name (IdeTask *self)
{
  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
  const gchar *ret;

  g_return_val_if_fail (IDE_IS_TASK (self), NULL);

  g_mutex_lock (&priv->mutex);
  ret = priv->name;
  g_mutex_unlock (&priv->mutex);

  return ret;
}

/**
 * ide_task_set_name:
 * @self: a #IdeTask
 *
 * Sets a useful name for the task.
 *
 * This string is interned, so it is best to avoid dynamic names as
 * that can result in lots of unnecessary strings being interned for
 * the lifetime of the process.
 *
 * This name may be used in various g_critical() messages which can
 * be useful in troubleshooting.
 *
 * If using #IdeTask from C, a default name is set using the source
 * file name and line number.
 *
 * Since: 3.32
 */
void
ide_task_set_name (IdeTask *self,
                   const gchar *name)
{
  IdeTaskPrivate *priv = ide_task_get_instance_private (self);

  g_return_if_fail (IDE_IS_TASK (self));

  name = g_intern_string (name);

  g_mutex_lock (&priv->mutex);
  priv->name = name;
  g_mutex_unlock (&priv->mutex);

#ifdef ENABLE_TIME_CHART
  g_message ("TASK-BEGIN: %s", name);
#endif
}

/**
 * ide_task_had_error:
 * @self: a #IdeTask
 *
 * Checks to see if the task had an error.
 *
 * Returns: %TRUE if an error has occurred
 *
 * Since: 3.32
 */
gboolean
ide_task_had_error (IdeTask *self)
{
  IdeTaskPrivate *priv = ide_task_get_instance_private (self);
  gboolean ret;

  g_return_val_if_fail (IDE_IS_TASK (self), FALSE);

  g_mutex_lock (&priv->mutex);
  ret = (priv->result != NULL && priv->result->type == IDE_TASK_RESULT_ERROR) ||
        (priv->thread_result != NULL && priv->thread_result->type == IDE_TASK_RESULT_ERROR);
  g_mutex_unlock (&priv->mutex);

  return ret;
}

static gpointer
ide_task_get_user_data (GAsyncResult *result)
{
  IdeTask *self = (IdeTask *)result;
  IdeTaskPrivate *priv = ide_task_get_instance_private (self);

  g_assert (IDE_IS_TASK (self));

  return priv->user_data;
}

static GObject *
ide_task_get_source_object_full (GAsyncResult *result)
{
  IdeTask *self = (IdeTask *)result;
  IdeTaskPrivate *priv = ide_task_get_instance_private (self);

  g_assert (IDE_IS_TASK (self));

  return priv->source_object ? g_object_ref (priv->source_object) : NULL;
}

static gboolean
ide_task_is_tagged (GAsyncResult *result,
                    gpointer      source_tag)
{
  IdeTask *self = (IdeTask *)result;
  IdeTaskPrivate *priv = ide_task_get_instance_private (self);

  g_assert (IDE_IS_TASK (self));

  return source_tag == priv->source_tag;
}

static void
async_result_init_iface (GAsyncResultIface *iface)
{
  iface->get_user_data = ide_task_get_user_data;
  iface->get_source_object = ide_task_get_source_object_full;
  iface->is_tagged = ide_task_is_tagged;
}

void
_ide_dump_tasks (void)
{
  guint i = 0;

  G_LOCK (global_task_list);

  for (const GList *iter = global_task_list.head; iter; iter = iter->next)
    {
      IdeTask *self = iter->data;
      IdeTaskPrivate *priv = ide_task_get_instance_private (self);

      g_printerr ("[%02d]: %s %s\n", i++, priv->name,
                  priv->completed ? "completed" : "");
    }

  G_UNLOCK (global_task_list);
}
