/* WirePlumber
 *
 * Copyright © 2022 Collabora Ltd.
 *    @author Ashok Sidipotu <ashok.sidipotu@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include "core.h"
#include "settings.h"
#include "metadata.h"
#include "log.h"
#include "object-manager.h"

WP_DEFINE_LOCAL_LOG_TOPIC ("wp-settings")

/*! \defgroup wpsettings WpSettings */

/*!
 * \struct WpSettingsSpec
 *
 * WpSettingSpec holds the specification of a setting.
 */
struct _WpSettingsSpec {
  grefcount ref;
  gchar *desc;
  WpSettingsSpecType type;
  WpSpaJson *def_value;
  WpSpaJson *min_value;
  WpSpaJson *max_value;
};

G_DEFINE_BOXED_TYPE (WpSettingsSpec, wp_settings_spec, wp_settings_spec_ref,
    wp_settings_spec_unref)

/*!
 * \brief Increases the reference count of a settings spec object
 * \ingroup wpsettings
 * \param self a settings spec object
 * \returns (transfer full): \a self with an additional reference count on it
 */
WpSettingsSpec *
wp_settings_spec_ref (WpSettingsSpec * self)
{
  g_ref_count_inc (&self->ref);
  return self;
}

static void
wp_settings_spec_free (WpSettingsSpec * self)
{
  g_clear_pointer (&self->desc, g_free);
  g_clear_pointer (&self->def_value, wp_spa_json_unref);
  g_clear_pointer (&self->min_value, wp_spa_json_unref);
  g_clear_pointer (&self->max_value, wp_spa_json_unref);
  g_slice_free (WpSettingsSpec, self);
}

/*!
 * \brief Decreases the reference count on \a self and frees it when the ref
 * count reaches zero.
 * \ingroup wpsettings
 * \param self (transfer full): a settings spec object
 */
void
wp_settings_spec_unref (WpSettingsSpec * self)
{
  if (g_ref_count_dec (&self->ref))
    wp_settings_spec_free (self);
}

static WpSettingsSpec *
wp_settings_spec_new (WpSpaJson * spec_json)
{
  WpSettingsSpec *self;
  g_autofree gchar *desc = NULL;
  g_autofree gchar *type_str = NULL;
  WpSettingsSpecType type = WP_SETTINGS_SPEC_TYPE_UNKNOWN;
  g_autoptr (WpSpaJson) def_value = NULL;
  g_autoptr (WpSpaJson) min_value = NULL;
  g_autoptr (WpSpaJson) max_value = NULL;

  g_return_val_if_fail (spec_json, NULL);

  if (!wp_spa_json_is_object (spec_json))
    return NULL;

  /* Parse mandatory fields */
  if (!wp_spa_json_object_get (spec_json,
      "description", "s", &desc,
      "type", "s", &type_str,
      "default", "J", &def_value,
      NULL))
    return NULL;

  /* Parse type and check if values are correct */
  if (g_str_equal (type_str, "bool")) {
    type = WP_SETTINGS_SPEC_TYPE_BOOL;
    if (!wp_spa_json_is_boolean (def_value))
      return NULL;
  } else if (g_str_equal (type_str, "int")) {
    type = WP_SETTINGS_SPEC_TYPE_INT;
    if (!wp_spa_json_object_get (spec_json,
        "min", "J", &min_value,
        "max", "J", &max_value,
        NULL))
      return NULL;
    if (!wp_spa_json_is_int (def_value) ||
        !min_value || !wp_spa_json_is_int (min_value) ||
        !max_value || !wp_spa_json_is_int (max_value))
      return NULL;
  } else if (g_str_equal (type_str, "float")) {
    type = WP_SETTINGS_SPEC_TYPE_FLOAT;
    if (!wp_spa_json_object_get (spec_json,
        "min", "J", &min_value,
        "max", "J", &max_value,
        NULL))
      return NULL;
    if (!wp_spa_json_is_float (def_value) ||
        !min_value || !wp_spa_json_is_float (min_value) ||
        !max_value || !wp_spa_json_is_float (max_value))
      return NULL;
  } else if (g_str_equal (type_str, "string")) {
    type = WP_SETTINGS_SPEC_TYPE_STRING;
  } else if (g_str_equal (type_str, "array")) {
    type = WP_SETTINGS_SPEC_TYPE_ARRAY;
    if (!wp_spa_json_is_array (def_value))
      return NULL;
  } else if (g_str_equal (type_str, "object")) {
    type = WP_SETTINGS_SPEC_TYPE_OBJECT;
    if (!wp_spa_json_is_object (def_value))
      return NULL;
  } else {
    return NULL;
  }

  self = g_slice_new0 (WpSettingsSpec);
  g_ref_count_init (&self->ref);
  self->desc = g_steal_pointer (&desc);
  self->type = type;
  self->def_value = g_steal_pointer (&def_value);
  self->min_value = g_steal_pointer (&min_value);
  self->max_value = g_steal_pointer (&max_value);
  return self;
}

/*!
 * \brief Gets the description of a settings spec
 * \ingroup wpsettings
 * \param self the settings spec object
 * \returns the description of the settings spec
 */
