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

#include "event.h"
#include "event-dispatcher.h"
#include "event-hook.h"
#include "log.h"
#include "proxy.h"

#include <spa/utils/defs.h>
#include <spa/utils/list.h>

WP_DEFINE_LOCAL_LOG_TOPIC ("wp-event")

typedef struct _HookData HookData;
struct _HookData
{
  struct spa_list link;
  WpEventHook *hook;
  GPtrArray *dependencies;
};

static inline HookData *
hook_data_new (WpEventHook * hook)
{
  HookData *hook_data = g_new0 (HookData, 1);
  spa_list_init (&hook_data->link);
  hook_data->hook = g_object_ref (hook);
  hook_data->dependencies = g_ptr_array_new ();
  return hook_data;
}

static void
hook_data_free (HookData *self)
{
  g_clear_object (&self->hook);
  g_clear_pointer (&self->dependencies, g_ptr_array_unref);
  g_free (self);
}

struct _WpEvent
{
  grefcount ref;
  GData *datalist;
  struct spa_list hooks;

  /* immutable fields */
  gint priority;
  WpProperties *properties;
  GObject *source;
  GObject *subject;
  GCancellable *cancellable;
  gchar *name;
};

G_DEFINE_BOXED_TYPE (WpEvent, wp_event, wp_event_ref, wp_event_unref)

static gchar *
form_event_name (WpEvent *e)
{
  WpProperties *props = e->properties;
  const gchar *type = wp_properties_get (props, "event.type");
  const gchar *subject_type = wp_properties_get (props, "event.subject.type");
  const gchar *metadata_name = wp_properties_get (props, "metadata.name");
  const gchar *param = wp_properties_get (props, "event.subject.param-id");

  return g_strdup_printf ("<%p>%s%s%s%s%s%s%s", e, (type ? type : ""),
    ((type && subject_type) ? "@" : ""),
    (subject_type ? subject_type : ""),
    ((subject_type && metadata_name) ? "@" : ""),
    (metadata_name ? metadata_name : ""),
    ((param && subject_type) ? "@" : ""),
    (param ? param : "")
    );
}

/*!
 * \brief Creates a new event
 * \ingroup wpevent
 * \param type the type of the event
 * \param priority the priority of the event
 * \param properties (transfer full)(nullable): properties of the event
 * \param source (transfer none): the source of the event
 * \param subject (transfer none)(nullable): the object that the event is about
 * \return (transfer full): the newly constructed event
 */
WpEvent *
wp_event_new (const gchar * type, gint priority, WpProperties * properties,
    GObject * source, GObject * subject)
{
  WpEvent * self = g_new0 (WpEvent, 1);
  g_ref_count_init (&self->ref);
  g_datalist_init (&self->datalist);
  spa_list_init (&self->hooks);

  self->priority = priority;
  self->properties = properties ?
      wp_properties_ensure_unique_owner (properties) :
      wp_properties_new_empty ();

  self->source = source ? g_object_ref (source) : NULL;
  self->subject = subject ? g_object_ref (subject) : NULL;
  self->cancellable = g_cancellable_new ();

  if (self->subject) {
    /* merge properties from subject */
    /* PW properties */
    GParamSpec *pspec = g_object_class_find_property (
        G_OBJECT_GET_CLASS (self->subject), "properties");
    if (pspec && G_PARAM_SPEC_VALUE_TYPE (pspec) == WP_TYPE_PROPERTIES) {
      g_autoptr (WpProperties) subj_props = NULL;
      g_object_get (self->subject, "properties", &subj_props, NULL);
      if (subj_props) {
        wp_properties_update (self->properties, subj_props);
      }
    }

    /* global properties */
    pspec = g_object_class_find_property ( G_OBJECT_GET_CLASS (self->subject),
        "global-properties");
    if (pspec && G_PARAM_SPEC_VALUE_TYPE (pspec) == WP_TYPE_PROPERTIES) {
      g_autoptr (WpProperties) subj_props = NULL;
      g_object_get (self->subject, "global-properties", &subj_props, NULL);
      if (subj_props) {
        wp_properties_update (self->properties, subj_props);
      }
    }
  }

  wp_properties_set (self->properties, "event.type", type);
  self->name = form_event_name (self);

  wp_trace ("event(%s) created", self->name);
  return self;
}

/*!
 * \brief Gets the name of the event
 * \ingroup wpevent
 * \param self the event
 * \return the event name
 */
