/* WirePlumber
 *
 * Copyright © 2022 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "event-hook.h"
#include "event-dispatcher.h"
#include "transition.h"
#include "log.h"
#include "wpenums.h"

WP_DEFINE_LOCAL_LOG_TOPIC ("wp-event-hook")

typedef struct _WpEventHookPrivate WpEventHookPrivate;
struct _WpEventHookPrivate
{
  GWeakRef dispatcher;
  gchar *name;
  gchar **before;
  gchar **after;
};

enum {
  PROP_0,
  PROP_NAME,
  PROP_RUNS_BEFORE_HOOKS,
  PROP_RUNS_AFTER_HOOKS,
  PROP_DISPATCHER,
};

G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (WpEventHook, wp_event_hook, G_TYPE_OBJECT)

static void
wp_event_hook_init (WpEventHook * self)
{
  WpEventHookPrivate *priv = wp_event_hook_get_instance_private (self);
  g_weak_ref_init (&priv->dispatcher, NULL);
}

static void
wp_event_hook_finalize (GObject * object)
{
  WpEventHook *self = WP_EVENT_HOOK (object);
  WpEventHookPrivate *priv = wp_event_hook_get_instance_private (self);

  g_weak_ref_clear (&priv->dispatcher);
  g_strfreev (priv->before);
  g_strfreev (priv->after);
  g_free (priv->name);

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

static void
wp_event_hook_set_property (GObject * object, guint property_id,
    const GValue * value, GParamSpec * pspec)
{
  WpEventHook *self = WP_EVENT_HOOK (object);
  WpEventHookPrivate *priv = wp_event_hook_get_instance_private (self);

  switch (property_id) {
  case PROP_NAME:
    priv->name = g_value_dup_string (value);
    break;
  case PROP_RUNS_BEFORE_HOOKS:
    priv->before = g_value_dup_boxed (value);
    break;
  case PROP_RUNS_AFTER_HOOKS:
    priv->after = g_value_dup_boxed (value);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
wp_event_hook_get_property (GObject * object, guint property_id, GValue * value,
    GParamSpec * pspec)
{
  WpEventHook *self = WP_EVENT_HOOK (object);
  WpEventHookPrivate *priv = wp_event_hook_get_instance_private (self);

  switch (property_id) {
  case PROP_NAME:
    g_value_set_string (value, priv->name);
    break;
  case PROP_RUNS_BEFORE_HOOKS:
    g_value_set_boxed (value, priv->before);
    break;
  case PROP_RUNS_AFTER_HOOKS:
    g_value_set_boxed (value, priv->after);
    break;
  case PROP_DISPATCHER:
    g_value_take_object (value, wp_event_hook_get_dispatcher (self));
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
wp_event_hook_class_init (WpEventHookClass * klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;

  object_class->finalize = wp_event_hook_finalize;
  object_class->set_property = wp_event_hook_set_property;
  object_class->get_property = wp_event_hook_get_property;

  g_object_class_install_property (object_class, PROP_NAME,
      g_param_spec_string ("name", "name", "The hook name", "",
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_RUNS_BEFORE_HOOKS,
      g_param_spec_boxed ("runs-before-hooks", "runs-before-hooks",
          "runs-before-hooks", G_TYPE_STRV,
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_RUNS_AFTER_HOOKS,
      g_param_spec_boxed ("runs-after-hooks", "runs-after-hooks",
          "runs-after-hooks", G_TYPE_STRV,
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_DISPATCHER,
      g_param_spec_object ("dispatcher", "dispatcher",
          "The associated event dispatcher", WP_TYPE_EVENT_DISPATCHER,
          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
}

/*!
 * \brief Returns the name of the hook
 *
 * \ingroup wpeventhook
 * \param self the event hook
 * \return the event hook name
 */
const gchar *
wp_event_hook_get_name (WpEventHook * self)
{
  g_return_val_if_fail (WP_IS_EVENT_HOOK (self), 0);
  WpEventHookPrivate *priv = wp_event_hook_get_instance_private (self);
  return priv->name;
}

/*!
 * \brief Returns the names of the hooks that should run after this hook,
 *    or in other words, this hook should run before them
 *
 * \ingroup wpeventhook
 * \param self the event hook
 * \return (array zero-terminated=1)(element-type utf8)(transfer none):
 *    a NULL-terminated array of hook names
 */
