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

/**
 * SECTION: props
 * @title: Dynamic Proxy Properties
 */

#define G_LOG_DOMAIN "wp-props"

#include "props.h"
#include "debug.h"
#include "spa-type.h"
#include "wpenums.h"
#include <spa/param/param.h>

struct entry
{
  guint32 id;
  gchar *description;
  WpSpaPod *type;
  WpSpaPod *value;
};

struct entry *
entry_new (void)
{
  struct entry *e = g_slice_new0 (struct entry);
  return e;
}

static void
entry_free (struct entry *e)
{
  g_free (e->description);
  g_clear_pointer (&e->type, wp_spa_pod_unref);
  g_clear_pointer (&e->value, wp_spa_pod_unref);
  g_slice_free (struct entry, e);
}

struct _WpProps
{
  GObject parent;

  GWeakRef proxy;
  WpPropsMode mode;
  GList *entries;
};

enum {
  PROP_0,
  PROP_PROXY,
  PROP_MODE,
};

enum
{
  SIGNAL_PROP_CHANGED,
  LAST_SIGNAL,
};

static guint signals[LAST_SIGNAL] = { 0 };

/**
 * WpProps:
 *
 * #WpProps handles dynamic properties on PipeWire objects, which are
 * known in PipeWire as "params" of type `SPA_PARAM_Props`.
 *
 * #WpProps has two modes of operation:
 *  - %WP_PROPS_MODE_CACHE: In this mode, this object caches properties that are
 *    actually stored and discovered from the associated proxy object.
 *    When setting a property, the property is first set on the proxy and the
 *    cache is updated asynchronously (so wp_props_get() will not immediately
 *    return the value that was set with wp_props_set()).
 *  - %WP_PROPS_MODE_STORE: In this mode, this object is the actual store of
 *    properties. This is used by object implementations, such as #WpImplSession.
 *    Before storing anything, properties need to be registered with
 *    wp_props_register().
 */
G_DEFINE_TYPE (WpProps, wp_props, G_TYPE_OBJECT)

static void
wp_props_init (WpProps * self)
{
  g_weak_ref_init (&self->proxy, NULL);
}

