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

#include "session-item.h"
#include "core.h"
#include "log.h"
#include "error.h"
#include <spa/utils/defs.h>

WP_DEFINE_LOCAL_LOG_TOPIC ("wp-si")

/*! \defgroup wpsessionitem WpSessionItem */
/*!
 * \struct WpSessionItem
 *
 * Session items are high level objects that wrap underlying PipeWire objects
 * and manage them. For example, a session item may be managing a node, taking
 * responsibility for configuring the PortConfig and Format parameters of the
 * node. Or another may be managing links between two nodes.
 *
 * All the implementations are provided by modules and instantiated via the
 * WpSiFactory class.
 *
 * \gproperties
 *
 * \gproperty{id, guint, G_PARAM_READABLE,
 *   The session item unique id}
 *
 * \gproperty{properties, WpProperties *, G_PARAM_READABLE,
 *   The session item properties}
 */

enum {
  STEP_ACTIVATE = WP_TRANSITION_STEP_CUSTOM_START,
  STEP_EXPORT,
};

typedef struct _WpSessionItemPrivate WpSessionItemPrivate;
struct _WpSessionItemPrivate
{
  WpProperties *properties;
};

enum {
  PROP_0,
  PROP_PROPERTIES,
};

G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (WpSessionItem, wp_session_item,
    WP_TYPE_OBJECT)

static void
wp_session_item_init (WpSessionItem * self)
{
  WpSessionItemPrivate *priv = wp_session_item_get_instance_private (self);

  priv->properties = NULL;
}

static void
wp_session_item_default_reset (WpSessionItem * self)
{
  WpSessionItemPrivate *priv = wp_session_item_get_instance_private (self);

  g_clear_pointer (&priv->properties, wp_properties_unref);
}