const gchar * const *
wp_event_hook_get_runs_before_hooks (WpEventHook * self)
{
  g_return_val_if_fail (WP_IS_EVENT_HOOK (self), NULL);
  WpEventHookPrivate *priv = wp_event_hook_get_instance_private (self);
  return (const gchar * const *) priv->before;
}

/*!
 * \brief Returns the names of the hooks that should run before this hook,
 *    or in other words, this hook should run after them
 *
 * \ingroup wpeventhook
 * \param self the event hook
 * \return (array zero-terminated=1)(element-type utf8)(transfer none):
 *    a NULL-terminated array of hook names
 */
const gchar * const *
wp_event_hook_get_runs_after_hooks (WpEventHook * self)
{
  g_return_val_if_fail (WP_IS_EVENT_HOOK (self), NULL);
  WpEventHookPrivate *priv = wp_event_hook_get_instance_private (self);
  return (const gchar * const *) priv->after;
}

/*!
 * \brief Returns the associated event dispatcher
 *
 * \private
 * \ingroup wpeventhook
 * \param self the event hook
 * \return (transfer full)(nullable): the event dispatcher on which this hook
 *   is registered, or NULL if the hook is not registered
 */
WpEventDispatcher *
wp_event_hook_get_dispatcher (WpEventHook * self)
{
  g_return_val_if_fail (WP_IS_EVENT_HOOK (self), NULL);
  WpEventHookPrivate *priv = wp_event_hook_get_instance_private (self);
  return g_weak_ref_get (&priv->dispatcher);
}

/*!
 * \brief Sets the associated event dispatcher
 *
 * \private
 * \ingroup wpeventhook
 * \param self the event hook
 * \param dispatcher (transfer none): the event dispatcher on which
 *  this hook is registered
 */
void
wp_event_hook_set_dispatcher (WpEventHook * self, WpEventDispatcher * dispatcher)
{
  WpEventHookPrivate *priv = wp_event_hook_get_instance_private (self);
  wp_trace_object (dispatcher, "hook (%s) registered", priv->name);
  g_weak_ref_set (&priv->dispatcher, dispatcher);
}

/*!
 * \brief Checks if the hook should be executed for a given event
 *
 * \ingroup wpeventhook
 * \param self the event hook
 * \param event the event
 * \return TRUE if the hook should be executed for the given event,
 *    FALSE otherwise
 */
gboolean
wp_event_hook_runs_for_event (WpEventHook * self, WpEvent * event)
{
  g_return_val_if_fail (WP_IS_EVENT_HOOK (self), FALSE);
  g_return_val_if_fail (WP_EVENT_HOOK_GET_CLASS (self)->runs_for_event, FALSE);
  return WP_EVENT_HOOK_GET_CLASS (self)->runs_for_event (self, event);
}

/*!
 * \brief Runs the hook on the given event
 *
 * \ingroup wpeventhook
 * \param self the event hook
 * \param event the event that triggered the hook
 * \param cancellable (nullable): a GCancellable to cancel the async operation
 * \param callback (scope async)(closure callback_data): a callback to fire after execution of the hook
 *    has completed
 * \param callback_data data for the callback
 */
void
wp_event_hook_run (WpEventHook * self,
    WpEvent * event, GCancellable * cancellable,
    GAsyncReadyCallback callback, gpointer callback_data)
{
  g_return_if_fail (WP_IS_EVENT_HOOK (self));
  g_return_if_fail (WP_EVENT_HOOK_GET_CLASS (self)->run);
  WP_EVENT_HOOK_GET_CLASS (self)->run (self, event, cancellable, callback,
      callback_data);
}

/*!
 * \brief Gets all the matching event types for this hook if any.
 *
 * \ingroup wpeventhook
 * \param self the event hook
 * \returns (element-type gchar*) (transfer full) (nullable): the matching
 * event types for this hook if any.
 * \since 0.5.13
 */
GPtrArray *
wp_event_hook_get_matching_event_types (WpEventHook * self)
{
  g_return_val_if_fail (WP_IS_EVENT_HOOK (self), NULL);
  g_return_val_if_fail (
      WP_EVENT_HOOK_GET_CLASS (self)->get_matching_event_types, NULL);
  return WP_EVENT_HOOK_GET_CLASS (self)->get_matching_event_types (self);
}

/*!
 * \brief Finishes the async operation that was started by wp_event_hook_run()
 *
 * \ingroup wpeventhook
 * \param self the event hook
 * \param res the async operation result
 * \param error (out) (optional): the error of the operation, if any
 * \returns FALSE if there was an error, TRUE otherwise
 */