const gchar *
wp_settings_spec_get_description (WpSettingsSpec * self)
{
  g_return_val_if_fail (self, NULL);
  return self->desc;
}

/*!
 * \brief Gets the type of a settings spec
 * \ingroup wpsettings
 * \param self the settings spec object
 * \returns the type of the settings spec
 */
WpSettingsSpecType
wp_settings_spec_get_value_type (WpSettingsSpec * self)
{
  g_return_val_if_fail (self, WP_SETTINGS_SPEC_TYPE_UNKNOWN);
  return self->type;
}

/*!
 * \brief Gets the default value of a settings spec
 * \ingroup wpsettings
 * \param self the settings spec object
 * \returns (transfer full): the default value of the settings spec
 */
WpSpaJson *
wp_settings_spec_get_default_value (WpSettingsSpec * self)
{
  g_return_val_if_fail (self, NULL);
  g_return_val_if_fail (self->def_value, NULL);
  return wp_spa_json_ref (self->def_value);
}

/*!
 * \brief Gets the minimum value of a settings spec.
 * \ingroup wpsettings
 * \param self the settings spec object
 * \returns (transfer full)(nullable): the minimum value of the settings spec,
 * or NULL if the spec type is not WP_SETTINGS_SPEC_TYPE_INT or
 * WP_SETTINGS_SPEC_TYPE_FLOAT
 */
WpSpaJson *
wp_settings_spec_get_min_value (WpSettingsSpec * self)
{
  g_return_val_if_fail (self, NULL);
  return self->min_value ? wp_spa_json_ref (self->min_value) : NULL;
}

/*!
 * \brief Gets the maximum value of a settings spec.
 * \ingroup wpsettings
 * \param self the settings spec object
 * \returns (transfer full)(nullable): the maximum value of the settings spec,
 * or NULL if the spec type is not WP_SETTINGS_SPEC_TYPE_INT or
 * WP_SETTINGS_SPEC_TYPE_FLOAT
 */
WpSpaJson *
wp_settings_spec_get_max_value (WpSettingsSpec * self)
{
  g_return_val_if_fail (self, NULL);
  return self->max_value ? wp_spa_json_ref (self->max_value) : NULL;
}

/*!
 * \brief Checks whether a value is compatible with the spec or not
 * \ingroup wpsettings
 * \param self the settings spec object
 * \param value (transfer none): the value to check
 * \returns TRUE if the value is compatible with the spec, FALSE otherwise
 */
gboolean
wp_settings_spec_check_value (WpSettingsSpec * self, WpSpaJson *value)
{
  g_return_val_if_fail (self, FALSE);
  g_return_val_if_fail (value, FALSE);

  switch (self->type) {
    case WP_SETTINGS_SPEC_TYPE_BOOL:
      return wp_spa_json_is_boolean (value);
    case WP_SETTINGS_SPEC_TYPE_INT: {
      gint val = 0, min = 0, max = 0;
      if (!wp_spa_json_is_int (value) || !wp_spa_json_parse_int (value, &val))
        return FALSE;
      if (!wp_spa_json_parse_int (self->min_value, &min) ||
          !wp_spa_json_parse_int (self->max_value, &max))
        return FALSE;
      return val >= min && val <= max;
    }
    case WP_SETTINGS_SPEC_TYPE_FLOAT: {
      float val = 0.0, min = 0.0, max = 0.0;
      if (wp_spa_json_is_int (value) || !wp_spa_json_is_float (value) ||
          !wp_spa_json_parse_float (value, &val))
        return FALSE;
      if (!wp_spa_json_parse_float (self->min_value, &min) ||
          !wp_spa_json_parse_float (self->max_value, &max))
        return FALSE;
      return val >= min && val <= max;
    }
    case WP_SETTINGS_SPEC_TYPE_STRING:
      /* We also accept strings without quotes, which is why we dont use
       * wp_spa_json_is_string() */
      return !wp_spa_json_is_boolean (value) && !wp_spa_json_is_int (value) &&
        !wp_spa_json_is_float (value) && !wp_spa_json_is_array (value) &&
        !wp_spa_json_is_object (value);
    case WP_SETTINGS_SPEC_TYPE_ARRAY:
      return wp_spa_json_is_array (value);
    case WP_SETTINGS_SPEC_TYPE_OBJECT:
      return wp_spa_json_is_object (value);
    default:
      break;
  }

  return FALSE;
}

/*!
 * \struct WpSettingsItem
 *
 * WpSettingsItem holds the key and value of a setting
 */
struct _WpSettingsItem
{
  gchar *key;
  WpSpaJson *value;
};

G_DEFINE_BOXED_TYPE (WpSettingsItem, wp_settings_item,
    wp_settings_item_ref, wp_settings_item_unref)

static WpSettingsItem *
wp_settings_item_new (const gchar *key, const gchar *value)
{
  WpSettingsItem *self = g_rc_box_new0 (WpSettingsItem);
  self->key = g_strdup (key);
  self->value = wp_spa_json_new_from_string (value);
  return self;
}

