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

/**
 * SECTION: WpPolicy
 */

#define G_LOG_DOMAIN "wp-policy"

#include <pipewire/pipewire.h>

#include "policy.h"
#include "debug.h"
#include "private.h"

/* WpPolicyManager */

struct _WpPolicyManager
{
  GObject parent;
  GList *policies;
  WpObjectManager *endpoints_om;
  WpObjectManager *sessions_om;
};

enum {
  SIGNAL_CHANGED,
  N_SIGNALS,
};

static guint signals[N_SIGNALS];

G_DEFINE_TYPE (WpPolicyManager, wp_policy_manager, G_TYPE_OBJECT)

static void
wp_policy_manager_init (WpPolicyManager *self)
{
  self->endpoints_om = wp_object_manager_new ();
  self->sessions_om = wp_object_manager_new ();
}

static void
wp_policy_manager_finalize (GObject *object)
{
  WpPolicyManager *self = WP_POLICY_MANAGER (object);

  wp_trace_object (self, "destroyed");

  g_clear_object (&self->sessions_om);
  g_clear_object (&self->endpoints_om);
  g_list_free_full (self->policies, g_object_unref);

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

static void
wp_policy_manager_class_init (WpPolicyManagerClass *klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;

  object_class->finalize = wp_policy_manager_finalize;

  signals[SIGNAL_CHANGED] = g_signal_new ("policy-changed",
      G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL,
      G_TYPE_NONE, 0);
}

static void
policy_mgr_endpoint_added (WpObjectManager *om, WpBaseEndpoint *ep,
    WpPolicyManager *self)
{
  GList *l;
  WpPolicy *p;

  for (l = g_list_first (self->policies); l; l = g_list_next (l)) {
    p = WP_POLICY (l->data);

    if (WP_POLICY_GET_CLASS (p)->endpoint_added)
      WP_POLICY_GET_CLASS (p)->endpoint_added (p, ep);
  }
}

static void
policy_mgr_endpoint_removed (WpObjectManager *om, WpBaseEndpoint *ep,
    WpPolicyManager *self)
{
  GList *l;
  WpPolicy *p;

  for (l = g_list_first (self->policies); l; l = g_list_next (l)) {
    p = WP_POLICY (l->data);

    if (WP_POLICY_GET_CLASS (p)->endpoint_removed)
      WP_POLICY_GET_CLASS (p)->endpoint_removed (p, ep);
  }
}

/**
 * wp_policy_manager_get_instance:
 * @core: the #WpCore
 *
 * Returns: (transfer full): the instance of #WpPolicyManager that is
 * registered on the @core
 */
WpPolicyManager *
wp_policy_manager_get_instance (WpCore *core)
{
  WpPolicyManager *mgr;

  g_return_val_if_fail (WP_IS_CORE (core), NULL);

  mgr = wp_registry_find_object (wp_core_get_registry (core),
      (GEqualFunc) WP_IS_POLICY_MANAGER, NULL);
  if (G_UNLIKELY (!mgr)) {
    mgr = g_object_new (WP_TYPE_POLICY_MANAGER, NULL);

    /* install the object manager to listen to added/removed endpoints */
    wp_object_manager_add_interest_1 (mgr->endpoints_om,
        WP_TYPE_BASE_ENDPOINT, NULL);
    g_signal_connect_object (mgr->endpoints_om, "object-added",
        (GCallback) policy_mgr_endpoint_added, mgr, 0);
    g_signal_connect_object (mgr->endpoints_om, "object-removed",
        (GCallback) policy_mgr_endpoint_removed, mgr, 0);
    wp_core_install_object_manager (core, mgr->endpoints_om);

    /* install the object manager to listen to changed sessions */
    wp_object_manager_add_interest_1 (mgr->sessions_om,
        WP_TYPE_IMPL_SESSION, NULL);
    wp_object_manager_request_proxy_features (mgr->sessions_om,
        WP_TYPE_IMPL_SESSION,
        WP_PROXY_FEATURES_STANDARD | WP_PROXY_FEATURE_CONTROLS);
    wp_core_install_object_manager (core, mgr->sessions_om);

    wp_registry_register_object (wp_core_get_registry (core),
        g_object_ref (mgr));
  }

  return mgr;
}

/**
 * wp_policy_manager_get_session:
 * @self: the policy manager
 *
 * Returns: (transfer full) (nullable): the active session
 */
WpSession *
wp_policy_manager_get_session (WpPolicyManager *self)
{
  g_autoptr (WpIterator) it = NULL;
  g_auto (GValue) val = G_VALUE_INIT;

  g_return_val_if_fail (WP_IS_POLICY_MANAGER (self), NULL);

  it = wp_object_manager_iterate (self->sessions_om);
  if (wp_iterator_next (it, &val))
    return g_value_dup_object (&val);
  return NULL;
}

static inline gboolean
media_class_matches (const gchar * media_class, const gchar * lookup)
{
  const gchar *c1 = media_class, *c2 = lookup;

  /* empty lookup matches all classes */
  if (!lookup)
    return TRUE;

  /* compare until we reach the end of the lookup string */
  for (; *c2 != '\0'; c1++, c2++) {
    if (*c1 != *c2)
      return FALSE;
  }

  /* the lookup may not end in a slash, however it must match up
   * to the end of a submedia_class. i.e.:
   * match: media_class: Audio/Source/Virtual
   *        lookup: Audio/Source
   *
   * NO match: media_class: Audio/Source/Virtual
   *           lookup: Audio/Sou
   *
   * if *c1 is not /, also check the previous char, because the lookup
   * may actually end in a slash:
   *
   * match: media_class: Audio/Source/Virtual
   *        lookup: Audio/Source/
   */
  if (!(*c1 == '/' || *c1 == '\0' || *(c1 - 1) == '/'))
    return FALSE;

  return TRUE;
}

static gboolean
list_endpoints_fold_func (const GValue *item, GValue *ret, gpointer data)
{
  GPtrArray *ret_arr = g_value_get_boxed (ret);
  WpBaseEndpoint *ep = g_value_get_object (item);
  if (media_class_matches (wp_base_endpoint_get_media_class (ep),
          (const gchar *) data))
    g_ptr_array_add (ret_arr, g_object_ref (ep));
  return TRUE;
}

/**
 * wp_policy_manager_list_endpoints:
 * @self: the policy manager
 * @media_class: the media class lookup string
 *
 * Returns: (transfer full) (element-type WpBaseEndpoint*): an array with all the
 *   endpoints matching the media class lookup string
 */
GPtrArray *
wp_policy_manager_list_endpoints (WpPolicyManager * self,
    const gchar * media_class)
{
  g_autoptr (WpIterator) it = NULL;
  g_auto (GValue) val = G_VALUE_INIT;
  GPtrArray *ret_arr;

  g_return_val_if_fail (WP_IS_POLICY_MANAGER (self), NULL);

  ret_arr = g_ptr_array_new_with_free_func (g_object_unref);
  g_value_init (&val, G_TYPE_PTR_ARRAY);
  g_value_set_boxed (&val, ret_arr);

  it = wp_object_manager_iterate (self->endpoints_om);
  wp_iterator_fold (it, list_endpoints_fold_func, &val, (gpointer) media_class);

  return ret_arr;
}

/* WpPolicy */

/**
 * WpPolicyClass::endpoint_added:
 * @self: the policy
 * @ep: the endpoint
 *
 * Called when a new endpoint has been added.
 * This is only informative, to be used for internal bookeeping purposes.
 * No action should be taken to do something with this endpoint.
 */

/**
 * WpPolicyClass::endpoint_removed:
 * @self: the policy
 * @ep: the endpoint
 *
 * Called when an endpoint has been removed.
 * This is only informative, to be used for internal bookeeping purposes.
 */

/**
 * WpPolicyClass::find_endpoint:
 * @self: the policy
 * @props: properties of the lookup
 * @stream_id: (out): the relevant stream id of the returned endpoint
 *
 * Called to locate an endpoint with a specific set of properties,
 * which may be used to implement decision making when multiple endpoints
 * can match.
 *
 * The most notorious use case of this function is to locate a target
 * device endpoint in order to link a client one.
 *
 * @props is expected to be a dictionary (a{sv}) GVariant with keys that
 * describe the situation. Some of these keys can be:
 *  * "action" (s): Currently the value can be "link" or "mixer". "link" is
 *          to find a target for linking a client. "mixer" is to find a target
 *          to modify mixer controls.
 *  * "media.role" (s): the role of the media stream, as defined in pipewire
 *  * "media.class" (s): the media class that the returned endpoint is supposed
 *          to have (policy is free to ignore this)
 *  * "target.properties" (a{sv}): the properties of the other endpoint in case
 *          the action is "link"
 *
 * @stream_id is to be set to the stream id of the returned endpoint that
 * the policy wants to be used for this action.
 *
 * Returns: (transfer full) (nullable): the found endpoint, or NULL
 */

typedef struct _WpPolicyPrivate WpPolicyPrivate;
struct _WpPolicyPrivate
{
  guint32 rank;
  GWeakRef core;
};

enum {
  PROP_0,
  PROP_RANK
};

G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (WpPolicy, wp_policy, G_TYPE_OBJECT)

static void
wp_policy_init (WpPolicy *self)
{
  WpPolicyPrivate *priv = wp_policy_get_instance_private (self);

  g_weak_ref_init (&priv->core, NULL);
}

static void
wp_policy_finalize (GObject * object)
{
  WpPolicyPrivate *priv = wp_policy_get_instance_private (WP_POLICY (object));

  g_weak_ref_clear (&priv->core);

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

static void
wp_policy_set_property (GObject * object, guint property_id,
    const GValue * value, GParamSpec * pspec)
{
  WpPolicyPrivate *priv = wp_policy_get_instance_private (WP_POLICY (object));

  switch (property_id) {
  case PROP_RANK:
    priv->rank = g_value_get_uint (value);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
wp_policy_get_property (GObject * object, guint property_id, GValue * value,
    GParamSpec * pspec)
{
  WpPolicyPrivate *priv = wp_policy_get_instance_private (WP_POLICY (object));

  switch (property_id) {
  case PROP_RANK:
    g_value_set_uint (value, priv->rank);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
wp_policy_class_init (WpPolicyClass *klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;

  object_class->finalize = wp_policy_finalize;
  object_class->set_property = wp_policy_set_property;
  object_class->get_property = wp_policy_get_property;

  g_object_class_install_property (object_class, PROP_RANK,
      g_param_spec_uint ("rank", "rank", "The rank of the policy",
          0, G_MAXINT32, WP_POLICY_RANK_UPSTREAM,
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
}

guint32
wp_policy_get_rank (WpPolicy *self)
{
  WpPolicyPrivate *priv;

  g_return_val_if_fail (WP_IS_POLICY (self), 0);

  priv = wp_policy_get_instance_private (self);
  return priv->rank;
}

/**
 * wp_policy_get_core:
 * @self: the policy
 *
 * Returns: (transfer full): the core of the policy
 */

WpCore *
wp_policy_get_core (WpPolicy *self)
{
  WpPolicyPrivate *priv;

  g_return_val_if_fail (WP_IS_POLICY (self), NULL);

  priv = wp_policy_get_instance_private (self);
  return g_weak_ref_get (&priv->core);
}

static gint
compare_ranks (const WpPolicy * a, const WpPolicy * b)
{
  WpPolicyPrivate *a_priv = wp_policy_get_instance_private ((WpPolicy *) a);
  WpPolicyPrivate *b_priv = wp_policy_get_instance_private ((WpPolicy *) b);
  return (gint) b_priv->rank - (gint) a_priv->rank;
}

void
wp_policy_register (WpPolicy *self, WpCore *core)
{
  g_autoptr (WpPolicyManager) mgr = NULL;
  WpPolicyPrivate *priv;

  g_return_if_fail (WP_IS_POLICY (self));
  g_return_if_fail (WP_IS_CORE (core));

  priv = wp_policy_get_instance_private (self);
  g_weak_ref_set (&priv->core, core);

  mgr = wp_policy_manager_get_instance (core);
  mgr->policies = g_list_insert_sorted (mgr->policies, g_object_ref (self),
      (GCompareFunc) compare_ranks);
  g_signal_emit (mgr, signals[SIGNAL_CHANGED], 0);
}

void
wp_policy_unregister (WpPolicy *self)
{
  g_autoptr (WpPolicyManager) mgr = NULL;
  g_autoptr (WpCore) core = NULL;
  WpPolicyPrivate *priv;

  g_return_if_fail (WP_IS_POLICY (self));

  priv = wp_policy_get_instance_private (self);

  core = g_weak_ref_get (&priv->core);
  if (core) {
    mgr = wp_registry_find_object (wp_core_get_registry (core),
        (GEqualFunc) WP_IS_POLICY_MANAGER, NULL);
    if (G_UNLIKELY (!mgr)) {
      g_critical (WP_OBJECT_FORMAT " seems registered, but the policy manager "
          "is absent", WP_OBJECT_ARGS (self));
      return;
    }

    mgr->policies = g_list_remove (mgr->policies, self);
    g_signal_emit (mgr, signals[SIGNAL_CHANGED], 0);
    g_object_unref (self);
  }
}

void
wp_policy_notify_changed (WpPolicy *self)
{
  g_autoptr (WpPolicyManager) mgr = NULL;
  g_autoptr (WpCore) core = NULL;
  WpPolicyPrivate *priv;

  g_return_if_fail (WP_IS_POLICY (self));

  priv = wp_policy_get_instance_private (self);

  core = g_weak_ref_get (&priv->core);
  if (core) {
    mgr = wp_registry_find_object (wp_core_get_registry (core),
        (GEqualFunc) WP_IS_POLICY_MANAGER, NULL);
    if (G_UNLIKELY (!mgr)) {
      g_critical (WP_OBJECT_FORMAT " seems registered, but the policy manager "
          "is absent", WP_OBJECT_ARGS (self));
      return;
    }

    g_signal_emit (mgr, signals[SIGNAL_CHANGED], 0);
  }
}

/**
 * wp_policy_find_endpoint:
 * @core: the #WpCore
 * @props: (transfer floating): properties of the lookup
 * @stream_id: (out): the relevant stream id of the returned endpoint
 *
 * Calls #WpPolicyClass::find_endpoint on all policies, in order, until
 * it finds a suitable endpoint.
 *
 * Returns: (transfer full) (nullable): the found endpoint, or NULL
 */
WpBaseEndpoint *
wp_policy_find_endpoint (WpCore *core, GVariant *props,
    guint32 *stream_id)
{
  g_autoptr (WpPolicyManager) mgr = NULL;
  GList *l;
  WpPolicy *p;
  WpBaseEndpoint * ret;

  g_return_val_if_fail (WP_IS_CORE (core), NULL);
  g_return_val_if_fail (g_variant_is_of_type (props, G_VARIANT_TYPE_VARDICT), NULL);
  g_return_val_if_fail (stream_id != NULL, NULL);

  mgr = wp_registry_find_object (wp_core_get_registry (core),
        (GEqualFunc) WP_IS_POLICY_MANAGER, NULL);
  if (mgr) {
    for (l = g_list_first (mgr->policies); l; l = g_list_next (l)) {
      p = WP_POLICY (l->data);

      if (WP_POLICY_GET_CLASS (p)->find_endpoint &&
          (ret = WP_POLICY_GET_CLASS (p)->find_endpoint (p, props, stream_id)))
        return ret;
    }
  }

  if (g_variant_is_floating (props))
    g_variant_unref (props);

  return NULL;
}