const gchar *
wp_event_get_name(WpEvent *self)
{
  g_return_val_if_fail(self != NULL, NULL);
  return self->name;
}

static void
wp_event_free (WpEvent * self)
{
  HookData *hook_data;
  spa_list_consume (hook_data, &self->hooks, link) {
    spa_list_remove (&hook_data->link);
    hook_data_free (hook_data);
  }
  g_datalist_clear (&self->datalist);
  g_clear_pointer (&self->properties, wp_properties_unref);
  g_clear_object (&self->source);
  g_clear_object (&self->subject);
  g_cancellable_cancel (self->cancellable);
  g_clear_object (&self->cancellable);
  g_free (self->name);
  g_free (self);
}

WpEvent *
wp_event_ref (WpEvent * self)
{
  g_ref_count_inc (&self->ref);
  return self;
}

void
wp_event_unref (WpEvent * self)
{
  if (g_ref_count_dec (&self->ref))
    wp_event_free (self);
}

/*!
 * \brief Gets the priority of the event
 * \ingroup wpevent
 * \param self the event
 * \return the event priority
 */
gint
wp_event_get_priority (WpEvent * self)
{
  g_return_val_if_fail (self != NULL, 0);
  return self->priority;
}

/*!
 * \brief Gets the properties of the Event
 * \ingroup wpevent
 * \param self the handle
 * \return (transfer full): the properties of the event
 */
WpProperties *
wp_event_get_properties (WpEvent * self)
{
  g_return_val_if_fail(self != NULL, NULL);
  return wp_properties_ref (self->properties);
}

/*!
 * \brief Gets the Source Object of the Event
 * \ingroup wpevent
 * \param self the handle
 * \return (transfer full): the source of the event
 */
GObject *
wp_event_get_source (WpEvent * self)
{
  g_return_val_if_fail(self != NULL, NULL);
  return self->source ? g_object_ref (self->source) : NULL;
}

/*!
 * \brief Gets the Subject Object of the Event
 * \ingroup wpevent
 * \param self the handle
 * \return (transfer full): the subject of the event
 */
GObject *
wp_event_get_subject (WpEvent * self)
{
  g_return_val_if_fail(self != NULL, NULL);
  return self->subject ? g_object_ref (self->subject) : NULL;
}

/*!
 * \brief Returns the internal GCancellable that is used to track whether this
 *    event has been stopped by wp_event_stop_processing()
 * \ingroup wpevent
 * \param self the event
 * \return (transfer none): the cancellable
 */
GCancellable *
wp_event_get_cancellable (WpEvent * self)
{
  return self->cancellable;
}

/*!
 * \brief Stops processing of this event; any further hooks will not be executed
 *   from this moment onwards and the event will be discarded from the stack
 * \ingroup wpevent
 * \param self the event
 */
void
wp_event_stop_processing (WpEvent * self)
{
  g_return_if_fail (self != NULL);
  wp_debug ("stopping event(%s)", self->name);
  g_cancellable_cancel (self->cancellable);
}

static void
destroy_event_data (gpointer data)
{
  g_value_unset ((GValue *) data);
  g_free (data);
}

/*!
 * \brief Stores \a data on the event, associated with the specified \a key
 *
 * This can be used to exchange arbitrary data between hooks that run for
 * this event.
 *
 * \ingroup wpevent
 * \param self the event
 * \param key the key to associate \a data with
 * \param data (transfer none)(nullable): the data element, or \c NULL to
 *   remove any previous data associated with this \a key
 */
void
wp_event_set_data (WpEvent * self, const gchar * key, const GValue * data)
{
  g_return_if_fail (self != NULL);
  g_return_if_fail (key != NULL);
  GValue *data_copy = NULL;

  if (data && G_IS_VALUE (data)) {
    data_copy = g_new0 (GValue, 1);
    g_value_init (data_copy, G_VALUE_TYPE (data));
    g_value_copy (data, data_copy);
  }

  g_datalist_set_data_full (&self->datalist, key, data_copy,
      data_copy ? destroy_event_data : NULL);
}

/*!
 * \brief Gets the data that was previously associated with \a key by
 *    wp_event_set_data()
 * \ingroup wpevent
 * \param self the event
 * \param key the key
 * \return (transfer none)(nullable): the data associated with \a key or \c NULL
 */