static void
wp_settings_item_free (gpointer p)
{
  WpSettingsItem *self = p;
  g_clear_pointer (&self->key, g_free);
  g_clear_pointer (&self->value, wp_spa_json_unref);
}

/*!
 * \brief Increases the reference count of a settings item object
 * \ingroup wpsettings
 * \param self a settings item object
 * \returns (transfer full): \a self with an additional reference count on it
 */
WpSettingsItem *
wp_settings_item_ref (WpSettingsItem *self)
{
  return g_rc_box_acquire (self);
}

/*!
 * \brief Decreases the reference count on \a self and frees it when the ref
 * count reaches zero.
 * \ingroup wpsettings
 * \param self (transfer full): a settings item object
 */
void
wp_settings_item_unref (WpSettingsItem *self)
{
  g_rc_box_release_full (self, wp_settings_item_free);
}

/*!
 * \brief Gets the key from a settings item
 *
 * \ingroup wpsettings
 * \param self the item held by the GValue that was returned from the WpIterator
 *   of wp_settings_new_iterator()
 * \returns (transfer none): the settings key of the \a item
 */
const gchar *
wp_settings_item_get_key (WpSettingsItem * self)
{
  return self->key;
}

/*!
 * \brief Gets the value from a settings item
 *
 * \ingroup wpsettings
 * \param self the item held by the GValue that was returned from the WpIterator
 *   of wp_settings_new_iterator()
 * \returns (transfer full): the settings value of the \a item
 */
WpSpaJson *
wp_settings_item_get_value (WpSettingsItem * self)
{
  return wp_spa_json_ref (self->value);
}

/*!
 * \struct WpSettings
 *
 * WpSettings loads and parses the "sm-settings" (default value) metadata, which
 * contains wireplumber settings, and provides APIs to its clients (modules, lua
 * scripts etc) to access them.
 *
 * Being a WpObject subclass, the settings inherits WpObject's activation
 * system.
 */

struct _WpSettings
{
  WpObject parent;

  /* element-type: Callback* */
  GPtrArray *callbacks;

  gchar *metadata_name;
  gchar *metadata_schema_name;
  gchar *metadata_persistent_name;

  WpObjectManager *metadata_om;
  GWeakRef metadata;
  GWeakRef metadata_schema;
  GWeakRef metadata_persistent;

  /* We keep a hash table with all the settings info to make sure their values
   * are always updated when using the settings API. This avoids syncing issues
   * if pipewire has not finished syncing the metadata objects yet */
  GHashTable *schema;
  GHashTable *settings;
  GHashTable *saved_settings;
};

typedef struct
{
  GClosure *closure;
  gchar *pattern;
} Callback;

enum {
  PROP_0,
  PROP_METADATA_NAME,
  PROP_PROPERTIES,
};

G_DEFINE_TYPE (WpSettings, wp_settings, WP_TYPE_OBJECT)

static void
wp_settings_init (WpSettings * self)
{
  g_weak_ref_init (&self->metadata, NULL);
  g_weak_ref_init (&self->metadata_schema, NULL);
  g_weak_ref_init (&self->metadata_persistent, NULL);

  self->schema = g_hash_table_new_full (g_str_hash, g_str_equal, g_free,
      (GDestroyNotify) wp_settings_spec_unref);
  self->settings = g_hash_table_new_full (g_str_hash, g_str_equal, g_free,
      (GDestroyNotify) g_free);
  self->saved_settings = g_hash_table_new_full (g_str_hash, g_str_equal, g_free,
      (GDestroyNotify) g_free);
}

