/* WirePlumber
 *
 * Copyright © 2019-2020 Collabora Ltd.
 *    @author Julian Bouzas <julian.bouzas@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#define G_LOG_DOMAIN "wp-link"

#include "link.h"
#include "wpenums.h"
#include "private/pipewire-object-mixin.h"

/*! \defgroup wplink WpLink */
/*!
 * \struct WpLink
 *
 * The WpLink class allows accessing the properties and methods of a
 * PipeWire link object (`struct pw_link`).
 *
 * A WpLink is constructed internally when a new link appears on the
 * PipeWire registry and it is made available through the WpObjectManager API.
 * Alternatively, a WpLink can also be constructed using
 * wp_link_new_from_factory(), which creates a new link object
 * on the remote PipeWire server by calling into a factory.
 *
 * \gproperties
 *
 * \gproperty{state, WpLinkState, G_PARAM_READABLE, The current state of the link}
 *
 * \gsignals
 *
 * \par state-changed
 * \parblock
 * \code
 * void
 * state_changed_callback (WpLink * self,
 *                         WpLinkState * old_state,
 *                         WpLinkState * new_state,
 *                         gpointer user_data)
 * \endcode
 *
 * Emitted when the link changes state. This is only emitted when
 * WP_PIPEWIRE_OBJECT_FEATURE_INFO is enabled.
 *
 * Parameters:
 * - `old_state` - the old state
 * - `new_state` - the new state
 *
 * Flags: G_SIGNAL_RUN_LAST
 * \endparblock
 */

enum {
  PROP_STATE = WP_PW_OBJECT_MIXIN_PROP_CUSTOM_START,
};

enum {
  SIGNAL_STATE_CHANGED,
  N_SIGNALS,
};

static guint32 signals[N_SIGNALS] = {0};

struct _WpLink
{
  WpGlobalProxy parent;
};

static void wp_link_pw_object_mixin_priv_interface_init (
    WpPwObjectMixinPrivInterface * iface);

G_DEFINE_TYPE_WITH_CODE (WpLink, wp_link, WP_TYPE_GLOBAL_PROXY,
    G_IMPLEMENT_INTERFACE (WP_TYPE_PIPEWIRE_OBJECT,
        wp_pw_object_mixin_object_interface_init)
    G_IMPLEMENT_INTERFACE (WP_TYPE_PW_OBJECT_MIXIN_PRIV,
        wp_link_pw_object_mixin_priv_interface_init))

static void
wp_link_init (WpLink * self)
{
}

static void
wp_link_get_property (GObject * object, guint property_id,
    GValue * value, GParamSpec * pspec)
{
  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (object);

  switch (property_id) {
  case PROP_STATE:
    g_value_set_enum (value, d->info ?
        ((struct pw_link_info *) d->info)->state : 0);
    break;
  default:
    wp_pw_object_mixin_get_property (object, property_id, value, pspec);
    break;
  }
}

static WpObjectFeatures
wp_link_get_supported_features (WpObject * object)
{
  return wp_pw_object_mixin_get_supported_features (object)
      | WP_LINK_FEATURE_ESTABLISHED;
}

enum {
  STEP_WAIT_ESTABLISHED = WP_PW_OBJECT_MIXIN_STEP_CUSTOM_START,
};

static void
wp_link_activate_execute_step (WpObject * object,
    WpFeatureActivationTransition * transition, guint step,
    WpObjectFeatures missing)
{
  switch (step) {
  case WP_PW_OBJECT_MIXIN_STEP_BIND:
  case WP_TRANSITION_STEP_ERROR:
    /* base class can handle BIND and ERROR */
    WP_OBJECT_CLASS (wp_link_parent_class)->
        activate_execute_step (object, transition, step, missing);
    break;
  case WP_PW_OBJECT_MIXIN_STEP_WAIT_INFO:
    /* just wait, info will be emitted anyway after binding */
    break;
  case STEP_WAIT_ESTABLISHED:
    /* just wait again, the state will be changed automatically */
    break;
  default:
    g_assert_not_reached ();
  }
}

static const struct pw_link_events link_events = {
  PW_VERSION_LINK_EVENTS,
  .info = (HandleEventInfoFunc(link)) wp_pw_object_mixin_handle_event_info,
};

static void
wp_link_pw_proxy_created (WpProxy * proxy, struct pw_proxy * pw_proxy)
{
  wp_pw_object_mixin_handle_pw_proxy_created (proxy, pw_proxy,
      link, &link_events);
}

static void
wp_link_pw_proxy_destroyed (WpProxy * proxy)
{
  wp_pw_object_mixin_handle_pw_proxy_destroyed (proxy);
  wp_object_update_features (WP_OBJECT (proxy), 0, WP_LINK_FEATURE_ESTABLISHED);

  WP_PROXY_CLASS (wp_link_parent_class)->pw_proxy_destroyed (proxy);
}