const GValue *
wp_event_get_data (WpEvent * self, const gchar * key)
{
  g_return_val_if_fail (self != NULL, NULL);
  g_return_val_if_fail (key != NULL, NULL);

  return g_datalist_get_data (&self->datalist, key);
}

static inline void
record_dependency (struct spa_list *list, const gchar *target,
    const gchar *dependency)
{
  HookData *hook_data;
  spa_list_for_each (hook_data, list, link) {
    if (g_pattern_match_simple (target, wp_event_hook_get_name (hook_data->hook))) {
      g_ptr_array_insert (hook_data->dependencies, -1, (gchar *) dependency);
      break;
    }
  }
}

static inline gboolean
hook_exists_in (const gchar *hook_name, struct spa_list *list)
{
  HookData *hook_data;
  if (!spa_list_is_empty (list)) {
    spa_list_for_each (hook_data, list, link) {
      if (g_pattern_match_simple (hook_name, wp_event_hook_get_name (hook_data->hook))) {
        return TRUE;
      }
    }
  }
  return FALSE;
}

/*!
 * \brief Collects all the hooks registered in the \a dispatcher that run for
 *    this \a event
 * \ingroup wpevent
 * \param event the event
 * \param dispatcher the event dispatcher
 * \return TRUE if at least one hook has been collected,
 *    FALSE if no hooks run for this event or an error occurred
 */
gboolean
wp_event_collect_hooks (WpEvent * event, WpEventDispatcher * dispatcher)
{
  struct spa_list collected, result, remaining;
  g_autoptr (WpIterator) all_hooks = NULL;
  g_auto (GValue) value = G_VALUE_INIT;

  g_return_val_if_fail (event != NULL, FALSE);
  g_return_val_if_fail (WP_IS_EVENT_DISPATCHER (dispatcher), FALSE);

  /* hooks already collected */
  if (!spa_list_is_empty (&event->hooks))
    return TRUE;

  spa_list_init (&collected);
  spa_list_init (&result);
  spa_list_init (&remaining);

  /* collect hooks that run for this event */
  all_hooks = wp_event_dispatcher_new_hooks_iterator (dispatcher);
  while (wp_iterator_next (all_hooks, &value)) {
    WpEventHook *hook = g_value_get_object (&value);

    if (wp_event_hook_runs_for_event (hook, event)) {
      HookData *hook_data = hook_data_new (hook);

      /* record "after" dependencies directly */
      const gchar * const * strv =
          wp_event_hook_get_runs_after_hooks (hook_data->hook);
      while (strv && *strv) {
        g_ptr_array_insert (hook_data->dependencies, -1, (gchar *) *strv);
        strv++;
      }

      spa_list_append (&collected, &hook_data->link);

      wp_debug_boxed (WP_TYPE_EVENT, event, "added "WP_OBJECT_FORMAT"(%s)",
          WP_OBJECT_ARGS (hook), wp_event_hook_get_name (hook));
    }

    g_value_unset (&value);
  }

  if (!spa_list_is_empty (&collected)) {
    HookData *hook_data;

    /* convert "before" dependencies into "after" dependencies */
    spa_list_for_each (hook_data, &collected, link) {
      const gchar * const * strv =
          wp_event_hook_get_runs_before_hooks (hook_data->hook);
      while (strv && *strv) {
        /* record hook_data->hook as a dependency of the *strv hook */
        record_dependency (&collected, *strv,
            wp_event_hook_get_name (hook_data->hook));
        strv++;
      }
    }

    /* sort */
    while (!spa_list_is_empty (&collected)) {
      gboolean made_progress = FALSE;

      /* examine each hook to see if its dependencies are satisfied in the
         result list; if yes, then append it to the result too */
      spa_list_consume (hook_data, &collected, link) {
        guint deps_satisfied = 0;

        spa_list_remove (&hook_data->link);

        wp_trace_boxed (WP_TYPE_EVENT, event,
              "examining: %s", wp_event_hook_get_name (hook_data->hook));

        for (guint i = 0; i < hook_data->dependencies->len; i++) {
          const gchar *dep = g_ptr_array_index (hook_data->dependencies, i);
          /* if the dependency is already in the sorted result list or if
             it doesn't exist at all, we consider it satisfied */
          if (hook_exists_in (dep, &result) ||
              !(hook_exists_in (dep, &collected) ||
                hook_exists_in (dep, &remaining))) {
            deps_satisfied++;
          }

          wp_trace_boxed (WP_TYPE_EVENT, event, "depends: %s, satisfied: %u/%u",
              dep, deps_satisfied, hook_data->dependencies->len);
        }

        if (deps_satisfied == hook_data->dependencies->len) {
          wp_trace_boxed (WP_TYPE_EVENT, event,
              "sorted: "WP_OBJECT_FORMAT"(%s)",
              WP_OBJECT_ARGS (hook_data->hook),
              wp_event_hook_get_name (hook_data->hook));

          spa_list_append (&result, &hook_data->link);
          made_progress = TRUE;
        } else {
          spa_list_append (&remaining, &hook_data->link);
        }
      }

      if (made_progress) {
        /* run again with the remaining hooks */
        spa_list_insert_list (&collected, &remaining);
        spa_list_init (&remaining);
      }
      else if (!spa_list_is_empty (&remaining)) {
        /* if we did not make any progress towards growing the result list,
           it means the dependencies cannot be satisfied because of circles */
        wp_critical_boxed (WP_TYPE_EVENT, event, "detected circular "
            "dependencies in the collected hooks!");

        /* clean up */
        spa_list_consume (hook_data, &result, link) {
          spa_list_remove (&hook_data->link);
          hook_data_free (hook_data);
        }
        spa_list_consume (hook_data, &remaining, link) {
          spa_list_remove (&hook_data->link);
          hook_data_free (hook_data);
        }

        return FALSE;
      }
    }
  }

  spa_list_insert_list (&event->hooks, &result);
  return !spa_list_is_empty (&event->hooks);
}