static void
wp_settings_set_property (GObject * object, guint property_id,
    const GValue * value, GParamSpec * pspec)
{
  WpSettings *self = WP_SETTINGS (object);

  switch (property_id) {
  case PROP_METADATA_NAME:
    self->metadata_name = g_value_dup_string (value);
    self->metadata_schema_name = g_strdup_printf (
        WP_SETTINGS_SCHEMA_METADATA_NAME_PREFIX "%s", self->metadata_name);
    self->metadata_persistent_name = g_strdup_printf (
        WP_SETTINGS_PERSISTENT_METADATA_NAME_PREFIX "%s", self->metadata_name);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
wp_settings_get_property (GObject * object, guint property_id,
    GValue * value, GParamSpec * pspec)
{
  WpSettings *self = WP_SETTINGS (object);

  switch (property_id) {
  case PROP_METADATA_NAME:
    g_value_set_string (value, self->metadata_name);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

enum {
  STEP_LOAD = WP_TRANSITION_STEP_CUSTOM_START,
};

static WpObjectFeatures
wp_settings_get_supported_features (WpObject * self)
{
  return WP_SETTINGS_LOADED;
}

static guint
wp_settings_activate_get_next_step (WpObject * object,
    WpFeatureActivationTransition * transition, guint step,
    WpObjectFeatures missing)
{
  g_return_val_if_fail (missing == WP_SETTINGS_LOADED,
      WP_TRANSITION_STEP_ERROR);

  return STEP_LOAD;
}

static void
settings_notify_changed (WpSettings *self, const gchar *key, const gchar *value)
{
  for (guint i = 0; i < self->callbacks->len; i++) {
    Callback *cb = g_ptr_array_index (self->callbacks, i);

    if (g_pattern_match_simple (cb->pattern, key)) {
      g_autoptr (WpSpaJson) json = NULL;
      GValue values[3] = { G_VALUE_INIT, G_VALUE_INIT, G_VALUE_INIT };

      g_value_init (&values[0], G_TYPE_OBJECT);
      g_value_init (&values[1], G_TYPE_STRING);
      g_value_init (&values[2], WP_TYPE_SPA_JSON);

      g_value_set_object (&values[0], self);
      g_value_set_string (&values[1], key);
      json = value ? wp_spa_json_new_wrap_string (value) : NULL;
      g_value_set_boxed (&values[2], json);

      g_closure_invoke (cb->closure, NULL, 3, values, NULL);

      g_value_unset (&values[0]);
      g_value_unset (&values[1]);
      g_value_unset (&values[2]);
    }
  }
}

static void
settings_notify_changed_cb (gpointer key, gpointer value, gpointer data)
{
  WpSettings *self = WP_SETTINGS (data);
  const gchar *k = key;
  const gchar *v = value;

  settings_notify_changed (self, k, v);
}

static void
on_metadata_changed (WpMetadata *m, guint32 subject,
   const gchar *key, const gchar *type, const gchar *value, gpointer d)
{
  WpSettings *self = WP_SETTINGS (d);

  if (key) {
    if (value) {
      g_hash_table_insert (self->settings, g_strdup (key), g_strdup (value));
      wp_info_object (self, "setting \"%s\" changed to \"%s\"", key, value);
    } else {
      g_hash_table_remove (self->settings, key);
      wp_info_object (self, "setting \"%s\" removed", key);
    }

    settings_notify_changed (self, key, value);
  } else {
    wp_info_object (self, "all settings removed");
    g_hash_table_foreach (self->settings, settings_notify_changed_cb, self);
    g_hash_table_remove_all (self->settings);
  }
}

static void
on_metadata_persistent_changed (WpMetadata *m, guint32 subject,
   const gchar *key, const gchar *type, const gchar *value, gpointer d)
{
  WpSettings *self = WP_SETTINGS (d);

  if (key) {
    if (value) {
      g_hash_table_insert (self->saved_settings, g_strdup (key),
          g_strdup (value));
      wp_info_object (self, "saved setting \"%s\" changed to \"%s\"", key,
          value);
    } else {
      g_hash_table_remove (self->saved_settings, key);
      wp_info_object (self, "saved setting \"%s\" removed", key);
    }
  } else {
    wp_info_object (self, "all saved settings removed");
    g_hash_table_remove_all (self->saved_settings);
  }
}

static void
on_metadata_added (WpObjectManager *om, WpMetadata *m, gpointer d)
{
  WpTransition * transition = WP_TRANSITION (d);
  WpSettings * self = wp_transition_get_source_object (transition);
  g_autoptr (WpProperties) props = NULL;
  const gchar *metadata_name = NULL;
  g_autoptr (WpMetadata) metadata = NULL;
  g_autoptr (WpMetadata) metadata_schema = NULL;
  g_autoptr (WpMetadata) metadata_persistent = NULL;

  /* make sure the metadata has a name */
  props = wp_global_proxy_get_global_properties (WP_GLOBAL_PROXY (m));
  if (props)
    metadata_name = wp_properties_get (props, "metadata.name");
  if (!metadata_name)
    return;

  /* sm-settings */
  if (g_str_equal (metadata_name, self->metadata_name)) {
    g_autoptr (WpIterator) it = NULL;
    g_auto (GValue) item = G_VALUE_INIT;
    it = wp_metadata_new_iterator (m, 0);
    for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
      WpMetadataItem *mi = g_value_get_boxed (&item);
      const gchar *key = wp_metadata_item_get_key (mi);
      const gchar *value = wp_metadata_item_get_value (mi);
      g_hash_table_insert (self->settings, g_strdup (key), g_strdup (value));
    }
    g_signal_connect_object (m, "changed", G_CALLBACK (on_metadata_changed),
        self, 0);
    g_weak_ref_set (&self->metadata, m);
  }

  /* schema-sm-settings */
  else if (g_str_equal (metadata_name, self->metadata_schema_name)) {
    g_autoptr (WpIterator) it = NULL;
    g_auto (GValue) item = G_VALUE_INIT;
    it = wp_metadata_new_iterator (m, 0);
    for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
      WpMetadataItem *mi = g_value_get_boxed (&item);
      const gchar *key = wp_metadata_item_get_key (mi);
      const gchar *value = wp_metadata_item_get_value (mi);
      g_autoptr (WpSpaJson) spec_json = NULL;
      g_autoptr (WpSettingsSpec) spec = NULL;
      spec_json = wp_spa_json_new_from_string (value);
      spec = wp_settings_spec_new (spec_json);
      if (spec)
        g_hash_table_insert (self->schema, g_strdup (key),
            g_steal_pointer (&spec));
      else
        wp_warning_object (self, "malformed setting spec: %s", value);
    }
    g_weak_ref_set (&self->metadata_schema, m);
  }

  /* presistent-sm-settings */
  else if (g_str_equal (metadata_name, self->metadata_persistent_name)) {
    g_autoptr (WpIterator) it = NULL;
    g_auto (GValue) item = G_VALUE_INIT;
    it = wp_metadata_new_iterator (m, 0);
    for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
      WpMetadataItem *mi = g_value_get_boxed (&item);
      const gchar *key = wp_metadata_item_get_key (mi);
      const gchar *value = wp_metadata_item_get_value (mi);
      g_hash_table_insert (self->saved_settings, g_strdup (key),
          g_strdup (value));
    }
    g_signal_connect_object (m, "changed",
        G_CALLBACK (on_metadata_persistent_changed), self, 0);
    g_weak_ref_set (&self->metadata_persistent, m);
  }

  /* Finish loading when all metadatas are found */
  metadata = g_weak_ref_get (&self->metadata);
  metadata_schema = g_weak_ref_get (&self->metadata_schema);
  metadata_persistent = g_weak_ref_get (&self->metadata_persistent);
  if (metadata && metadata_schema && metadata_persistent)
    wp_object_update_features (WP_OBJECT (self), WP_SETTINGS_LOADED, 0);
}

static void
callback_unref (Callback * self)
{
  g_free (self->pattern);
  g_clear_pointer (&self->closure, g_closure_unref);
  g_slice_free (Callback, self);
}

static void
wp_settings_activate_execute_step (WpObject * object,
    WpFeatureActivationTransition * transition, guint step,
    WpObjectFeatures missing)
{
  WpSettings * self = WP_SETTINGS (object);
  g_autoptr (WpCore) core = wp_object_get_core (object);

  switch (step) {
  case STEP_LOAD: {
    self->callbacks = g_ptr_array_new_with_free_func
        ((GDestroyNotify) callback_unref);

    self->metadata_om = wp_object_manager_new ();
    wp_object_manager_add_interest (self->metadata_om, WP_TYPE_METADATA,
        WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "metadata.name", "=s",
            self->metadata_name, NULL);
    wp_object_manager_add_interest (self->metadata_om, WP_TYPE_METADATA,
        WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "metadata.name", "=s",
            self->metadata_schema_name, NULL);
    wp_object_manager_add_interest (self->metadata_om, WP_TYPE_METADATA,
        WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "metadata.name", "=s",
            self->metadata_persistent_name, NULL);
    wp_object_manager_request_object_features (self->metadata_om,
        WP_TYPE_METADATA, WP_OBJECT_FEATURES_ALL);
    g_signal_connect_object (self->metadata_om, "object-added",
        G_CALLBACK (on_metadata_added), transition, 0);
    wp_core_install_object_manager (core, self->metadata_om);

    wp_info_object (self, "looking for metadata object named %s",
        self->metadata_name);
    break;
  }
  case WP_TRANSITION_STEP_ERROR:
    break;
  default:
    g_assert_not_reached ();
  }
}

static void
wp_settings_deactivate (WpObject * object, WpObjectFeatures features)
{
  WpSettings *self = WP_SETTINGS (object);

  g_clear_object (&self->metadata_om);
  g_clear_pointer (&self->callbacks, g_ptr_array_unref);

  wp_object_update_features (WP_OBJECT (self), 0, WP_OBJECT_FEATURES_ALL);
}

static void
wp_settings_finalize (GObject * object)
{
  WpSettings *self = WP_SETTINGS (object);

  g_clear_pointer (&self->metadata_name, g_free);
  g_clear_pointer (&self->metadata_schema_name, g_free);
  g_clear_pointer (&self->metadata_persistent_name, g_free);

  g_clear_pointer (&self->schema, g_hash_table_unref);
  g_clear_pointer (&self->settings, g_hash_table_unref);
  g_clear_pointer (&self->saved_settings, g_hash_table_unref);

  g_weak_ref_clear (&self->metadata);
  g_weak_ref_clear (&self->metadata_schema);
  g_weak_ref_clear (&self->metadata_persistent);

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

static void
wp_settings_class_init (WpSettingsClass * klass)
{
  GObjectClass * object_class = (GObjectClass *) klass;
  WpObjectClass * wpobject_class = (WpObjectClass *) klass;

  object_class->finalize = wp_settings_finalize;
  object_class->set_property = wp_settings_set_property;
  object_class->get_property = wp_settings_get_property;

  wpobject_class->get_supported_features = wp_settings_get_supported_features;
  wpobject_class->activate_get_next_step = wp_settings_activate_get_next_step;
  wpobject_class->activate_execute_step = wp_settings_activate_execute_step;
  wpobject_class->deactivate = wp_settings_deactivate;

  g_object_class_install_property (object_class, PROP_METADATA_NAME,
      g_param_spec_string ("metadata-name", "metadata-name",
          "The metadata object to look after", NULL,
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
}

/*!
 * \brief Creates a new WpSettings object
 *
 * \ingroup wpsettings
 * \param core the WpCore
 * \param metadata_name (nullable): the name of the metadata object to
 *    associate with the settings object; NULL means the default "sm-settings"
 * \returns (transfer full): a new WpSettings object
 */
WpSettings *
wp_settings_new (WpCore * core, const gchar * metadata_name)
{
  return g_object_new (WP_TYPE_SETTINGS,
      "core", core,
      "metadata-name", metadata_name ? metadata_name : "sm-settings",
      NULL);
}

static gboolean
find_settings_func (gpointer g_object, gpointer metadata_name)
{
  if (!WP_IS_SETTINGS (g_object))
    return FALSE;

  if (metadata_name)
    return g_str_equal (((WpSettings *) g_object)->metadata_name,
        (gchar *) metadata_name);
  else
    return TRUE;
}

/*!
 * \brief Finds a registered WpSettings object by its metadata name
 *
 * \ingroup wpsettings
 * \param core the WpCore
 * \param metadata_name (nullable): the name of the metadata object that the
 *    settings object is associated with; NULL returns the first settings object
 *    that is found
 * \returns (transfer full) (nullable): the WpSettings object, or NULL if not
 *    found
 */
WpSettings *
wp_settings_find (WpCore * core, const gchar * metadata_name)
{
  g_return_val_if_fail (WP_IS_CORE (core), NULL);

  GObject *s = wp_core_find_object (core, (GEqualFunc) find_settings_func,
      metadata_name);
  return s ? WP_SETTINGS (s) : NULL;
}

/*!
 * \brief Subscribes callback for a given setting pattern(a glob-style pattern
 * matched using g_pattern_match_simple), this allows clients to look
 * for any changes made in settings through metadata.
 *
 * \ingroup wpsettings
 * \param self the settings object
 * \param pattern name of the pattern to match the settings with
 * \param callback (scope async): the callback triggered when the settings
 *  change.
 * \param user_data data to pass to \a callback
 * \returns the subscription ID (always greater than 0 for successful
 *  subscriptions)
 */
guintptr
wp_settings_subscribe (WpSettings *self,
    const gchar *pattern, WpSettingsChangedCallback callback,
    gpointer user_data)
{
  return wp_settings_subscribe_closure (self, pattern,
      g_cclosure_new (G_CALLBACK (callback), user_data, NULL));
}

/*!
 * \brief Subscribes callback for a given setting pattern(a glob-style pattern
 * matched using g_pattern_match_simple), this allows clients to look
 * for any changes made in settings through metadata.
 *
 * \ingroup wpsettings
 * \param self the settings object
 * \param pattern name of the pattern to match the settings with
 * \param closure (nullable): a GAsyncReadyCallback wrapped in a GClosure
 * \returns the subscription ID (always greater than 0 for success)
 */
guintptr
wp_settings_subscribe_closure (WpSettings *self, const gchar *pattern,
    GClosure *closure)
{
  g_return_val_if_fail (WP_IS_SETTINGS (self), 0);
  g_return_val_if_fail (pattern, 0);
  g_return_val_if_fail (closure, 0);

  Callback *cb = g_slice_new0 (Callback);
  g_return_val_if_fail (cb, 0);

  cb->closure = g_closure_ref (closure);
  g_closure_sink (closure);
  if (G_CLOSURE_NEEDS_MARSHAL (closure))
    g_closure_set_marshal (closure, g_cclosure_marshal_generic);

  cb->pattern = g_strdup (pattern);

  g_ptr_array_add (self->callbacks, cb);

  wp_debug_object (self, "callback(%p) subscribed for pattern(%s)",
      (void *) cb, pattern);

  return (guintptr) cb;
}

/*!
 * \brief Unsubscribes callback for a given subscription_id.
 *
 * \ingroup wpsettings
 * \param self the settings object
 * \param subscription_id identifies the callback
 * \returns TRUE if success, FALSE otherwise
 */
gboolean
wp_settings_unsubscribe (WpSettings *self, guintptr subscription_id)
{
  gboolean ret = FALSE;
  g_return_val_if_fail (WP_IS_SETTINGS (self), FALSE);
  g_return_val_if_fail (subscription_id, FALSE);

  Callback *cb = (Callback *) subscription_id;

  ret = g_ptr_array_remove (self->callbacks, cb);

  wp_debug_object (self, "callback(%p) unsubscription %s", (void *) cb,
      (ret)? "succeeded": "failed");

  return ret;
}

/*!
 * \brief Gets the WpSpaJson value of a setting
 * \ingroup wpsettings
 * \param self the settings object
 * \param name the name of the setting
 * \returns (transfer full) (nullable): The WpSpaJson value of the setting, or
 * NULL if the setting does not exist
 */
WpSpaJson *
wp_settings_get (WpSettings *self, const gchar *name)
{
  const gchar *value;
  g_autoptr (WpSettingsSpec) spec = NULL;

  g_return_val_if_fail (WP_IS_SETTINGS (self), NULL);
  g_return_val_if_fail (name, NULL);

  spec = wp_settings_get_spec (self, name);
  if (!spec) {
    wp_warning ("Setting '%s' does not exist in the settings schema", name);
    return NULL;
  }

  value = g_hash_table_lookup (self->settings, name);
  return value ? wp_spa_json_new_wrap_string (value) :
      wp_settings_spec_get_default_value (spec);
}

/*!
 * \brief Gets the WpSpaJson saved value of a setting
 * \ingroup wpsettings
 * \param self the settings object
 * \param name the name of the setting
 * \returns (transfer full) (nullable): The WpSpaJson saved value of the
 * setting, or NULL if the setting does not exist
 */
WpSpaJson *
wp_settings_get_saved (WpSettings *self, const gchar *name)
{
  const gchar *value;
  g_autoptr (WpSettingsSpec) spec = NULL;

  g_return_val_if_fail (WP_IS_SETTINGS (self), NULL);
  g_return_val_if_fail (name, NULL);

  spec = wp_settings_get_spec (self, name);
  if (!spec) {
    wp_warning ("Setting '%s' does not exist in the settings schema", name);
    return NULL;
  }

  value = g_hash_table_lookup (self->saved_settings, name);
  return value ? wp_spa_json_new_wrap_string (value) : NULL;
}

/*!
 * \brief Gets the specification of a setting
 * \ingroup wpsettings
 * \param self the settings object
 * \param name the name of the setting
 * \returns (transfer full) (nullable): the specification of the setting
 */
WpSettingsSpec *
wp_settings_get_spec (WpSettings *self, const gchar *name)
{
  WpSettingsSpec *spec;

  g_return_val_if_fail (WP_IS_SETTINGS (self), NULL);
  g_return_val_if_fail (name, NULL);

  spec = g_hash_table_lookup (self->schema, name);
  return spec ? wp_settings_spec_ref (spec) : NULL;
}

/*!
 * \brief Sets a new setting value
 * \ingroup wpsettings
 * \param self the settings object
 * \param name the name of the setting
 * \param value (transfer none): the JSON value of the setting
 * \returns TRUE if the setting could be set, FALSE otherwise
 */
gboolean
wp_settings_set (WpSettings *self, const gchar *name, WpSpaJson *value)
{
  g_autoptr (WpMetadata) m = NULL;
  g_autoptr (WpSettingsSpec) spec = NULL;
  g_autofree gchar *value_str = NULL;

  g_return_val_if_fail (WP_IS_SETTINGS (self), FALSE);
  g_return_val_if_fail (name, FALSE);
  g_return_val_if_fail (value, FALSE);

  m = g_weak_ref_get (&self->metadata);
  if (!m)
    return FALSE;

  spec = wp_settings_get_spec (self, name);
  if (!spec) {
    wp_warning ("Setting '%s' does not exist in the settings schema", name);
    return FALSE;
  }

  value_str = wp_spa_json_to_string (value);
  if (!wp_settings_spec_check_value (spec, value)) {
    wp_warning ("Cannot set setting '%s' with value: %s", name, value_str);
    return FALSE;
  }

  g_hash_table_insert (self->settings, g_strdup (name), g_strdup (value_str));
  wp_metadata_set (m, 0, name, "Spa:String:JSON", value_str);
  return TRUE;
}

/*!
 * \brief Resets the setting to its default value
 * \ingroup wpsettings
 * \param self the settings object
 * \param name the name of the setting to reset
 * \returns TRUE if the setting could be reset, FALSE otherwise
 */
gboolean
wp_settings_reset (WpSettings *self, const char *name)
{
  g_autoptr (WpSettingsSpec) spec = NULL;
  g_autoptr (WpSpaJson) def_value = NULL;

  g_return_val_if_fail (WP_IS_SETTINGS (self), FALSE);
  g_return_val_if_fail (name, FALSE);

  spec = wp_settings_get_spec (self, name);
  if (!spec) {
    wp_warning ("Setting '%s' does not exist in the settings schema", name);
    return FALSE;
  }

  def_value = wp_settings_spec_get_default_value (spec);
  return wp_settings_set (self, name, def_value);
}

/*!
 * \brief Saves a setting to make it persistent after reboot
 * \ingroup wpsettings
 * \param self the settings object
 * \param name the name of the setting to be saved
 * \returns TRUE if the setting could be saved, FALSE otherwise
 */
gboolean
wp_settings_save (WpSettings *self, const char *name)
{
  g_autoptr (WpMetadata) mp = NULL;
  g_autoptr (WpSpaJson) value = NULL;
  g_autofree gchar *value_str = NULL;

  g_return_val_if_fail (WP_IS_SETTINGS (self), FALSE);
  g_return_val_if_fail (name, FALSE);

  mp = g_weak_ref_get (&self->metadata_persistent);
  if (!mp)
    return FALSE;

  value = wp_settings_get (self, name);
  if (!value)
    return FALSE;

  value_str = wp_spa_json_to_string (value);
  g_hash_table_insert (self->saved_settings, g_strdup (name),
      g_strdup (value_str));
  wp_metadata_set (mp, 0, name, "Spa:String:JSON", value_str);
  return TRUE;
}

/*!
 * \brief Deletes a saved setting to not make it persistent after reboot
 * \ingroup wpsettings
 * \param self the settings object
 * \param name the name of the saved setting to be deleted
 * \returns TRUE if the setting could be deleted, FALSE otherwise
 */
gboolean
wp_settings_delete (WpSettings *self, const char *name)
{
  g_autoptr (WpMetadata) mp = NULL;
  g_autoptr (WpSettingsSpec) spec = NULL;

  g_return_val_if_fail (WP_IS_SETTINGS (self), FALSE);
  g_return_val_if_fail (name, FALSE);

  spec = wp_settings_get_spec (self, name);
  if (!spec) {
    wp_warning ("Setting '%s' does not exist in the settings schema", name);
    return FALSE;
  }

  mp = g_weak_ref_get (&self->metadata_persistent);
  if (!mp)
    return FALSE;

  g_hash_table_remove (self->saved_settings, name);
  wp_metadata_set (mp, 0, name, NULL, NULL);
  return TRUE;
}

static void
settings_reset_cb (gpointer key, gpointer value, gpointer data)
{
  WpSettings *self = WP_SETTINGS (data);
  const gchar *k = key;

  if (!wp_settings_reset (self, k))
    wp_warning_object (self, "Failed to reset setting %s", k);
}

/*!
 * \brief Resets all the settings to their default value
 * \ingroup wpsettings
 * \param self the settings object
 */
void wp_settings_reset_all (WpSettings *self)
{
  g_return_if_fail (WP_IS_SETTINGS (self));

  g_hash_table_foreach (self->settings, settings_reset_cb, self);
}

static void
settings_save_cb (gpointer key, gpointer value, gpointer data)
{
  WpSettings *self = WP_SETTINGS (data);
  const gchar *k = key;

  if (!wp_settings_save (self, k))
    wp_warning_object (self, "Failed to save setting %s", k);
}

/*!
 * \brief Saves all the settings to make them persistent after reboot
 * \ingroup wpsettings
 * \param self the settings object
 */
void wp_settings_save_all (WpSettings *self)
{
  g_return_if_fail (WP_IS_SETTINGS (self));

  g_hash_table_foreach (self->settings, settings_save_cb, self);
}

/*!
 * \brief Deletes all saved setting to not make them persistent after reboot
 * \ingroup wpsettings
 * \param self the settings object
 */
void wp_settings_delete_all (WpSettings *self)
{
  g_autoptr (WpMetadata) mp = NULL;

  g_return_if_fail (WP_IS_SETTINGS (self));

  g_hash_table_remove_all (self->saved_settings);
  mp = g_weak_ref_get (&self->metadata_persistent);
  if (mp)
    wp_metadata_clear (mp);
}

struct settings_iterator_data
{
  WpSettings *settings;
  const gchar **keys;
  guint len;
  guint curr;
};

static void
settings_iterator_reset (WpIterator *it)
{
  struct settings_iterator_data *it_data = wp_iterator_get_user_data (it);
  it_data->curr = 0;
}

static gboolean
settings_iterator_next (WpIterator *it, GValue *item)
{
  struct settings_iterator_data *it_data = wp_iterator_get_user_data (it);
  g_autoptr (WpSettingsItem) si = NULL;
  const gchar *key, *value;

  if (it_data->curr < it_data->len) {
    key = it_data->keys [it_data->curr++];
    value = g_hash_table_lookup (it_data->settings->settings, key);
    g_return_val_if_fail (value, FALSE);
  } else {
    return FALSE;
  }

  si = wp_settings_item_new (key, value);
  g_value_init (item, WP_TYPE_SETTINGS_ITEM);
  g_value_take_boxed (item, g_steal_pointer (&si));
  return TRUE;
}

static void
settings_iterator_finalize (WpIterator *it)
{
  struct settings_iterator_data *it_data = wp_iterator_get_user_data (it);
  g_clear_pointer (&it_data->keys, g_free);
}

static const WpIteratorMethods settings_iterator_methods = {
  .version = WP_ITERATOR_METHODS_VERSION,
  .reset = settings_iterator_reset,
  .next = settings_iterator_next,
  .fold = NULL,
  .foreach = NULL,
  .finalize = settings_iterator_finalize,
};

/*!
 * \brief Iterates over settings
 * \ingroup wpsettings
 * \param self the settings object
 * \returns (transfer full): an iterator that iterates over the settings.
 */
WpIterator *
wp_settings_new_iterator (WpSettings *self)
{
  g_autoptr (WpIterator) it = NULL;
  struct settings_iterator_data *it_data;
  g_autoptr (WpMetadata) m = NULL;

  g_return_val_if_fail (WP_IS_SETTINGS (self), NULL);

  m = g_weak_ref_get (&self->metadata);
  if (!m)
    return NULL;

  it = wp_iterator_new (&settings_iterator_methods,
      sizeof (struct settings_iterator_data));
  it_data = wp_iterator_get_user_data (it);
  it_data->settings = g_object_ref (self);
  it_data->keys = (const gchar **)g_hash_table_get_keys_as_array (
      self->settings, &it_data->len);
  it_data->curr = 0;
  return g_steal_pointer (&it);
}
