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

#include "json-utils.h"
#include "error.h"
#include "log.h"

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

WP_DEFINE_LOCAL_LOG_TOPIC ("wp-json-utils")

/*! \defgroup wpjsonutils Json Utilities */

struct match_rules_cb_data
{
  WpRuleMatchCallback callback;
  gpointer data;
  GError **error;
};

static int
match_rules_cb (void *data, const char *location, const char *action,
    const char *str, size_t len)
{
  struct match_rules_cb_data *cb_data = data;
  g_autoptr (WpSpaJson) json = wp_spa_json_new_wrap_stringn (str, len);
  return cb_data->callback (cb_data->data, action, json, cb_data->error) ? 0 : -EPIPE;
}

/*!
 * \brief Matches the given properties against a set of rules described in JSON
 * and calls the given callback to perform actions on a successful match.
 *
 * The given JSON should be an array of objects, where each object has a
 * "matches" and an "actions" property. The "matches" value should also be
 * an array of objects, where each object is a set of properties to match.
 * Inside such an object, all properties must match to consider a successful
 * match. However, if multiple objects are provided, only one object needs
 * to match.
 *
 * The "actions" value should be an object where the key is the action name
 * and the value can be any valid JSON. Both the action name and the value are
 * passed as-is on the \a callback.
 *
 * \verbatim
 * [
 *     {
 *         matches = [
 *             # any of the items in matches needs to match, if one does,
 *             # actions are emitted.
 *             {
 *                 # all keys must match the value. ! negates. ~ starts regex.
 *                 <key> = <value>
 *                 ...
 *             }
 *             ...
 *         ]
 *         actions = {
 *             <action> = <value>
 *             ...
 *         }
 *     }
 * ]
 * \endverbatim
 *
 * \ingroup wpjsonutils
 * \param json a JSON array containing rules in the described format
 * \param match_props (transfer none): the properties to match against the rules
 * \param callback (scope call)(closure data): a function to call for each action on a successful match
 * \param data data to be passed to \a callback
 * \param error (out)(optional): the error that occurred, if any
 * \returns FALSE if an error occurred, TRUE otherwise
 */
gboolean
wp_json_utils_match_rules (WpSpaJson *json, WpProperties *match_props,
    WpRuleMatchCallback callback, gpointer data, GError ** error)
{
  g_autoptr (GError) cb_error = NULL;
  struct match_rules_cb_data cb_data = { callback, data, &cb_error };

  int res = pw_conf_match_rules (wp_spa_json_get_data (json),
      wp_spa_json_get_size (json), NULL, wp_properties_peek_dict (match_props),
      match_rules_cb, &cb_data);

  if (res < 0) {
    if (cb_error)
      g_propagate_error (error, g_steal_pointer (&cb_error));
    else
      g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
          "match rules error: %s", spa_strerror (res));
    return FALSE;
  }

  return TRUE;
}

struct update_props_cb_data
{
  WpProperties *props;
  gint count;
};

static gboolean
update_props_cb (gpointer data, const gchar * action, WpSpaJson * value,
    GError ** error)
{
  struct update_props_cb_data *cb_data = data;
  if (g_str_equal (action, "update-props"))
    cb_data->count += wp_properties_update_from_json (cb_data->props, value);
  return TRUE;
}

/*!
 * \brief Matches the given properties against a set of rules described in JSON
 * and updates the properties if the rule actions include the "update-props"
 * action.
 *
 * \ingroup wpjsonutils
 * \param json a JSON array containing rules in the format accepted by
 *    wp_json_utils_match_rules()
 * \param props (transfer none): the properties to match against the rules
 *    and also update, acting on the "update-props" action
 * \returns the number of properties that were updated
 */
gint
wp_json_utils_match_rules_update_properties (WpSpaJson *json, WpProperties *props)
{
  g_autoptr (GError) cb_error = NULL;
  struct update_props_cb_data cb_data = { props, 0 };

  wp_json_utils_match_rules (json, props, update_props_cb, &cb_data, &cb_error);
  if (cb_error)
    wp_notice ("%s", cb_error->message);

  return cb_data.count;
}


#define OVERRIDE_SECTION_PREFIX "override."