static void
wp_session_item_dispose (GObject * object)
{
  WpSessionItem * self = WP_SESSION_ITEM (object);

  wp_trace_object (self, "dispose");

  wp_session_item_reset (self);

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

static void
wp_session_item_get_gobject_property (GObject * object, guint property_id,
    GValue * value, GParamSpec * pspec)
{
  WpSessionItem *self = WP_SESSION_ITEM (object);

  switch (property_id) {
  case PROP_PROPERTIES:
    g_value_take_boxed (value, wp_session_item_get_properties (self));
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static WpObjectFeatures
session_item_default_get_supported_features (WpObject * self)
{
  return WP_SESSION_ITEM_FEATURE_ACTIVE | WP_SESSION_ITEM_FEATURE_EXPORTED;
}

static guint
session_item_default_activate_get_next_step (WpObject * object,
     WpFeatureActivationTransition * t, guint step, WpObjectFeatures missing)
{
  switch (step) {
    case WP_TRANSITION_STEP_NONE:
      if (missing & WP_SESSION_ITEM_FEATURE_ACTIVE)
        return STEP_ACTIVATE;
      else
        if (missing & WP_SESSION_ITEM_FEATURE_EXPORTED)
          return STEP_EXPORT;
        else
          return WP_TRANSITION_STEP_NONE;

    case STEP_ACTIVATE:
      if (missing & WP_SESSION_ITEM_FEATURE_EXPORTED)
        return STEP_EXPORT;
      else
        return WP_TRANSITION_STEP_NONE;

    case STEP_EXPORT:
      return WP_TRANSITION_STEP_NONE;

    default:
      return WP_TRANSITION_STEP_ERROR;
  }
}

static void
session_item_default_activate_execute_step (WpObject * object,
    WpFeatureActivationTransition * t, guint step, WpObjectFeatures missing)
{
  WpSessionItem *self = WP_SESSION_ITEM (object);
  WpTransition *transition = WP_TRANSITION (t);

  switch (step) {
    case STEP_ACTIVATE:
      if (!WP_SESSION_ITEM_GET_CLASS (self)->enable_active) {
        wp_transition_return_error (transition,
            g_error_new (WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
            "session-item: virtual enable_active method is not defined"));
        return;
      }
      WP_SESSION_ITEM_GET_CLASS (self)->enable_active (self, transition);
      break;

    case STEP_EXPORT:
      if (!WP_SESSION_ITEM_GET_CLASS (self)->enable_exported) {
        wp_transition_return_error (transition,
            g_error_new (WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
            "session-item: virtual enable_exported method is not defined"));
        return;
      }
      WP_SESSION_ITEM_GET_CLASS (self)->enable_exported (self, transition);
      break;

    case WP_TRANSITION_STEP_ERROR:
      break;

    default:
      g_return_if_reached ();
  }
}

static void
session_item_default_deactivate (WpObject * object, WpObjectFeatures features)
{
  WpSessionItem *self = WP_SESSION_ITEM (object);
  guint current = wp_object_get_active_features (object);

  if (features & current & WP_SESSION_ITEM_FEATURE_ACTIVE) {
    g_return_if_fail (WP_SESSION_ITEM_GET_CLASS (self)->disable_active);
    WP_SESSION_ITEM_GET_CLASS (self)->disable_active (self);
  }

  if (features & current & WP_SESSION_ITEM_FEATURE_EXPORTED) {
    g_return_if_fail (WP_SESSION_ITEM_GET_CLASS (self)->disable_exported);
    WP_SESSION_ITEM_GET_CLASS (self)->disable_exported (self);
  }
}

static void
wp_session_item_class_init (WpSessionItemClass * klass)
{
  WpObjectClass * wpobject_class = (WpObjectClass *) klass;
  GObjectClass * object_class = (GObjectClass *) klass;

  object_class->dispose = wp_session_item_dispose;
  object_class->get_property = wp_session_item_get_gobject_property;

  wpobject_class->get_supported_features =
      session_item_default_get_supported_features;
  wpobject_class->activate_get_next_step =
      session_item_default_activate_get_next_step;
  wpobject_class->activate_execute_step =
      session_item_default_activate_execute_step;
  wpobject_class->deactivate = session_item_default_deactivate;

  klass->reset = wp_session_item_default_reset;

  g_object_class_install_property (object_class, PROP_PROPERTIES,
      g_param_spec_boxed ("properties", "properties",
          "The session item properties", WP_TYPE_PROPERTIES,
          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
}

/*!
 * \brief Resets the session item.
 *
 * This essentially removes the configuration and deactivates all active features.
 *
 * \ingroup wpsessionitem
 * \param self the session item
 */
void
wp_session_item_reset (WpSessionItem * self)
{
  g_return_if_fail (WP_IS_SESSION_ITEM (self));
  g_return_if_fail (WP_SESSION_ITEM_GET_CLASS (self)->reset);

  return WP_SESSION_ITEM_GET_CLASS (self)->reset (self);
}

/*!
 * \brief Configures the session item with a set of properties
 *
 * \ingroup wpsessionitem
 * \param self the session item
 * \param props (transfer full): the properties used to configure the item
 * \returns TRUE on success, FALSE if the item could not be configured
 */
gboolean
wp_session_item_configure (WpSessionItem * self, WpProperties * props)
{
  g_return_val_if_fail (WP_IS_SESSION_ITEM (self), FALSE);
  g_return_val_if_fail (WP_SESSION_ITEM_GET_CLASS (self)->configure,
      FALSE);

  return WP_SESSION_ITEM_GET_CLASS (self)->configure (self, props);
}

/*!
 * \brief Checks if the session item is configured
 *
 * \ingroup wpsessionitem
 * \param self the session item
 * \returns TRUE if the item is configured, FALSE otherwise
 */
gboolean
wp_session_item_is_configured (WpSessionItem * self)
{
  WpSessionItemPrivate *priv = NULL;

  g_return_val_if_fail (WP_IS_SESSION_ITEM (self), FALSE);

  priv = wp_session_item_get_instance_private (self);
  return priv->properties != NULL;
}

/*!
 * \brief An associated proxy is a WpProxy subclass instance that
 * is somehow related to this item.
 *
 * \ingroup wpsessionitem
 * \param self the session item
 * \param proxy_type a WpProxy subclass GType
 * \returns (nullable) (transfer full) (type WpProxy): the associated proxy
 *   of the specified @em proxy_type, or NULL if there is no association to
 *   such a proxy
 */
gpointer
wp_session_item_get_associated_proxy (WpSessionItem * self, GType proxy_type)
{
  g_return_val_if_fail (WP_IS_SESSION_ITEM (self), NULL);
  g_return_val_if_fail (WP_SESSION_ITEM_GET_CLASS (self)->get_associated_proxy,
      NULL);
  g_return_val_if_fail (g_type_is_a (proxy_type, WP_TYPE_PROXY), NULL);

  return WP_SESSION_ITEM_GET_CLASS (self)->get_associated_proxy (self, proxy_type);
}

/*!
 * \brief Gets the bound id of a proxy associated with the session item
 *
 * \ingroup wpsessionitem
 * \param self the session item
 * \param proxy_type a WpProxy subclass GType
 * \returns the bound id of the associated proxy of the specified @em proxy_type,
 *   or `SPA_ID_INVALID` if there is no association to such a proxy
 */
guint32
wp_session_item_get_associated_proxy_id (WpSessionItem * self, GType proxy_type)
{
  g_autoptr (WpProxy) proxy = wp_session_item_get_associated_proxy (self,
      proxy_type);
  if (!proxy)
    return SPA_ID_INVALID;

  return wp_proxy_get_bound_id (proxy);
}

/*!
 * \brief Registers the session item to its associated core
 *
 * \ingroup wpsessionitem
 * \param self (transfer full): the session item
 */
void
wp_session_item_register (WpSessionItem * self)
{
  g_autoptr (WpCore) core = NULL;

  g_return_if_fail (WP_IS_SESSION_ITEM (self));

  core = wp_object_get_core (WP_OBJECT (self));
  wp_core_register_object (core, self);
}

/*!
 * \brief Removes the session item from its associated core
 *
 * \ingroup wpsessionitem
 * \param self (transfer none): the session item
 */
void
wp_session_item_remove (WpSessionItem * self)
{
  g_autoptr (WpCore) core = NULL;

  g_return_if_fail (WP_IS_SESSION_ITEM (self));

  core = wp_object_get_core (WP_OBJECT (self));
  wp_core_remove_object (core, self);
}

/*!
 * \brief Gets the properties of a session item.
 *
 * \ingroup wpsessionitem
 * \param self the session item
 * \returns (transfer full): the item's properties.
 */
WpProperties *
wp_session_item_get_properties (WpSessionItem * self)
{
  WpSessionItemPrivate *priv = NULL;

  g_return_val_if_fail (WP_IS_SESSION_ITEM (self), NULL);

  priv = wp_session_item_get_instance_private (self);
  return priv->properties ? wp_properties_ref (priv->properties) : NULL;
}

/*!
 * \brief Looks up a named session item property value for a given key.
 *
 * \ingroup wpsessionitem
 * \param self the session item
 * \param key the property key
 * \returns the item property value for the given key.
 */
const gchar *
wp_session_item_get_property (WpSessionItem * self, const gchar *key)
{
  WpSessionItemPrivate *priv = NULL;

  g_return_val_if_fail (WP_IS_SESSION_ITEM (self), NULL);

  priv = wp_session_item_get_instance_private (self);
  return priv->properties ? wp_properties_get (priv->properties, key) : NULL;
}

/*!
 * \brief Sets the item's properties.
 *
 * This should only be done by sub-classes after the configuration has been done.
 *
 * \ingroup wpsessionitem
 * \param self the session item
 * \param props (transfer full): the new properties to set
 */
void
wp_session_item_set_properties (WpSessionItem * self,
    WpProperties *props)
{
  WpSessionItemPrivate *priv = NULL;

  g_return_if_fail (WP_IS_SESSION_ITEM (self));

  priv = wp_session_item_get_instance_private (self);
  g_clear_pointer (&priv->properties, wp_properties_unref);
  priv->properties = wp_properties_ensure_unique_owner (props);
}

static gboolean
on_session_item_proxy_destroyed_deferred (WpSessionItem * item)
{
  wp_info_object (item, "destroying session item upon request by the server");
  wp_object_deactivate (WP_OBJECT (item), WP_SESSION_ITEM_FEATURE_EXPORTED);
  return G_SOURCE_REMOVE;
}

/*!
 * \brief Helper callback for sub-classes that defers and unexports
 * the session item.
 *
 * Only meant to be used when the pipewire proxy destroyed signal is triggered.
 *
 * \ingroup wpsessionitem
 * \param proxy the proxy that was destroyed by the server
 * \param item the associated session item
 */
void
wp_session_item_handle_proxy_destroyed (WpProxy * proxy, WpSessionItem * item)
{
  g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (proxy));
  if (core)
    wp_core_idle_add_closure (core, NULL, g_cclosure_new_object (
        G_CALLBACK (on_session_item_proxy_destroyed_deferred),
        G_OBJECT (item)));
}