static void
wp_link_class_init (WpLinkClass * klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;
  WpObjectClass *wpobject_class = (WpObjectClass *) klass;
  WpProxyClass *proxy_class = (WpProxyClass *) klass;

  object_class->get_property = wp_link_get_property;

  wpobject_class->get_supported_features = wp_link_get_supported_features;
  wpobject_class->activate_get_next_step =
      wp_pw_object_mixin_activate_get_next_step;
  wpobject_class->activate_execute_step = wp_link_activate_execute_step;

  proxy_class->pw_iface_type = PW_TYPE_INTERFACE_Link;
  proxy_class->pw_iface_version = PW_VERSION_LINK;
  proxy_class->pw_proxy_created = wp_link_pw_proxy_created;
  proxy_class->pw_proxy_destroyed = wp_link_pw_proxy_destroyed;

  wp_pw_object_mixin_class_override_properties (object_class);

  g_object_class_install_property (object_class, PROP_STATE,
      g_param_spec_enum ("state", "state", "state", WP_TYPE_LINK_STATE, 0,
          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));

  signals[SIGNAL_STATE_CHANGED] = g_signal_new (
      "state-changed", G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 2,
      WP_TYPE_LINK_STATE, WP_TYPE_LINK_STATE);

}

static void
wp_link_process_info (gpointer instance, gpointer old_info, gpointer i)
{
  WpObject *object = instance;
  const struct pw_link_info *info = i;

  if (info->change_mask & PW_LINK_CHANGE_MASK_STATE) {
    enum pw_link_state old_state = old_info ?
        ((struct pw_link_info *) old_info)->state : PW_LINK_STATE_INIT;

    g_signal_emit (instance, signals[SIGNAL_STATE_CHANGED], 0,
        old_state, info->state);

    if (info->state >= PW_LINK_STATE_PAUSED && old_state < PW_LINK_STATE_PAUSED)
      wp_object_update_features (object, WP_LINK_FEATURE_ESTABLISHED, 0);
    else if (info->state < PW_LINK_STATE_PAUSED && old_state >= PW_LINK_STATE_PAUSED)
      wp_object_update_features (object, 0, WP_LINK_FEATURE_ESTABLISHED);
  }
}

static void
wp_link_pw_object_mixin_priv_interface_init (
    WpPwObjectMixinPrivInterface * iface)
{
  wp_pw_object_mixin_priv_interface_info_init_no_params (iface, link, LINK);
  iface->process_info = wp_link_process_info;
}

/*!
 * \brief Constructs a link on the PipeWire server by asking the remote factory
 * \a factory_name to create it.
 *
 * Because of the nature of the PipeWire protocol, this operation completes
 * asynchronously at some point in the future. In order to find out when
 * this is done, you should call wp_object_activate(), requesting at least
 * WP_PROXY_FEATURE_BOUND. When this feature is ready, the link is ready for
 * use on the server. If the link cannot be created, this activation operation
 * will fail.
 *
 * \ingroup wplink
 * \param core the wireplumber core
 * \param factory_name the pipewire factory name to construct the link
 * \param properties (nullable) (transfer full): the properties to pass to the
 *   factory
 * \returns (nullable) (transfer full): the new link or NULL if the core
 *   is not connected and therefore the link cannot be created
 */
WpLink *
wp_link_new_from_factory (WpCore * core,
    const gchar * factory_name, WpProperties * properties)
{
  g_autoptr (WpProperties) props = properties;
  return g_object_new (WP_TYPE_LINK,
      "core", core,
      "factory-name", factory_name,
      "global-properties", props,
      NULL);
}

/*!
 * \brief Retrieves the ids of the objects that are linked by this link
 *
 * \remark Requires WP_PIPEWIRE_OBJECT_FEATURE_INFO
 *
 * \ingroup wplink
 * \param self the link
 * \param output_node (out) (optional): the bound id of the output (source) node
 * \param output_port (out) (optional): the bound id of the output (source) port
 * \param input_node (out) (optional): the bound id of the input (sink) node
 * \param input_port (out) (optional): the bound id of the input (sink) port
 */
void
wp_link_get_linked_object_ids (WpLink * self,
    guint32 * output_node, guint32 * output_port,
    guint32 * input_node, guint32 * input_port)
{
  g_return_if_fail (WP_IS_LINK (self));

  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (self);
  struct pw_link_info *info = d->info;
  g_return_if_fail (info);

  if (output_node)
    *output_node = info->output_node_id;
  if (output_port)
    *output_port = info->output_port_id;
  if (input_node)
    *input_node = info->input_node_id;
  if (input_port)
    *input_port = info->input_port_id;
}

/*!
 * \brief Gets the current state of the link
 * \ingroup wplink
 * \param self the link
 * \param error (out) (optional) (transfer none): the error
 * \returns the current state of the link
 * \since 0.4.11
 */
WpLinkState
wp_link_get_state (WpLink * self, const gchar ** error)
{
  g_return_val_if_fail (WP_IS_LINK (self), WP_LINK_STATE_ERROR);
  g_return_val_if_fail (wp_object_get_active_features (WP_OBJECT (self)) &
          WP_PIPEWIRE_OBJECT_FEATURE_INFO, WP_LINK_STATE_ERROR);

  WpPwObjectMixinData *d = wp_pw_object_mixin_get_data (self);
  const struct pw_link_info *info = d->info;

  if (error)
    *error = info->error;
  return (WpLinkState) info->state;
}