static WpSpaJson *
merge_json_objects (WpSpaJson *a, WpSpaJson *b)
{
  g_autoptr (WpSpaJsonBuilder) builder = NULL;

  builder = wp_spa_json_builder_new_object ();

  /* Add all properties from 'a' that don't exist in 'b' */
  {
    g_autoptr (WpIterator) it = wp_spa_json_new_iterator (a);
    g_auto (GValue) item = G_VALUE_INIT;
    for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
      g_autoptr (WpSpaJson) key = NULL;
      g_autoptr (WpSpaJson) val = NULL;
      g_autoptr (WpSpaJson) j = NULL;
      g_autofree gchar *str = NULL;
      const gchar *key_str;
      g_autofree gchar *override_key_str = NULL;
      gboolean override;

      key = g_value_dup_boxed (&item);
      key_str = str = wp_spa_json_parse_string (key);
      g_return_val_if_fail (key_str, NULL);
      override = g_str_has_prefix (str, OVERRIDE_SECTION_PREFIX);
      if (override)
        key_str += strlen (OVERRIDE_SECTION_PREFIX);
      override_key_str = g_strdup_printf (OVERRIDE_SECTION_PREFIX "%s", key_str);

      g_value_unset (&item);
      g_return_val_if_fail (wp_iterator_next (it, &item), NULL);
      val = g_value_dup_boxed (&item);

      if (!wp_spa_json_object_get (b, key_str, "J", &j, NULL) &&
          !wp_spa_json_object_get (b, override_key_str, "J", &j, NULL)) {
        wp_spa_json_builder_add_property (builder, key_str);
        wp_spa_json_builder_add_json (builder, val);
      }
    }
  }

  /* Add properties from 'b' that don't exist in 'a'. If a property
   * exists in 'a' and does not have the 'override.' prefix, recursively
   * merge it before adding it. Otherwise override it. */
  {
    g_autoptr (WpIterator) it = wp_spa_json_new_iterator (b);
    g_auto (GValue) item = G_VALUE_INIT;
    for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
      g_autoptr (WpSpaJson) key = NULL;
      g_autoptr (WpSpaJson) val = NULL;
      g_autoptr (WpSpaJson) j = NULL;
      g_autofree gchar *str = NULL;
      const gchar *key_str;
      g_autofree gchar *override_key_str = NULL;
      gboolean override;

      key = g_value_dup_boxed (&item);
      key_str = str = wp_spa_json_parse_string (key);
      g_return_val_if_fail (key_str, NULL);
      override = g_str_has_prefix (str, OVERRIDE_SECTION_PREFIX);
      if (override)
        key_str += strlen (OVERRIDE_SECTION_PREFIX);
      override_key_str = g_strdup_printf (OVERRIDE_SECTION_PREFIX "%s", key_str);

      g_value_unset (&item);
      g_return_val_if_fail (wp_iterator_next (it, &item), NULL);
      val = g_value_dup_boxed (&item);

      if (!override && wp_spa_json_is_container (val) &&
          (wp_spa_json_object_get (a, key_str, "J", &j, NULL) ||
           wp_spa_json_object_get (a, override_key_str, "J", &j, NULL))) {
        g_autoptr (WpSpaJson) merged = wp_json_utils_merge_containers (j, val);
        if (!merged) {
          wp_warning ("skipping merge of %s as JSON values are not compatible containers",
              key_str);
          continue;
        }
        wp_spa_json_builder_add_property (builder, key_str);
        wp_spa_json_builder_add_json (builder, merged);
      } else {
        wp_spa_json_builder_add_property (builder, key_str);
        wp_spa_json_builder_add_json (builder, val);
      }
    }
  }

  return wp_spa_json_builder_end (builder);
}

static WpSpaJson *
merge_json_arrays (WpSpaJson * a, WpSpaJson * b)
{
  g_autoptr (WpSpaJsonBuilder) builder = NULL;

  builder = wp_spa_json_builder_new_array ();

  /* Add all elements from 'a' */
  {
    g_autoptr (WpIterator) it = wp_spa_json_new_iterator (a);
    g_auto (GValue) item = G_VALUE_INIT;
    for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
      WpSpaJson *j = g_value_get_boxed (&item);
      wp_spa_json_builder_add_json (builder, j);
    }
  }

  /* Add all elements from 'b' */
  {
    g_autoptr (WpIterator) it = wp_spa_json_new_iterator (b);
    g_auto (GValue) item = G_VALUE_INIT;
    for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
      WpSpaJson *j = g_value_get_boxed (&item);
      wp_spa_json_builder_add_json (builder, j);
    }
  }

  return wp_spa_json_builder_end (builder);
}

/*!
 * \brief Merges two JSON containers (objects or arrays) into one.
 *
 * If both \a a and \a b are objects, the result will be a new object
 * containing all properties from both \a a and \a b. If a property exists
 * in both \a a and \a b, the values are recursively merged. If a property
 * exists in both \a a and \a b and the property name starts with the
 * "override." prefix in either of those, the value from the key with the
 * prefix is used.
 *
 * If both \a a and \a b are arrays, the result will be a new array
 * containing all elements from both \a a and \a b.
 *
 * If \a a and \a b are not of the same type, NULL is returned.
 *
 * \ingroup wpjsonutils
 * \param a (transfer none): a JSON container
 * \param b (transfer none): a JSON container
 * \returns a new JSON container containing the merged contents of \a a and \a b
 *    or NULL if \a a and \a b are not of the same type
 */
WpSpaJson *
wp_json_utils_merge_containers (WpSpaJson * a, WpSpaJson * b)
{
  if (wp_spa_json_is_array (a) && wp_spa_json_is_array (b))
    return merge_json_arrays (a, b);
  else if (wp_spa_json_is_object (a) && wp_spa_json_is_object (b))
    return merge_json_objects (a, b);
  return NULL;
}
