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

#include "global-proxy.h"
#include "private/registry.h"
#include "core.h"
#include "error.h"
#include "log.h"

WP_DEFINE_LOCAL_LOG_TOPIC ("wp-global-proxy")

/*! \defgroup wpglobalproxy WpGlobalProxy */
/*!
 * \struct WpGlobalProxy
 *
 * A proxy that represents a PipeWire global object, i.e. an
 * object that is made available through the PipeWire registry.
 *
 * \gproperties
 *
 * \gproperty{global, WpGlobal *, G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY,
 *   Internal WpGlobal object}
 *
 * \gproperty{factory-name, gchar *, G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY,
 *   The factory name}
 *
 * \gproperty{global-properties, WpProperties *,
 *   G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY,
 *   The pipewire global properties}
 *
 * \gproperty{permissions, guint, G_PARAM_READABLE,
 *   The pipewire global permissions}
 */

typedef struct _WpGlobalProxyPrivate WpGlobalProxyPrivate;
struct _WpGlobalProxyPrivate
{
  WpGlobal *global;
  gchar factory_name[96];
  WpProperties *properties;
};

enum {
  PROP_0,
  PROP_GLOBAL,
  PROP_FACTORY_NAME,
  PROP_GLOBAL_PROPERTIES,
  PROP_PERMISSIONS,
};

G_DEFINE_TYPE_WITH_PRIVATE (WpGlobalProxy, wp_global_proxy, WP_TYPE_PROXY)

static void
wp_global_proxy_init (WpGlobalProxy * self)
{
}