gboolean
wp_event_hook_finish (WpEventHook * self, GAsyncResult * res, GError ** error)
{
  g_return_val_if_fail (WP_IS_EVENT_HOOK (self), FALSE);
  g_return_val_if_fail (WP_EVENT_HOOK_GET_CLASS (self)->finish, FALSE);
  return WP_EVENT_HOOK_GET_CLASS (self)->finish (self, res, error);
}



/*!
 * \struct WpInterestEventHook
 *
 * An event hook that declares interest in specific events. This subclass
 * implements the WpEventHook.runs_for_event() vmethod and returns TRUE from
 * that method if the given event has properties that match one of the declared
 * interests.
 */

typedef struct _WpInterestEventHookPrivate WpInterestEventHookPrivate;
struct _WpInterestEventHookPrivate
{
  /* element-type: WpObjectInterest* */
  GPtrArray *interests;
};

G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (WpInterestEventHook,
                                     wp_interest_event_hook, WP_TYPE_EVENT_HOOK)

static void
wp_interest_event_hook_init (WpInterestEventHook * self)
{
  WpInterestEventHookPrivate *priv =
      wp_interest_event_hook_get_instance_private (self);
  priv->interests = g_ptr_array_new_with_free_func (
      (GDestroyNotify) wp_object_interest_unref);
}

