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

#define G_LOG_DOMAIN "wp-proxy"

#include "proxy.h"
#include "log.h"
#include "error.h"

#include <pipewire/pipewire.h>
#include <spa/utils/hook.h>
#include <spa/utils/result.h>

/*! \defgroup wpproxy WpProxy */
/*!
 * \struct WpProxy
 *
 * Base class for all objects that expose PipeWire objects using `pw_proxy`
 * underneath.
 *
 * This base class cannot be instantiated. It provides handling of
 * pw_proxy's events and exposes common functionality.
 *
 * \gproperties
 *
 * \gproperty{bound-id, guint, G_PARAM_READABLE,
 *   The id that this object has on the registry}
 *
 * \gproperty{pw-proxy, gpointer, G_PARAM_READABLE,
 *   The `struct pw_proxy *`}
 *
 * \gsignals
 *
 * \par bound
 * \parblock
 * \code
 * void
 * bound_callback (WpProxy * self,
 *                 guint id,
 *                 gpointer user_data)
 * \endcode
 * Parameters:
 * - `id` - the bound id of the proxy
 *
 * Flags: G_SIGNAL_RUN_FIRST
 * \endparblock
 *
 * \par pw-proxy-created
 * \parblock
 * \code
 * void
 * pw_proxy_created_callback (WpProxy * self,
 *                            gpointer object,
 *                            gpointer user_data)
 * \endcode
 * Parameters:
 * - `object` - pointer to the pw_proxy that was just created
 *
 * Flags: G_SIGNAL_RUN_FIRST
 * \endparblock
 *
 * \par pw-proxy-destroyed
 * \parblock
 * \code
 * void
 * pw_proxy_destroyed_callback (WpProxy * self,
 *                              gpointer user_data)
 * \endcode
 * Flags: G_SIGNAL_RUN_FIRST
 * \endparblock
 *
 * \par error
 * \parblock
 * \code
 * void
 * error_callback (WpProxy * self,
 *                 gint seq, gint res, const gchar *message,
 *                 gpointer user_data)
 * \endcode
 * Emitted when an error occurs on the remote object.
 * The parameters are exactly the same as on the underlying pw_proxy error
 * callback.
 *
 * Parameters:
 * - `seq` - the sequence number of the operation that caused the error
 * - `res` - the error code
 * - `message` - a description of the error
 *
 * Flags: G_SIGNAL_RUN_FIRST
 * \endparblock
 */

typedef struct _WpProxyPrivate WpProxyPrivate;
struct _WpProxyPrivate
{
  struct pw_proxy *pw_proxy;
  struct spa_hook listener;
};

enum {
  PROP_0,
  PROP_BOUND_ID,
  PROP_PW_PROXY,
};

enum
{
  SIGNAL_PW_PROXY_CREATED,
  SIGNAL_PW_PROXY_DESTROYED,
  SIGNAL_BOUND,
  SIGNAL_ERROR,
  LAST_SIGNAL,
};

static guint signals[LAST_SIGNAL] = { 0 };

G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (WpProxy, wp_proxy, WP_TYPE_OBJECT)

static void
proxy_event_destroy (void *data)
{
  WpProxy *self = WP_PROXY (data);
  WpProxyPrivate *priv = wp_proxy_get_instance_private (self);

  wp_trace_object (self, "destroyed pw_proxy %p (%u)", priv->pw_proxy,
      pw_proxy_get_bound_id (priv->pw_proxy));

  spa_hook_remove (&priv->listener);
  priv->pw_proxy = NULL;
  wp_object_update_features (WP_OBJECT (self), 0, WP_PROXY_FEATURE_BOUND);

  wp_object_abort_activation (WP_OBJECT (self), "PipeWire proxy destroyed");

  g_signal_emit (self, signals[SIGNAL_PW_PROXY_DESTROYED], 0);
}

static void
proxy_event_bound (void *data, uint32_t global_id)
{
  WpProxy *self = WP_PROXY (data);

  wp_trace_object (self, "bound to %u", global_id);

  wp_object_update_features (WP_OBJECT (self), WP_PROXY_FEATURE_BOUND, 0);
  g_signal_emit (self, signals[SIGNAL_BOUND], 0, global_id);
}

static void
proxy_event_removed (void *data)
{
  wp_trace_object (data, "removed");
}

static void
proxy_event_error (void *data, int seq, int res, const char *message)
{
  WpProxy *self = WP_PROXY (data);
  WpProxyPrivate *priv = wp_proxy_get_instance_private (self);

  wp_trace_object (self, "error seq:%d res:%d (%s) %s",
      seq, res, spa_strerror(res), message);

  /* we destroy the proxy on error if feature bound is still not enabled */
  if (priv->pw_proxy &&
      !(wp_object_get_active_features (WP_OBJECT (self)) & WP_PROXY_FEATURE_BOUND))
    pw_proxy_destroy (priv->pw_proxy);

  wp_object_abort_activation (WP_OBJECT (self), message);

  g_signal_emit (self, signals[SIGNAL_ERROR], 0, seq, res, message);
}

static const struct pw_proxy_events proxy_events = {
  PW_VERSION_PROXY_EVENTS,
  .destroy = proxy_event_destroy,
  .bound = proxy_event_bound,
  .removed = proxy_event_removed,
  .error = proxy_event_error,
};

static void
wp_proxy_init (WpProxy * self)
{
}