static void
wp_global_proxy_dispose (GObject * object)
{
  WpGlobalProxy *self = WP_GLOBAL_PROXY (object);
  WpGlobalProxyPrivate *priv =
      wp_global_proxy_get_instance_private (self);

  if (priv->global)
    wp_global_rm_flag (priv->global, WP_GLOBAL_FLAG_OWNED_BY_PROXY);

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

static void
wp_global_proxy_finalize (GObject * object)
{
  WpGlobalProxy *self = WP_GLOBAL_PROXY (object);
  WpGlobalProxyPrivate *priv =
      wp_global_proxy_get_instance_private (self);

  g_clear_pointer (&priv->properties, wp_properties_unref);
  g_clear_pointer (&priv->global, wp_global_unref);

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

static void
wp_global_proxy_set_property (GObject * object, guint property_id,
    const GValue * value, GParamSpec * pspec)
{
  WpGlobalProxy *self = WP_GLOBAL_PROXY (object);
  WpGlobalProxyPrivate *priv =
      wp_global_proxy_get_instance_private (self);

  switch (property_id) {
  case PROP_GLOBAL:
    priv->global = g_value_dup_boxed (value);
    break;
  case PROP_FACTORY_NAME:
    priv->factory_name[0] = '\0';
    strncpy (priv->factory_name, g_value_get_string (value),
        sizeof (priv->factory_name) - 1);
    break;
  case PROP_GLOBAL_PROPERTIES:
    priv->properties = g_value_dup_boxed (value);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
wp_global_proxy_get_property (GObject * object, guint property_id,
    GValue * value, GParamSpec * pspec)
{
  WpGlobalProxy *self = WP_GLOBAL_PROXY (object);

  switch (property_id) {
  case PROP_PERMISSIONS:
    g_value_set_uint (value, wp_global_proxy_get_permissions (self));
    break;
  case PROP_GLOBAL_PROPERTIES:
    g_value_take_boxed (value, wp_global_proxy_get_global_properties (self));
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static WpObjectFeatures
wp_global_proxy_get_supported_features (WpObject * object)
{
  return WP_PROXY_FEATURE_BOUND;
}

enum {
  STEP_BIND = WP_TRANSITION_STEP_CUSTOM_START,
};

static guint
wp_global_proxy_activate_get_next_step (WpObject * object,
    WpFeatureActivationTransition * transition, guint step,
    WpObjectFeatures missing)
{
  /* we only support BOUND, so this is the only
     feature that can be in @em missing */
  g_return_val_if_fail (missing == WP_PROXY_FEATURE_BOUND,
      WP_TRANSITION_STEP_ERROR);

  return STEP_BIND;
}

static void
wp_global_proxy_step_bind (WpObject * object,
    WpFeatureActivationTransition * transition, guint step,
    WpObjectFeatures missing)
{
  WpProxyClass *proxy_klass = WP_PROXY_GET_CLASS (WP_PROXY (object));
  WpGlobalProxy *self = WP_GLOBAL_PROXY (object);
  WpGlobalProxyPrivate *priv = wp_global_proxy_get_instance_private (self);

  /* Create the pipewire object if global is NULL */
  if (priv->global == NULL && priv->factory_name[0] != '\0') {
    g_autoptr (WpCore) core = NULL;
    struct pw_core *pw_core = NULL;
    struct pw_proxy * p = NULL;

    core = wp_object_get_core (WP_OBJECT (self));
    if (G_UNLIKELY (!core)) {
        wp_transition_return_error (WP_TRANSITION (transition), g_error_new (
            WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
            "The WirePlumber core is not valid; object cannot be created"));
      return;
    }

    pw_core = wp_core_get_pw_core (core);
    if (G_UNLIKELY (!pw_core)) {
        wp_transition_return_error (WP_TRANSITION (transition), g_error_new (
            WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
            "The WirePlumber core is not connected; object cannot be created"));
      return;
    }

    p = pw_core_create_object (pw_core, priv->factory_name,
        proxy_klass->pw_iface_type, proxy_klass->pw_iface_version,
        priv->properties ?
            wp_properties_peek_dict (priv->properties) : NULL, 0);
    if (G_UNLIKELY (!p)) {
      wp_transition_return_error (WP_TRANSITION (transition), g_error_new (
            WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
            "Failed to create object with given factory name and properties"));
      return;
    }

    wp_proxy_set_pw_proxy (WP_PROXY (self), p);
  }

  /* Bind */
  if (wp_proxy_get_pw_proxy (WP_PROXY (self)) == NULL) {
    if (!wp_global_proxy_bind (self)) {
      wp_transition_return_error (WP_TRANSITION (transition), g_error_new (
          WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
              "global not specified or destroyed; cannot bind proxy"));
    }
  }
}


static void
wp_global_proxy_activate_execute_step (WpObject * object,
    WpFeatureActivationTransition * transition, guint step,
    WpObjectFeatures missing)
{
  switch (step) {
  case STEP_BIND:
    wp_global_proxy_step_bind (object, transition, step, missing);
    break;
  case WP_TRANSITION_STEP_ERROR:
    break;
  default:
    g_assert_not_reached ();
  }
}

static void
wp_global_proxy_bound (WpProxy * proxy, guint32 global_id)
{
  WpGlobalProxy *self = WP_GLOBAL_PROXY (proxy);
  WpGlobalProxyPrivate *priv =
      wp_global_proxy_get_instance_private (self);
  g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));

  if (!priv->global) {
    wp_registry_prepare_new_global (wp_core_get_registry (core),
        global_id, PW_PERM_ALL, WP_GLOBAL_FLAG_OWNED_BY_PROXY,
        G_TYPE_FROM_INSTANCE (self), self,
        priv->properties ? wp_properties_peek_dict (priv->properties) : NULL,
        &priv->global);
  }
}

static void
wp_global_proxy_destroyed (WpProxy * proxy)
{
  WpGlobalProxy *self = WP_GLOBAL_PROXY (proxy);
  WpGlobalProxyPrivate *priv =
      wp_global_proxy_get_instance_private (self);

  if (priv->global && priv->global->proxy &&
      (priv->global->flags & WP_GLOBAL_FLAG_OWNED_BY_PROXY)) {
   /* We can end up here as a result of _request_destroy() followed by
    * _deactivate(FEATURE_BOUND), where the latter triggers this callback
    * before _remove_global is processed.
    * If proxy is owned, it is gone now, so not much owned left.
    * self might be cleaned up soon, so this is a good time to remove
    * the non-refcounted backreference in global.  If not done now, _dispose()
    * does not have a chance to cleanup (as the reference to global is gone).
    * If remove_global then comes in later, there is no more real work to
    * do when WP_GLOBAL_FLAG_APPEARS_ON_REGISTRY is removed
    */
    wp_global_rm_flag (priv->global, WP_GLOBAL_FLAG_OWNED_BY_PROXY);
  }
  g_clear_pointer (&priv->global, wp_global_unref);
}

static void
wp_global_proxy_class_init (WpGlobalProxyClass * klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;
  WpObjectClass *wpobject_class = (WpObjectClass *) klass;
  WpProxyClass *proxy_class = (WpProxyClass *) klass;

  object_class->finalize = wp_global_proxy_finalize;
  object_class->dispose = wp_global_proxy_dispose;
  object_class->set_property = wp_global_proxy_set_property;
  object_class->get_property = wp_global_proxy_get_property;

  wpobject_class->get_supported_features =
      wp_global_proxy_get_supported_features;
  wpobject_class->activate_get_next_step =
      wp_global_proxy_activate_get_next_step;
  wpobject_class->activate_execute_step =
      wp_global_proxy_activate_execute_step;

  proxy_class->bound = wp_global_proxy_bound;
  proxy_class->pw_proxy_destroyed = wp_global_proxy_destroyed;

  g_object_class_install_property (object_class, PROP_GLOBAL,
      g_param_spec_boxed ("global", "global", "Internal WpGlobal object",
          wp_global_get_type (),
          G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_FACTORY_NAME,
      g_param_spec_string ("factory-name", "factory-name",
          "The factory name", "",
          G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_GLOBAL_PROPERTIES,
      g_param_spec_boxed ("global-properties", "global-properties",
          "The pipewire global properties", WP_TYPE_PROPERTIES,
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_PERMISSIONS,
      g_param_spec_uint ("permissions", "permissions",
          "The pipewire global permissions", 0, G_MAXUINT, 0,
          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
}

/*!
 * \brief Requests the PipeWire server to destroy the object represented by
 * this proxy.
 *
 * If the server allows it, the object will be destroyed and the
 * WpProxy's `pw-proxy-destroyed` signal will be emitted. If the server does
 * not allow it, nothing will happen.
 *
 * This is mostly useful for destroying WpLink objects.
 *
 * \ingroup wpglobalproxy
 * \param self the pipewire global
 */
void
wp_global_proxy_request_destroy (WpGlobalProxy * self)
{
  g_return_if_fail (WP_IS_GLOBAL_PROXY (self));

  WpGlobalProxyPrivate *priv =
      wp_global_proxy_get_instance_private (self);
  g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));

  if (priv->global && core) {
    WpRegistry *reg = wp_core_get_registry (core);
    if (reg->pw_registry)
      pw_registry_destroy (reg->pw_registry, priv->global->id);
  }
}

/*!
 * \brief Gets the permissions of a pipewire global
 * \ingroup wpglobalproxy
 * \param self the pipewire global
 * \returns the permissions that wireplumber has on this object
 */
guint32
wp_global_proxy_get_permissions (WpGlobalProxy * self)
{
  g_return_val_if_fail (WP_IS_GLOBAL_PROXY (self), 0);

  WpGlobalProxyPrivate *priv =
      wp_global_proxy_get_instance_private (self);

  return priv->global ? priv->global->permissions : PW_PERM_ALL;
}

/*!
 * \brief Gets the global properties of a pipewire global
 * \ingroup wpglobalproxy
 * \param self the pipewire global
 * \returns (transfer full): the global (immutable) properties of this
 *   pipewire object
 */
WpProperties *
wp_global_proxy_get_global_properties (WpGlobalProxy * self)
{
  g_return_val_if_fail (WP_IS_GLOBAL_PROXY (self), NULL);

  WpGlobalProxyPrivate *priv =
      wp_global_proxy_get_instance_private (self);

  if (priv->global && priv->global->properties)
    return wp_properties_ref (priv->global->properties);
  return NULL;
}

/*!
 * \brief Binds to the global and creates the underlying `pw_proxy`.
 *
 * This is mostly meant to be called internally. It will create the `pw_proxy`
 * and will activate the WP_PROXY_FEATURE_BOUND feature.
 *
 * This may only be called if there is no `pw_proxy` associated with this
 * object yet.
 *
 * \ingroup wpglobalproxy
 * \param self the pipewire global
 * \returns TRUE on success, FALSE if there is no global to bind to
 */
gboolean
wp_global_proxy_bind (WpGlobalProxy * self)
{
  WpGlobalProxyPrivate *priv;
  struct pw_proxy *p;

  g_return_val_if_fail (WP_IS_GLOBAL_PROXY (self), FALSE);
  g_return_val_if_fail (wp_proxy_get_pw_proxy (WP_PROXY (self)) == NULL, FALSE);

  priv = wp_global_proxy_get_instance_private (self);

  if (!priv->global || !priv->global->proxy)
    return FALSE;

  /*
   * We can bind only if the WpGlobal will unbind this proxy on removal, so
   * assert here that this is so. Why: pw_registry_bind can race with the global
   * id being replaced with a different object on server side, so we must rely
   * on the registry_global messages to know if the object was replaced.  If the
   * race occurs, and a wrong object is going to be bound here, what will then
   * happen is that WpGlobal will dispose this proxy and no problem arises.
   */
  g_return_val_if_fail (priv->global->proxy == self, FALSE);

  p = wp_global_bind (priv->global);
  if (!p)
    return FALSE;

  wp_proxy_set_pw_proxy (WP_PROXY (self), p);
  return TRUE;
}