static void
wp_props_finalize (GObject * object)
{
  WpProps * self = WP_PROPS (object);

  g_list_free_full (self->entries, (GDestroyNotify) entry_free);
  g_weak_ref_clear (&self->proxy);

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

static void
wp_props_set_property (GObject * object, guint property_id,
    const GValue * value, GParamSpec * pspec)
{
  WpProps *self = WP_PROPS (object);

  switch (property_id) {
  case PROP_PROXY:
    g_weak_ref_set (&self->proxy, g_value_get_object (value));
    break;
  case PROP_MODE:
    self->mode = g_value_get_enum (value);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
wp_props_get_property (GObject * object, guint property_id,
    GValue * value, GParamSpec * pspec)
{
  WpProps *self = WP_PROPS (object);

  switch (property_id) {
  case PROP_PROXY:
    g_value_take_object (value, g_weak_ref_get (&self->proxy));
    break;
  case PROP_MODE:
    g_value_set_enum (value, self->mode);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
wp_props_class_init (WpPropsClass * klass)
{
  GObjectClass * object_class = (GObjectClass *) klass;

  object_class->finalize = wp_props_finalize;
  object_class->set_property = wp_props_set_property;
  object_class->get_property = wp_props_get_property;

  g_object_class_install_property (object_class, PROP_PROXY,
      g_param_spec_object ("proxy", "proxy", "The proxy", WP_TYPE_PROXY,
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (object_class, PROP_MODE,
      g_param_spec_enum ("mode", "mode", "The mode",
          WP_TYPE_PROPS_MODE, WP_PROPS_MODE_CACHE,
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  /**
   * WpProps::prop-changed:
   * @self: the props
   * @name: the name of the property that changed
   */
  signals[SIGNAL_PROP_CHANGED] = g_signal_new (
      "prop-changed", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST,
      0, NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_STRING);
}

static struct entry *
find_entry (WpProps * self, const gchar * name)
{
  GList *l = self->entries;
  guint32 id;

  if (!wp_spa_type_get_by_nick (WP_SPA_TYPE_TABLE_PROPS, name, &id,
          NULL, NULL)) {
    wp_critical_object (self, "prop id name '%s' is not registered", name);
    return NULL;
  }

  while (l && ((struct entry *) l->data)->id != id)
    l = g_list_next (l);
  if (!l)
    return NULL;

  return (struct entry *) l->data;
}

/* public */

/**
 * wp_props_new:
 * @mode: the mode
 * @proxy: (transfer none) (nullable): the associated proxy; can be %NULL
 *   if @mode is %WP_PROPS_MODE_STORE
 *
 * Returns: (transfer full): the newly created #WpProps object
 */
WpProps *
wp_props_new (WpPropsMode mode, WpProxy * proxy)
{
  return g_object_new (WP_TYPE_PROPS, "mode", mode, "proxy", proxy, NULL);
}

/**
 * wp_props_register:
 * @self: the props
 * @name: the name (registered spa type nick) of the property
 * @description: the description of the property
 * @pod: (transfer full): a pod that gives the type and the default value
 *
 * Registers a new property. This can only be used in %WP_PROPS_MODE_STORE mode.
 *
 * @name must be a valid spa type nickname, registered in the
 * %WP_SPA_TYPE_TABLE_PROPS table.
 *
 * @pod can be a value (which is taken as the default value) or a choice
 * (which defines the allowed values for this property)
 */
void
wp_props_register (WpProps * self, const gchar * name,
    const gchar * description, WpSpaPod * pod)
{
  guint32 id;

  g_return_if_fail (self->mode == WP_PROPS_MODE_STORE);

  if (!wp_spa_type_get_by_nick (WP_SPA_TYPE_TABLE_PROPS, name, &id,
          NULL, NULL)) {
    wp_critical_object (self, "prop id name '%s' is not registered", name);
    return;
  }

  struct entry *e = entry_new ();
  e->id = id;
  e->description = g_strdup (description);
  e->type = pod;
  e->value = wp_spa_pod_is_choice (e->type) ?
      wp_spa_pod_get_choice_child (e->type) : wp_spa_pod_ref (e->type);
  self->entries = g_list_append (self->entries, e);
}

/**
 * wp_props_register_from_info:
 * @self: the props
 * @pod: (transfer full): a `SPA_TYPE_OBJECT_PropInfo` pod
 *
 * Registers a new property using the information of the provided PropInfo @pod
 */
void
wp_props_register_from_info (WpProps * self, WpSpaPod * pod)
{
  g_autoptr (WpSpaPod) prop_info = pod;
  guint32 id;
  const gchar *description;
  g_autoptr (WpSpaPod) type = NULL;

  if (!wp_spa_pod_get_object (prop_info,
        "PropInfo", NULL,
        "id", "I", &id,
        "name", "s", &description,
        "type", "P", &type,
        NULL)) {
    wp_warning_boxed (WP_TYPE_SPA_POD, prop_info, "bad prop info object");
    return;
  }

  struct entry *e = entry_new ();
  e->id = id;
  e->description = g_strdup (description);
  e->type = wp_spa_pod_ref (type);
  e->value = wp_spa_pod_is_choice (e->type) ?
      wp_spa_pod_get_choice_child (e->type) : wp_spa_pod_ref (e->type);
  self->entries = g_list_append (self->entries, e);
}

/**
 * wp_props_iterate_prop_info:
 * @self: the props
 *
 * Returns: (transfer full): a #WpIterator that iterates over #WpSpaPod items
 *   where each pod is an object of type `SPA_TYPE_OBJECT_PropInfo`, and thus
 *   contains the id, the description and the type of each property.
 */
WpIterator *
wp_props_iterate_prop_info (WpProps * self)
{
  g_autoptr (GPtrArray) res =
      g_ptr_array_new_with_free_func ((GDestroyNotify) wp_spa_pod_unref);

  g_return_val_if_fail (WP_IS_PROPS (self), NULL);

  for (GList *l = self->entries; l != NULL; l = g_list_next (l)) {
    struct entry * e = (struct entry *) l->data;
    g_ptr_array_add (res, wp_spa_pod_new_object (
        "PropInfo", "PropInfo",
        "id", "I", e->id,
        "name", "s", e->description,
        "type", "P", e->type,
        NULL));
  }

  return wp_iterator_new_ptr_array (g_steal_pointer (&res), WP_TYPE_SPA_POD);
}

/**
 * wp_props_get_all:
 * @self: the props
 *
 * Returns: (transfer full): a pod object of type `SPA_TYPE_OBJECT_Props`
 *   that contains all the properties, as they would appear on the PipeWire
 *   object
 */
WpSpaPod *
wp_props_get_all (WpProps * self)
{
  g_autoptr (WpSpaPodBuilder) b = NULL;

  g_return_val_if_fail (WP_IS_PROPS (self), NULL);

  b = wp_spa_pod_builder_new_object ("Props", "Props");
  for (GList *l = self->entries; l != NULL; l = g_list_next (l)) {
    struct entry * e = (struct entry *) l->data;
    if (e->id && e->value) {
      wp_spa_pod_builder_add_property_id (b, e->id);
      wp_spa_pod_builder_add_pod (b, e->value);
    }
  }

  return wp_spa_pod_builder_end (b);
}

/**
 * wp_props_get:
 * @self: the props
 * @name: the name (registered spa type nick) of the property to get
 *
 * Returns: (transfer full) (nullable): a pod with the current value of the
 *   property or %NULL if the property is not found
 */
WpSpaPod *
wp_props_get (WpProps * self, const gchar * name)
{
  struct entry * e;

  g_return_val_if_fail (WP_IS_PROPS (self), NULL);

  if (!(e = find_entry (self, name)))
    return NULL;
  return wp_spa_pod_ref (e->value);
}

static void
wp_props_set_on_proxy (WpProps * self, const gchar * name, WpSpaPod * pod)
{
  g_autoptr (WpSpaPod) val = pod;
  g_autoptr (WpProxy) proxy = g_weak_ref_get (&self->proxy);
  g_autoptr (WpSpaPod) param = NULL;

  g_return_if_fail (proxy != NULL);

  if (name) {
    param = wp_spa_pod_new_object (
        "Props", "Props",
        name, "P", val,
        NULL);
  } else {
    param = wp_spa_pod_ref (pod);
  }

  /* our store will be updated by the param event */
  wp_proxy_set_param (proxy, "Props", param);
}

static void
wp_props_store_single (WpProps * self, const gchar * name, WpSpaPod * pod)
{
  g_autoptr (WpSpaPod) val = pod;
  struct entry * e;

  if (!(e = find_entry (self, name))) {
    wp_warning_object (self, "prop '%s' is not registered", name);
    return;
  }

  wp_trace_object (self, "storing '%s', entry:%p", name, e);

  /* TODO check the type */

  if (!wp_spa_pod_equal (e->value, val)) {
    g_clear_pointer (&e->value, wp_spa_pod_unref);
    e->value = wp_spa_pod_ensure_unique_owner (g_steal_pointer (&val));
    g_signal_emit (self, signals[SIGNAL_PROP_CHANGED], 0, name);
  }
}

static void
wp_props_store_many (WpProps * self, WpSpaPod * pod)
{
  g_autoptr (WpSpaPod) props = pod;
  g_autoptr (WpIterator) it = NULL;
  g_auto (GValue) item = G_VALUE_INIT;

  for (it = wp_spa_pod_iterate (props);
       wp_iterator_next (it, &item);
       g_value_unset (&item)) {
    WpSpaPod *p = g_value_get_boxed (&item);
    const char *name = NULL;
    WpSpaPod *val = NULL;

    if (!wp_spa_pod_get_property (p, &name, &val)) {
      wp_warning_object (self, "failed to get property name & value");
      continue;
    }
    wp_props_store (self, name, val);
  }
}

/**
 * wp_props_set:
 * @self: the props
 * @name: (nullable): the name (registered spa type nick) of the property to set
 * @value: (transfer full): the value to set
 *
 * Sets the property specified with @name to have the given @value.
 * If the mode is %WP_PROPS_MODE_CACHE, this property will be set on the
 * associated proxy first and will be updated asynchronously.
 *
 * If @name is %NULL, then @value must be an object of type
 * `SPA_TYPE_OBJECT_Props`, which may contain multiple properties to set.
 *
 * If any value actually changes, the #WpProps::prop-changed signal will be
 * emitted.
 */
void
wp_props_set (WpProps * self, const gchar * name, WpSpaPod * value)
{
  g_return_if_fail (WP_IS_PROPS (self));
  g_return_if_fail (value != NULL);

  switch (self->mode) {
    case WP_PROPS_MODE_CACHE:
      wp_props_set_on_proxy (self, name, value);
      break;
    case WP_PROPS_MODE_STORE:
      if (name)
        wp_props_store_single (self, name, value);
      else
        wp_props_store_many (self, value);
      break;
    default:
      g_return_if_reached ();
  }
}

/**
 * wp_props_store:
 * @self: the props
 * @name: (nullable): the name (registered spa type nick) of the property to set
 * @value: (transfer full): the value to set
 *
 * Stores the given @value for the property specified with @name.
 * This method always stores, even if the mode is %WP_PROPS_MODE_CACHE. This is
 * useful for caching implementations only.
 *
 * If @name is %NULL, then @value must be an object of type
 * `SPA_TYPE_OBJECT_Props`, which may contain multiple properties to set.
 *
 * If any value actually changes, the #WpProps::prop-changed signal will be
 * emitted.
 */
void
wp_props_store (WpProps * self, const gchar * name, WpSpaPod * value)
{
  g_return_if_fail (WP_IS_PROPS (self));
  g_return_if_fail (value != NULL);

  if (name)
    wp_props_store_single (self, name, value);
  else
    wp_props_store_many (self, value);
}