static void
wp_proxy_dispose (GObject * object)
{
  WpProxyPrivate *priv = wp_proxy_get_instance_private (WP_PROXY (object));

  if (priv->pw_proxy)
    pw_proxy_destroy (priv->pw_proxy);

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

static void
wp_proxy_get_property (GObject * object, guint property_id, GValue * value,
    GParamSpec * pspec)
{
  WpProxy *self = WP_PROXY (object);

  switch (property_id) {
  case PROP_BOUND_ID:
    g_value_set_uint (value, wp_proxy_get_bound_id (self));
    break;
  case PROP_PW_PROXY:
    g_value_set_pointer (value, wp_proxy_get_pw_proxy (self));
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
wp_proxy_deactivate (WpObject * object, WpObjectFeatures features)
{
  if (features & WP_PROXY_FEATURE_BOUND) {
    WpProxyPrivate *priv = wp_proxy_get_instance_private (WP_PROXY (object));
    if (priv->pw_proxy)
      pw_proxy_destroy (priv->pw_proxy);
    wp_object_update_features (object, 0, WP_PROXY_FEATURE_BOUND);
  }
}

static void
wp_proxy_class_init (WpProxyClass * klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;
  WpObjectClass *wpobject_class = (WpObjectClass *) klass;

  object_class->get_property = wp_proxy_get_property;
  object_class->dispose = wp_proxy_dispose;

  wpobject_class->deactivate = wp_proxy_deactivate;

  /* Install the properties */

  g_object_class_install_property (object_class, PROP_BOUND_ID,
      g_param_spec_uint ("bound-id", "bound-id",
          "The id that this object has on the registry", 0, G_MAXUINT, 0,
          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_PW_PROXY,
      g_param_spec_pointer ("pw-proxy", "pw-proxy", "The struct pw_proxy *",
          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));

  /* Signals */
  signals[SIGNAL_PW_PROXY_CREATED] = g_signal_new (
      "pw-proxy-created", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_FIRST,
      G_STRUCT_OFFSET (WpProxyClass, pw_proxy_created), NULL, NULL, NULL,
      G_TYPE_NONE, 1, G_TYPE_POINTER);

  signals[SIGNAL_PW_PROXY_DESTROYED] = g_signal_new (
      "pw-proxy-destroyed", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_FIRST,
      G_STRUCT_OFFSET (WpProxyClass, pw_proxy_destroyed), NULL, NULL, NULL,
      G_TYPE_NONE, 0);

  signals[SIGNAL_BOUND] = g_signal_new (
      "bound", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_FIRST,
      G_STRUCT_OFFSET (WpProxyClass, bound), NULL, NULL, NULL,
      G_TYPE_NONE, 1, G_TYPE_UINT);

  signals[SIGNAL_ERROR] = g_signal_new (
      "error", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_FIRST,
      G_STRUCT_OFFSET (WpProxyClass, error), NULL, NULL, NULL,
      G_TYPE_NONE, 3, G_TYPE_INT, G_TYPE_INT, G_TYPE_STRING);
}

/*!
 * \brief Returns the proxy bound id.
 *
 * The bound id is the id that this object has on the
 * PipeWire registry (a.k.a. the global id). The object must have the
 * WP_PROXY_FEATURE_BOUND feature before this method can be called.
 *
 * \remarks Requires WP_PROXY_FEATURE_BOUND
 *
 * \ingroup wpproxy
 * \param self the proxy
 * \returns the bound id of this object
 */
guint32
wp_proxy_get_bound_id (WpProxy * self)
{
  g_return_val_if_fail (WP_IS_PROXY (self), 0);
  g_warn_if_fail (wp_object_get_active_features (WP_OBJECT (self)) &
      WP_PROXY_FEATURE_BOUND);

  WpProxyPrivate *priv = wp_proxy_get_instance_private (self);
  return priv->pw_proxy ? pw_proxy_get_bound_id (priv->pw_proxy) : SPA_ID_INVALID;
}

/*!
 * \brief Gets the interface type of the proxied object
 * \ingroup wpproxy
 * \param self the proxy
 * \param version (out) (optional): the version of the interface
 * \returns the PipeWire type of the interface that is being proxied
 */
const gchar *
wp_proxy_get_interface_type (WpProxy * self, guint32 * version)
{
  g_return_val_if_fail (WP_IS_PROXY (self), NULL);

  WpProxyPrivate *priv = wp_proxy_get_instance_private (self);
  if (priv->pw_proxy)
    return pw_proxy_get_type (priv->pw_proxy, version);
  else {
    WpProxyClass *klass = WP_PROXY_GET_CLASS (self);
    if (version)
      *version = klass->pw_iface_version;
    return klass->pw_iface_type;
  }
}

/*!
 * \brief Gets the `pw_proxy` wrapped by this proxy object
 * \ingroup wpproxy
 * \param self the proxy
 * \returns a pointer to the underlying `pw_proxy` object
 */
struct pw_proxy *
wp_proxy_get_pw_proxy (WpProxy * self)
{
  g_return_val_if_fail (WP_IS_PROXY (self), NULL);

  WpProxyPrivate *priv = wp_proxy_get_instance_private (self);
  return priv->pw_proxy;
}

/*!
 * \brief Private method to be used by subclasses to set the `pw_proxy` pointer
 * when it is available.
 *
 * This can be called only if there is no `pw_proxy` already set.
 * Takes ownership of \a proxy.
 *
 * \ingroup wpproxy
 */
void
wp_proxy_set_pw_proxy (WpProxy * self, struct pw_proxy * proxy)
{
  g_return_if_fail (WP_IS_PROXY (self));

  WpProxyPrivate *priv = wp_proxy_get_instance_private (self);

  g_return_if_fail (proxy);

  g_return_if_fail (priv->pw_proxy == NULL);
  priv->pw_proxy = proxy;

  pw_proxy_add_listener (priv->pw_proxy, &priv->listener, &proxy_events,
      self);

  /* inform subclasses and listeners */
  g_signal_emit (self, signals[SIGNAL_PW_PROXY_CREATED], 0, priv->pw_proxy);
}