static void
wp_interest_event_hook_finalize (GObject * object)
{
  WpInterestEventHook *self = WP_INTEREST_EVENT_HOOK (object);
  WpInterestEventHookPrivate *priv =
      wp_interest_event_hook_get_instance_private (self);

  g_clear_pointer (&priv->interests, g_ptr_array_unref);

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

static gboolean
wp_interest_event_hook_runs_for_event (WpEventHook * hook, WpEvent * event)
{
  WpInterestEventHook *self = WP_INTEREST_EVENT_HOOK (hook);
  WpInterestEventHookPrivate *priv =
      wp_interest_event_hook_get_instance_private (self);
  g_autoptr (WpProperties) properties = wp_event_get_properties (event);
  g_autoptr (GObject) subject = wp_event_get_subject (event);
  guint i;
  WpObjectInterest *interest = NULL;

  for (i = 0; i < priv->interests->len; i++) {
    interest = g_ptr_array_index (priv->interests, i);
    if (wp_object_interest_matches_full (interest,
            WP_INTEREST_MATCH_FLAGS_NONE,
            WP_TYPE_EVENT, subject, properties, properties) == WP_INTEREST_MATCH_ALL)
        return TRUE;
  }
  return FALSE;
}

static void
add_unique (GPtrArray *array, const gchar * lookup)
{
  for (guint i = 0; i < array->len; i++)
    if (g_str_equal (g_ptr_array_index (array, i), lookup))
      return;
  g_ptr_array_add (array, g_strdup (lookup));
}

static GPtrArray *
wp_interest_event_hook_get_matching_event_types (WpEventHook * hook)
{
  WpInterestEventHook *self = WP_INTEREST_EVENT_HOOK (hook);
  WpInterestEventHookPrivate *priv =
      wp_interest_event_hook_get_instance_private (self);
  GPtrArray *res = g_ptr_array_new_with_free_func (g_free);
  guint i;

  for (i = 0; i < priv->interests->len; i++) {
    WpObjectInterest *interest = g_ptr_array_index (priv->interests, i);
    if (wp_object_interest_matches_full (interest, WP_INTEREST_MATCH_FLAGS_NONE,
        WP_TYPE_EVENT, NULL, NULL, NULL) & WP_INTEREST_MATCH_GTYPE) {
      g_autoptr (GPtrArray) values =
          wp_object_interest_find_defined_constraint_values (interest,
          WP_CONSTRAINT_TYPE_NONE, "event.type");
      if (!values || values->len == 0) {
        /* We always consider the hook undefined if it has at least one interest
         * without a defined 'event.type' constraint */
        return NULL;
      } else {
        for (guint j = 0; j < values->len; j++) {
          GVariant *v = g_ptr_array_index (values, j);
          if (g_variant_is_of_type (v, G_VARIANT_TYPE_STRING)) {
            const gchar *v_str = g_variant_get_string (v, NULL);
            add_unique (res, v_str);
          }
        }
      }
    }
  }

  return res;
}

static void
wp_interest_event_hook_class_init (WpInterestEventHookClass * klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;
  WpEventHookClass *hook_class = (WpEventHookClass *) klass;

  object_class->finalize = wp_interest_event_hook_finalize;
  hook_class->runs_for_event = wp_interest_event_hook_runs_for_event;
  hook_class->get_matching_event_types =
      wp_interest_event_hook_get_matching_event_types;
}

/*!
 * \brief Equivalent to:
 * \code
 * WpObjectInterest *i = wp_object_interest_new (WP_TYPE_EVENT, ...);
 * wp_interest_event_hook_add_interest_full (self, i);
 * \endcode
 *
 * The constraints specified in the variable arguments must follow the rules
 * documented in wp_object_interest_new().
 *
 * \ingroup wpeventhook
 * \param self the event hook
 * \param ... a list of constraints, terminated by NULL
 */
void
wp_interest_event_hook_add_interest (WpInterestEventHook * self, ...)
{
  WpObjectInterest *interest;
  va_list args;

  g_return_if_fail (WP_IS_INTEREST_EVENT_HOOK (self));

  va_start (args, self);
  interest = wp_object_interest_new_valist (WP_TYPE_EVENT, &args);
  wp_interest_event_hook_add_interest_full (self, interest);
  va_end (args);
}

/*!
 * \brief Declares interest on events. The interest must be constructed to
 * match WP_TYPE_EVENT objects and it is going to be matched against
 * the WpEvent's properties.
 *
 * \param self the event hook
 * \param interest (transfer full): the event interest
 */
void
wp_interest_event_hook_add_interest_full (WpInterestEventHook * self,
    WpObjectInterest * interest)
{
  g_autoptr (GError) error = NULL;

  g_return_if_fail (WP_IS_INTEREST_EVENT_HOOK (self));

  if (G_UNLIKELY (!wp_object_interest_validate (interest, &error))) {
    wp_critical_object (self, "interest validation failed: %s",
        error->message);
    wp_object_interest_unref (interest);
    return;
  }

  WpInterestEventHookPrivate *priv =
      wp_interest_event_hook_get_instance_private (self);
  g_ptr_array_add (priv->interests, interest);
}



/*!
 * \struct WpSimpleEventHook
 *
 * An event hook that runs a GClosure, synchronously.
 */

struct _WpSimpleEventHook
{
  WpInterestEventHook parent;
  GClosure *closure;
};

enum {
  PROP_SIMPLE_0,
  PROP_SIMPLE_CLOSURE,
};

G_DEFINE_TYPE (WpSimpleEventHook, wp_simple_event_hook,
               WP_TYPE_INTEREST_EVENT_HOOK)

static void
wp_simple_event_hook_init (WpSimpleEventHook * self)
{
}

static void
wp_simple_event_hook_finalize (GObject * object)
{
  WpSimpleEventHook *self = WP_SIMPLE_EVENT_HOOK (object);

  g_clear_pointer (&self->closure, g_closure_unref);

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

static void
wp_simple_event_hook_set_property (GObject * object, guint property_id,
    const GValue * value, GParamSpec * pspec)
{
  WpSimpleEventHook *self = WP_SIMPLE_EVENT_HOOK (object);

  switch (property_id) {
  case PROP_SIMPLE_CLOSURE:
    self->closure = g_value_dup_boxed (value);
    g_closure_sink (self->closure);
    if (G_CLOSURE_NEEDS_MARSHAL (self->closure))
      g_closure_set_marshal (self->closure, g_cclosure_marshal_generic);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
wp_simple_event_hook_run (WpEventHook * hook,
    WpEvent * event, GCancellable * cancellable,
    GAsyncReadyCallback callback, gpointer callback_data)
{
  WpSimpleEventHook *self = WP_SIMPLE_EVENT_HOOK (hook);

  GValue values[2] = { G_VALUE_INIT };
  g_value_init (&values[0], WP_TYPE_EVENT);
  g_value_set_boxed (&values[0], event);
  g_closure_invoke (self->closure, NULL, 1, values, NULL);
  g_value_unset (&values[0]);

  callback ((GObject *) self, NULL, callback_data);
}

static gboolean
wp_simple_event_hook_finish (WpEventHook * hook, GAsyncResult * res,
    GError ** error)
{
  return TRUE;
}

static void
wp_simple_event_hook_class_init (WpSimpleEventHookClass * klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;
  WpEventHookClass *hook_class = (WpEventHookClass *) klass;

  object_class->finalize = wp_simple_event_hook_finalize;
  object_class->set_property = wp_simple_event_hook_set_property;
  hook_class->run = wp_simple_event_hook_run;
  hook_class->finish = wp_simple_event_hook_finish;

  g_object_class_install_property (object_class, PROP_SIMPLE_CLOSURE,
      g_param_spec_boxed ("closure", "closure", "The closure", G_TYPE_CLOSURE,
          G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
}

/*!
 * \brief Constructs a new simple event hook
 *
 * \param name the name of the hook
 * \param before (array zero-terminated=1)(element-type utf8)(transfer none)(nullable):
 *    an array of hook names that should run after this hook
 * \param after (array zero-terminated=1)(element-type utf8)(transfer none)(nullable):
 *    an array of hook names that should run before this hook
 * \param closure the closure to invoke when the hook is executed; the closure
 *   should accept two parameters: the event dispatcher and the event, returning
 *   nothing
 * \return a new simple event hook
 */
WpEventHook *
wp_simple_event_hook_new (const gchar *name,
    const gchar * before[], const gchar * after[], GClosure * closure)
{
  g_return_val_if_fail (closure != NULL, NULL);

  return g_object_new (WP_TYPE_SIMPLE_EVENT_HOOK,
      "name", name,
      "runs-before-hooks", before,
      "runs-after-hooks", after,
      "closure", closure,
      NULL);
}


/* WpAsyncEventHookTransition */

#define WP_TYPE_ASYNC_EVENT_HOOK_TRANSITION \
    (wp_async_event_hook_transition_get_type ())
G_DECLARE_FINAL_TYPE (WpAsyncEventHookTransition,
                      wp_async_event_hook_transition,
                      WP, ASYNC_EVENT_HOOK_TRANSITION, WpTransition)


/*!
 * \struct WpAsyncEventHook
 *
 * An event hook that runs a WpTransition, implemented with closures.
 */

struct _WpAsyncEventHook
{
  WpInterestEventHook parent;
  GClosure *get_next_step;
  GClosure *execute_step;
};

enum {
  PROP_ASYNC_0,
  PROP_ASYNC_GET_NEXT_STEP,
  PROP_ASYNC_EXECUTE_STEP,
};

G_DEFINE_TYPE (WpAsyncEventHook, wp_async_event_hook,
               WP_TYPE_INTEREST_EVENT_HOOK)

static void
wp_async_event_hook_init (WpAsyncEventHook * self)
{
}

static void
wp_async_event_hook_finalize (GObject * object)
{
  WpAsyncEventHook *self = WP_ASYNC_EVENT_HOOK (object);

  g_clear_pointer (&self->get_next_step, g_closure_unref);
  g_clear_pointer (&self->execute_step, g_closure_unref);

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

static void
wp_async_event_hook_set_property (GObject * object, guint property_id,
    const GValue * value, GParamSpec * pspec)
{
  WpAsyncEventHook *self = WP_ASYNC_EVENT_HOOK (object);

  switch (property_id) {
  case PROP_ASYNC_GET_NEXT_STEP:
    self->get_next_step = g_value_dup_boxed (value);
    g_closure_sink (self->get_next_step);
    if (G_CLOSURE_NEEDS_MARSHAL (self->get_next_step))
      g_closure_set_marshal (self->get_next_step, g_cclosure_marshal_generic);
    break;
  case PROP_ASYNC_EXECUTE_STEP:
    self->execute_step = g_value_dup_boxed (value);
    g_closure_sink (self->execute_step);
    if (G_CLOSURE_NEEDS_MARSHAL (self->execute_step))
      g_closure_set_marshal (self->execute_step, g_cclosure_marshal_VOID__UINT);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
wp_async_event_hook_run (WpEventHook * hook,
    WpEvent * event, GCancellable * cancellable,
    GAsyncReadyCallback callback, gpointer callback_data)
{
  WpTransition *transition = wp_transition_new (
      WP_TYPE_ASYNC_EVENT_HOOK_TRANSITION,
      hook, cancellable, callback, callback_data);
  wp_transition_set_data (transition, wp_event_ref (event),
      (GDestroyNotify) wp_event_unref);
  wp_transition_set_source_tag (transition, wp_async_event_hook_run);
  wp_transition_advance (transition);
}

static gboolean
wp_async_event_hook_finish (WpEventHook * hook, GAsyncResult * res,
    GError ** error)
{
  g_return_val_if_fail (g_async_result_is_tagged (res, wp_async_event_hook_run),
      FALSE);
  return wp_transition_finish (res, error);
}

static void
wp_async_event_hook_class_init (WpAsyncEventHookClass * klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;
  WpEventHookClass *hook_class = (WpEventHookClass *) klass;

  object_class->finalize = wp_async_event_hook_finalize;
  object_class->set_property = wp_async_event_hook_set_property;
  hook_class->run = wp_async_event_hook_run;
  hook_class->finish = wp_async_event_hook_finish;

  g_object_class_install_property (object_class, PROP_ASYNC_GET_NEXT_STEP,
      g_param_spec_boxed ("get-next-step", "get-next-step",
          "The get-next-step closure", G_TYPE_CLOSURE,
          G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_ASYNC_EXECUTE_STEP,
      g_param_spec_boxed ("execute-step", "execute-step",
          "The execute-step closure", G_TYPE_CLOSURE,
          G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
}

/*!
 * \brief Constructs a new async event hook
 *
 * \param name the name of the hook
 * \param before (array zero-terminated=1)(element-type utf8)(transfer none)(nullable):
 *    an array of hook names that should run after this hook
 * \param after (array zero-terminated=1)(element-type utf8)(transfer none)(nullable):
 *    an array of hook names that should run before this hook
 * \param get_next_step the closure to invoke to get the next step
 * \param execute_step the closure to invoke to execute the step
 * \return a new async event hook
 */
WpEventHook *
wp_async_event_hook_new (const gchar *name,
    const gchar * before[], const gchar * after[],
    GClosure * get_next_step, GClosure * execute_step)
{
  g_return_val_if_fail (get_next_step != NULL, NULL);
  g_return_val_if_fail (execute_step != NULL, NULL);

  return g_object_new (WP_TYPE_ASYNC_EVENT_HOOK,
      "name", name,
      "runs-before-hooks", before,
      "runs-after-hooks", after,
      "get-next-step", get_next_step,
      "execute-step", execute_step,
      NULL);
}


/* WpAsyncEventHookTransition - implementation */

struct _WpAsyncEventHookTransition
{
  WpTransition parent;
};

G_DEFINE_TYPE (WpAsyncEventHookTransition,
               wp_async_event_hook_transition,
               WP_TYPE_TRANSITION)

static void
wp_async_event_hook_transition_init (
    WpAsyncEventHookTransition * transition)
{
}

static guint
wp_async_event_hook_transition_get_next_step (
    WpTransition * transition, guint step)
{
  WpAsyncEventHook *hook =
      WP_ASYNC_EVENT_HOOK (wp_transition_get_source_object (transition));
  guint next_step = WP_TRANSITION_STEP_ERROR;
  GValue ret = G_VALUE_INIT;
  GValue values[2] = { G_VALUE_INIT, G_VALUE_INIT };

  g_value_init (&ret, G_TYPE_UINT);
  g_value_init (&values[0], G_TYPE_OBJECT);
  g_value_init (&values[1], G_TYPE_UINT);
  g_value_set_object (&values[0], transition);
  g_value_set_uint (&values[1], step);
  g_closure_invoke (hook->get_next_step, &ret, 2, values, NULL);
  g_value_unset (&values[0]);
  g_value_unset (&values[1]);
  next_step = g_value_get_uint (&ret);
  g_value_unset (&ret);
  return next_step;
}

static void
wp_async_event_hook_transition_execute_step (
    WpTransition * transition, guint step)
{
  WpAsyncEventHook *hook =
      WP_ASYNC_EVENT_HOOK (wp_transition_get_source_object (transition));
  GValue values[2] = { G_VALUE_INIT, G_VALUE_INIT };

  g_value_init (&values[0], G_TYPE_OBJECT);
  g_value_init (&values[1], G_TYPE_UINT);
  g_value_set_object (&values[0], transition);
  g_value_set_uint (&values[1], step);
  g_closure_invoke (hook->execute_step, NULL, 2, values, NULL);
  g_value_unset (&values[0]);
  g_value_unset (&values[1]);
}

static void
wp_async_event_hook_transition_class_init (
    WpAsyncEventHookTransitionClass * klass)
{
  WpTransitionClass *transition_class = (WpTransitionClass *) klass;

  transition_class->get_next_step =
      wp_async_event_hook_transition_get_next_step;
  transition_class->execute_step =
      wp_async_event_hook_transition_execute_step;
}