struct event_hooks_iterator_data
{
  WpEvent *event;
  HookData *cur;
};

static void
event_hooks_iterator_reset (WpIterator *it)
{
  struct event_hooks_iterator_data *it_data = wp_iterator_get_user_data (it);
  struct spa_list *list = &it_data->event->hooks;

  if (!spa_list_is_empty (list))
    it_data->cur = spa_list_first (&it_data->event->hooks, HookData, link);
}

static gboolean
event_hooks_iterator_next (WpIterator *it, GValue *item)
{
  struct event_hooks_iterator_data *it_data = wp_iterator_get_user_data (it);
  struct spa_list *list = &it_data->event->hooks;

  if (!spa_list_is_empty (list) &&
      !spa_list_is_end (it_data->cur, list, link)) {
    g_value_init (item, WP_TYPE_EVENT_HOOK);
    g_value_set_object (item, it_data->cur->hook);
    it_data->cur = spa_list_next (it_data->cur, link);
    return TRUE;
  }
  return FALSE;
}

static gboolean
event_hooks_iterator_fold (WpIterator *it, WpIteratorFoldFunc func, GValue *ret,
    gpointer data)
{
  struct event_hooks_iterator_data *it_data = wp_iterator_get_user_data (it);
  struct spa_list *list = &it_data->event->hooks;
  HookData *hook_data;

  if (!spa_list_is_empty (list)) {
    spa_list_for_each (hook_data, list, link) {
      g_auto (GValue) item = G_VALUE_INIT;
      g_value_init (&item, WP_TYPE_EVENT_HOOK);
      g_value_set_object (&item, hook_data->hook);
      if (!func (&item, ret, data))
          return FALSE;
    }
  }
  return TRUE;
}

static void
event_hooks_iterator_finalize (WpIterator *it)
{
  struct event_hooks_iterator_data *it_data = wp_iterator_get_user_data (it);
  wp_event_unref (it_data->event);
}

static const WpIteratorMethods event_hooks_iterator_methods = {
  .version = WP_ITERATOR_METHODS_VERSION,
  .reset = event_hooks_iterator_reset,
  .next = event_hooks_iterator_next,
  .fold = event_hooks_iterator_fold,
  .finalize = event_hooks_iterator_finalize,
};

/*!
 * \brief Returns an iterator that iterates over all the hooks that were
 *    collected by wp_event_collect_hooks()
 * \ingroup wpevent
 * \param event the event
 * \return (transfer full): the new iterator
 */
WpIterator *
wp_event_new_hooks_iterator (WpEvent * event)
{
  WpIterator *it = NULL;
  struct event_hooks_iterator_data *it_data;

  g_return_val_if_fail (event != NULL, NULL);

  it = wp_iterator_new (&event_hooks_iterator_methods,
      sizeof (struct event_hooks_iterator_data));
  it_data = wp_iterator_get_user_data (it);
  it_data->event = wp_event_ref (event);
  event_hooks_iterator_reset (it);
  return it;
}
