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

/**
 * SECTION: object-interest
 * @title: Object Interest
 */

#define G_LOG_DOMAIN "wp-object-interest"

#include "object-interest.h"
#include "debug.h"
#include "error.h"
#include "private.h"

struct constraint
{
  WpConstraintType type;
  WpConstraintVerb verb;
  gchar subject_type; /* a basic GVariantType as a single char */
  gchar *subject;
  GVariant *value;
};

struct _WpObjectInterest
{
  gboolean valid;
  GType gtype;
  struct pw_array constraints;
};

/**
 * WpObjectInterest:
 * An object interest is a helper that is used in #WpObjectManager to
 * declare interest in certain kinds of objects.
 *
 * An interest is defined by a #GType and a set of constraints on the object's
 * properties. An object "matches" the interest if it is of the specified
 * #GType (either the same type or a descendant of it) and all the constraints
 * are satisfied.
 */
G_DEFINE_BOXED_TYPE (WpObjectInterest, wp_object_interest,
                     wp_object_interest_copy, wp_object_interest_free)

/**
 * wp_object_interest_new:
 * @gtype: the type of the object to declare interest in
 * @...: a set of constraints, terminated with %NULL
 *
 * Creates a new interest that declares interest in objects of the specified
 * @gtype, with the constraints specified in the variable arguments.
 *
 * The variable arguments should be a list of constraints terminated with %NULL,
 * where each constraint consists of the following arguments:
 *  - a #WpConstraintType: the constraint type
 *  - a `const gchar *`: the subject name
 *  - a `const gchar *`: the format string
 *  - 0 or more arguments according to the format string
 *
 * The format string is interpreted as follows:
 *  - the first character is the constraint verb:
 *     - `=`: %WP_CONSTRAINT_VERB_EQUALS
 *     - `c`: %WP_CONSTRAINT_VERB_IN_LIST
 *     - `~`: %WP_CONSTRAINT_VERB_IN_RANGE
 *     - `#`: %WP_CONSTRAINT_VERB_MATCHES
 *     - `+`: %WP_CONSTRAINT_VERB_IS_PRESENT
 *     - `-`: %WP_CONSTRAINT_VERB_IS_ABSENT
 *  - the rest of the characters are interpreted as a #GVariant format string,
 *    as it would be used in g_variant_new()
 *
 * The rest of this function's arguments up to the start of the next constraint
 * depend on the #GVariant format part of the format string and are used to
 * construct a #GVariant for the constraint's value argument.
 *
 * For further reading on the constraint's arguments, see
 * wp_object_interest_add_constraint()
 *
 * For example, this interest matches objects that are descendands of #WpProxy
 * with a "bound-id" between 0 and 100 (inclusive), with a pipewire property
 * called "format.dsp" that contains the string "audio" somewhere in the value
 * and with a pipewire property "port.name" being present (with any value):
 * |[
 * interest = wp_object_interest_new (WP_TYPE_PROXY,
 *     WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", "~(uu)", 0, 100,
 *     WP_CONSTRAINT_TYPE_PW_PROPERTY, "format.dsp", "#s", "*audio*",
 *     WP_CONSTRAINT_TYPE_PW_PROPERTY, "port.name", "+",
 *     NULL);
 * ]|
 *
 * Returns: (transfer full): the new object interest
 */
WpObjectInterest *
wp_object_interest_new (GType gtype, ...)
{
  WpObjectInterest *self;
  va_list args;
  va_start (args, gtype);
  self = wp_object_interest_new_valist (gtype, &args);
  va_end (args);
  return self;
}

/**
 * wp_object_interest_new_valist:
 * @gtype: the type of the object to declare interest in
 * @args: pointer to va_list containing the constraints
 *
 * va_list version of wp_object_interest_new()
 *
 * Returns: (transfer full): the new object interest
 */
WpObjectInterest *
wp_object_interest_new_valist (GType gtype, va_list *args)
{
  WpObjectInterest *self = wp_object_interest_new_type (gtype);
  WpConstraintType type;

  g_return_val_if_fail (self != NULL, NULL);

  for (type = va_arg (*args, WpConstraintType);
       type != WP_CONSTRAINT_TYPE_NONE;
       type = va_arg (*args, WpConstraintType))
  {
    const gchar *subject, *format;
    WpConstraintVerb verb = 0;
    GVariant *value = NULL;

    subject = va_arg (*args, const gchar *);
    g_return_val_if_fail (subject != NULL, NULL);

    format = va_arg (*args, const gchar *);
    g_return_val_if_fail (format != NULL, NULL);

    verb = format[0];
    if (verb != 0 && format[1] != '\0')
      value = g_variant_new_va (format + 1, NULL, args);

    wp_object_interest_add_constraint (self, type, subject, verb, value);
  }
  return self;
}

/**
 * wp_object_interest_new_type: (rename-to wp_object_interest_new)
 * @gtype: the type of the object to declare interest in
 *
 * Creates a new interest that declares interest in objects of the specified
 * @gtype, without any property constraints. To add property constraints,
 * you can call wp_object_interest_add_constraint() afterwards.
 *
 * Returns: (transfer full): the new object interest
 */
WpObjectInterest *
wp_object_interest_new_type (GType gtype)
{
  WpObjectInterest *self = g_slice_new0 (WpObjectInterest);
  g_return_val_if_fail (self != NULL, NULL);
  self->gtype = gtype;
  pw_array_init (&self->constraints, sizeof (struct constraint));
  return self;
}

/**
 * wp_object_interest_add_constraint:
 * @self: the object interest
 * @type: the constraint type
 * @subject: the subject that the constraint applies to
 * @verb: the operation that is performed to check the constraint
 * @value: (transfer floating)(nullable): the value to check for
 *
 * Adds a constraint to this interest. Constraints consist of a @type,
 * a @subject, a @verb and, depending on the @verb, a @value.
 *
 * Constraints are almost like a spoken language sentence that declare a
 * condition that must be true in order to consider that an object can match
 * this interest. For instance, a constraint can be "pipewire property
 * 'object.id' equals 10". This would be translated to:
 * |[
 * wp_object_interest_add_constraint (i,
 *    WP_CONSTRAINT_TYPE_PW_PROPERTY, "object.id",
 *    WP_CONSTRAINT_VERB_EQUALS, g_variant_new_int (10));
 * ]|
 *
 * Some verbs require a @value and some others do not. For those that do,
 * the @value must be of a specific type:
 *  - %WP_CONSTRAINT_VERB_EQUALS: @value can be a string, a (u)int32,
 *    a (u)int64, a double or a boolean. The @subject value must equal this
 *    value for the constraint to be satisfied
 *  - %WP_CONSTRAINT_VERB_IN_LIST: @value must be a tuple that contains any
 *    number of items of the same type; the items can be string, (u)int32,
 *    (u)int64 or double. These items make a list that the @subject's value
 *    will be checked against. If any of the items equals the @subject value,
 *    the constraint is satisfied
 *  - %WP_CONSTRAINT_VERB_IN_RANGE: @value must be a tuple that contains exactly
 *    2 numbers of the same type ((u)int32, (u)int64 or double), meaning the
 *    minimum and maximum (inclusive) of the range. If the @subject value is a
 *    number within this range, the constraint is satisfied
 *  - %WP_CONSTRAINT_VERB_MATCHES: @value must be a string that defines a
 *    pattern usable with #GPatternSpec. If the @subject value matches this
 *    pattern, the constraint is satisfied
 *
 * In case the type of the @subject value is not the same type as the one
 * requested by the type of the @value, the @subject value is converted.
 * For #GObject properties, this conversion is done using g_value_transform(),
 * so limitations of this function apply. In the case of PipeWire properties,
 * which are *always* strings, conversion is done as follows:
 *  - to boolean: `"true"` or `"1"` means %TRUE, `"false"` or `"0"` means %FALSE
 *  - to int / uint / int64 / uint64: One of the `strtol()` family of functions
 *    is used to convert, using base 10
 *  - to double: `strtod()` is used
 *
 * This method does not fail if invalid arguments are given. However,
 * wp_object_interest_validate() should be called after adding all the
 * constraints on an interest in order to catch errors.
 */
void
wp_object_interest_add_constraint (WpObjectInterest * self,
    WpConstraintType type, const gchar * subject,
    WpConstraintVerb verb, GVariant * value)
{
  struct constraint *c;

  g_return_if_fail (self != NULL);

  c = pw_array_add (&self->constraints, sizeof (struct constraint));
  g_return_if_fail (c != NULL);
  c->type = type;
  c->verb = verb;
  /* subject_type is filled in by _validate() */
  c->subject_type = '\0';
  c->subject = g_strdup (subject);
  c->value = value ? g_variant_ref_sink (value) : NULL;

  /* mark as invalid to force validation */
  self->valid = FALSE;
}

/**
 * wp_object_interest_copy:
 * @self: the object interest to copy
 *
 * Returns: (transfer full): a deep copy of @self
 */
WpObjectInterest *
wp_object_interest_copy (WpObjectInterest * self)
{
  WpObjectInterest *copy;
  struct constraint *c, *cc;

  g_return_val_if_fail (self != NULL, NULL);

  copy = wp_object_interest_new_type (self->gtype);
  g_return_val_if_fail (copy != NULL, NULL);

  pw_array_ensure_size (&copy->constraints, self->constraints.size);
  pw_array_for_each (c, &self->constraints) {
    cc = pw_array_add (&self->constraints, sizeof (struct constraint));
    g_return_val_if_fail (cc != NULL, NULL);
    cc->type = c->type;
    cc->verb = c->verb;
    cc->subject_type = c->subject_type;
    cc->subject = g_strdup (c->subject);
    cc->value = c->value ? g_variant_ref (c->value) : NULL;
  }
  copy->valid = self->valid;

  return copy;
}

/**
 * wp_object_interest_free:
 * @self: (transfer full): the object interest to free
 *
 * Releases @self and all the memory associated with it
 */
void
wp_object_interest_free (WpObjectInterest * self)
{
  struct constraint *c;

  g_return_if_fail (self != NULL);

  pw_array_for_each (c, &self->constraints) {
    g_clear_pointer (&c->subject, g_free);
    g_clear_pointer (&c->value, g_variant_unref);
  }
  pw_array_clear (&self->constraints);
  g_slice_free (WpObjectInterest, self);
}

/**
 * wp_object_interest_validate:
 * @self: the object interest to validate
 * @error: (out) (optional): the error, in case validation failed
 *
 * Validates the interest, ensuring that the interest #GType is a valid
 * object and that all the constraints have been expressed properly.
 *
 * Returns: %TRUE if the interest is valid and can be used in a match,
 *   %FALSE otherwise
 */
gboolean
wp_object_interest_validate (WpObjectInterest * self, GError ** error)
{
  struct constraint *c;
  gboolean is_proxy;

  g_return_val_if_fail (self != NULL, FALSE);

  /* if already validated, we are done */
  if (self->valid)
    return TRUE;

  if (!G_TYPE_IS_OBJECT (self->gtype)) {
    g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
        "type '%s' is not a GObject", g_type_name (self->gtype));
    return FALSE;
  }

  is_proxy = g_type_is_a (self->gtype, WP_TYPE_PROXY);

  pw_array_for_each (c, &self->constraints) {
    const GVariantType *value_type = NULL;

    if (c->type <= WP_CONSTRAINT_TYPE_NONE ||
        c->type > WP_CONSTRAINT_TYPE_G_PROPERTY) {
      g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
          "invalid constraint type %d", c->type);
      return FALSE;
    }

    if (!is_proxy && (c->type == WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY ||
            c->type == WP_CONSTRAINT_TYPE_PW_PROPERTY)) {
      g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
          "constraint type %d cannot apply to non-WpProxy type '%s'",
          c->type, g_type_name (self->gtype));
      return FALSE;
    }

    if (!c->subject) {
      g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
          "constraint subject cannot be NULL");
      return FALSE;
    }

    switch (c->verb) {
      case WP_CONSTRAINT_VERB_EQUALS:
      case WP_CONSTRAINT_VERB_IN_LIST:
      case WP_CONSTRAINT_VERB_IN_RANGE:
      case WP_CONSTRAINT_VERB_MATCHES:
        if (!c->value) {
          g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
              "verb %d (%c) requires a value", c->verb, (gchar) c->verb);
          return FALSE;
        }
        value_type = g_variant_get_type (c->value);
        break;

      case WP_CONSTRAINT_VERB_IS_PRESENT:
      case WP_CONSTRAINT_VERB_IS_ABSENT:
        if (c->value) {
          g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
              "verb %d (%c) should not have a value", c->verb, (gchar) c->verb);
          return FALSE;
        }
        break;

      default:
        g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
            "invalid constraint verb %d (%c)", c->verb, (gchar) c->verb);
        return FALSE;
    }

    switch (c->verb) {
      case WP_CONSTRAINT_VERB_EQUALS:
        if (!g_variant_type_equal (value_type, G_VARIANT_TYPE_STRING) &&
            !g_variant_type_equal (value_type, G_VARIANT_TYPE_BOOLEAN) &&
            !g_variant_type_equal (value_type, G_VARIANT_TYPE_INT32) &&
            !g_variant_type_equal (value_type, G_VARIANT_TYPE_UINT32) &&
            !g_variant_type_equal (value_type, G_VARIANT_TYPE_INT64) &&
            !g_variant_type_equal (value_type, G_VARIANT_TYPE_UINT64) &&
            !g_variant_type_equal (value_type, G_VARIANT_TYPE_DOUBLE)) {
          g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
              "WP_CONSTRAINT_VERB_EQUALS requires a basic GVariant type"
              " (actual type was '%s')", g_variant_get_type_string (c->value));
          return FALSE;
        }

        break;
      case WP_CONSTRAINT_VERB_IN_LIST: {
        const GVariantType *tuple_type;

        if (!g_variant_type_is_definite (value_type) ||
            !g_variant_type_is_tuple (value_type)) {
          g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
              "WP_CONSTRAINT_VERB_IN_LIST requires a tuple GVariant type"
              " (actual type was '%s')", g_variant_get_type_string (c->value));
          return FALSE;
        }

        for (tuple_type = value_type = g_variant_type_first (value_type);
            tuple_type != NULL;
            tuple_type = g_variant_type_next (tuple_type)) {
          if (!g_variant_type_equal (tuple_type, value_type)) {
            g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
                "tuple must contain children of the same type"
                " (mismatching type was '%s' at '%.*s')",
                g_variant_get_type_string (c->value),
                (int) g_variant_type_get_string_length (tuple_type),
                g_variant_type_peek_string (tuple_type));
            return FALSE;
          }
        }

        if (!g_variant_type_equal (value_type, G_VARIANT_TYPE_STRING) &&
            !g_variant_type_equal (value_type, G_VARIANT_TYPE_INT32) &&
            !g_variant_type_equal (value_type, G_VARIANT_TYPE_UINT32) &&
            !g_variant_type_equal (value_type, G_VARIANT_TYPE_INT64) &&
            !g_variant_type_equal (value_type, G_VARIANT_TYPE_UINT64) &&
            !g_variant_type_equal (value_type, G_VARIANT_TYPE_DOUBLE)) {
          g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
              "list tuple must contain string, (u)int32, (u)int64 or double"
              " (mismatching type was '%s' at '%.*s')",
              g_variant_get_type_string (c->value),
              (int) g_variant_type_get_string_length (value_type),
              g_variant_type_peek_string (value_type));
          return FALSE;
        }

        break;
      }
      case WP_CONSTRAINT_VERB_IN_RANGE: {
        const GVariantType *tuple_type;

        if (!g_variant_type_is_definite (value_type) ||
            !g_variant_type_is_tuple (value_type)) {
          g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
              "range requires a tuple GVariant type (actual type was '%s')",
              g_variant_get_type_string (c->value));
          return FALSE;
        }

        tuple_type = value_type = g_variant_type_first (value_type);
        if (!tuple_type) {
          g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
              "range requires a non-empty tuple (actual type was '%s')",
              g_variant_get_type_string (c->value));
          return FALSE;
        }

        if (!g_variant_type_equal (tuple_type, G_VARIANT_TYPE_INT32) &&
            !g_variant_type_equal (tuple_type, G_VARIANT_TYPE_UINT32) &&
            !g_variant_type_equal (tuple_type, G_VARIANT_TYPE_INT64) &&
            !g_variant_type_equal (tuple_type, G_VARIANT_TYPE_UINT64) &&
            !g_variant_type_equal (tuple_type, G_VARIANT_TYPE_DOUBLE)) {
          g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
              "range tuple must contain (u)int32, (u)int64 or double"
              " (mismatching type was '%s' at '%.*s')",
              g_variant_get_type_string (c->value),
              (int) g_variant_type_get_string_length (tuple_type),
              g_variant_type_peek_string (tuple_type));
          return FALSE;
        }

        tuple_type = g_variant_type_next (tuple_type);
        if (!tuple_type || !g_variant_type_equal (tuple_type, value_type)) {
          g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
              "range tuple must contain 2 children of the same type"
              " (mismatching type was '%s' at '%.*s')",
              g_variant_get_type_string (c->value),
              (int) g_variant_type_get_string_length (tuple_type),
              g_variant_type_peek_string (tuple_type));
          return FALSE;
        }

        tuple_type = g_variant_type_next (tuple_type);
        if (tuple_type) {
          g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
              "range tuple must contain exactly 2 children, not more"
              " (mismatching type was '%s')",
              g_variant_get_type_string (c->value));
          return FALSE;
        }

        break;
      }
      case WP_CONSTRAINT_VERB_MATCHES:
        if (!g_variant_type_equal (value_type, G_VARIANT_TYPE_STRING)) {
          g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
              "WP_CONSTRAINT_VERB_MATCHES requires a string GVariant"
              " (actual type was '%s')", g_variant_get_type_string (c->value));
          return FALSE;
        }

        break;
      case WP_CONSTRAINT_VERB_IS_PRESENT:
      case WP_CONSTRAINT_VERB_IS_ABSENT:
        break;
      default:
        g_return_val_if_reached (FALSE);
    }

    /* cache the type that the property must have */
    if (value_type)
      c->subject_type = *g_variant_type_peek_string (value_type);
  }

  return (self->valid = TRUE);
}

G_GNUC_CONST static GType
subject_type_to_gtype (gchar type)
{
  switch (type) {
    case 'b': return G_TYPE_BOOLEAN;
    case 'i': return G_TYPE_INT;
    case 'u': return G_TYPE_UINT;
    case 'x': return G_TYPE_INT64;
    case 't': return G_TYPE_UINT64;
    case 'd': return G_TYPE_DOUBLE;
    case 's': return G_TYPE_STRING;
    default: g_return_val_if_reached (G_TYPE_INVALID);
  }
}

static inline gboolean
property_string_to_gvalue (gchar subj_type, const gchar * str, GValue * val)
{
  g_value_init (val, subject_type_to_gtype (subj_type));

  switch (subj_type) {
    case 'b':
      if (!strcmp (str, "true") || !strcmp (str, "1"))
        g_value_set_boolean (val, TRUE);
      else if (!strcmp (str, "false") || !strcmp (str, "0"))
        g_value_set_boolean (val, FALSE);
      else {
        wp_trace ("failed to convert '%s' to boolean", str);
        return FALSE;
      }
      break;
    case 's':
      g_value_set_static_string (val, str);
      break;

#define CASE_NUMBER(l, T, convert) \
    case l: { \
      g##T number; \
      errno = 0; \
      number = convert; \
      if (errno != 0) { \
        wp_trace ("failed to convert '%s' to " #T, str); \
        return FALSE; \
      } \
      g_value_set_##T (val, number); \
      break; \
    }
    CASE_NUMBER ('i', int, strtol (str, NULL, 10))
    CASE_NUMBER ('u', uint, strtoul (str, NULL, 10))
    CASE_NUMBER ('x', int64, strtoll (str, NULL, 10))
    CASE_NUMBER ('t', uint64, strtoull (str, NULL, 10))
    CASE_NUMBER ('d', double, strtod (str, NULL))
#undef CASE_NUMBER
    default:
      g_return_val_if_reached (FALSE);
  }
  return TRUE;
}

static inline gboolean
constraint_verb_equals (gchar subj_type, const GValue * subj_val,
    GVariant * check_val)
{
  switch (subj_type) {
    case 'd': {
      gdouble a = g_value_get_double (subj_val);
      gdouble b = g_variant_get_double (check_val);
      return G_APPROX_VALUE (a, b, FLT_EPSILON);
    }
    case 's':
      return !g_strcmp0 (g_value_get_string (subj_val),
                         g_variant_get_string (check_val, NULL));
#define CASE_BASIC(l, T, R) \
    case l: \
      return (g_value_get_##T (subj_val) == g_variant_get_##R (check_val));
    CASE_BASIC ('b', boolean, boolean)
    CASE_BASIC ('i', int, int32)
    CASE_BASIC ('u', uint, uint32)
    CASE_BASIC ('x', int64, int64)
    CASE_BASIC ('t', uint64, uint64)
#undef CASE_BASIC
    default:
      g_return_val_if_reached (FALSE);
  }
}

static inline gboolean
constraint_verb_matches (gchar subj_type, const GValue * subj_val,
    GVariant * check_val)
{
  switch (subj_type) {
    case 's':
      return g_pattern_match_simple (g_variant_get_string (check_val, NULL),
                                     g_value_get_string (subj_val));
    default:
      g_return_val_if_reached (FALSE);
  }
  return TRUE;
}

static inline gboolean
constraint_verb_in_list (gchar subj_type, const GValue * subj_val,
    GVariant * check_val)
{
  GVariantIter iter;
  g_autoptr (GVariant) child = NULL;

  g_variant_iter_init (&iter, check_val);
  while ((child = g_variant_iter_next_value (&iter))) {
    if (constraint_verb_equals (subj_type, subj_val, child))
      return TRUE;
    g_variant_unref (child);
  }
  return FALSE;
}

static inline gboolean
constraint_verb_in_range (gchar subj_type, const GValue * subj_val,
    GVariant * check_val)
{
  switch (subj_type) {
#define CASE_RANGE(l, t, T) \
    case l: { \
      g##T val, min, max; \
      g_variant_get (check_val, "("#t#t")", &min, &max); \
      val = g_value_get_##T (subj_val); \
      if (val < min || val > max) \
        return FALSE; \
      break; \
    }
    CASE_RANGE('i', i, int)
    CASE_RANGE('u', u, uint)
    CASE_RANGE('x', x, int64)
    CASE_RANGE('t', t, uint64)
    CASE_RANGE('d', d, double)
#undef CASE_RANGE
    default:
      g_return_val_if_reached (FALSE);
  }
  return TRUE;
}

/**
 * wp_object_interest_matches:
 * @self: the object interest
 * @object: the target object to check for a match
 *
 * Checks if the specified @object matches the type and all the constraints
 * that are described in @self
 *
 * Equivalent to `wp_object_interest_matches_full (self,
 * G_OBJECT_TYPE (object), object, NULL, NULL)`
 *
 * Returns: %TRUE if the object matches, %FALSE otherwise
 */
gboolean
wp_object_interest_matches (WpObjectInterest * self, gpointer object)
{
  return wp_object_interest_matches_full (self, G_OBJECT_TYPE (object),
      object, NULL, NULL);
}

/**
 * wp_object_interest_matches_full:
 * @self: the object interest
 * @object_type: the type to be checked against the interest's type
 * @object: (type GObject)(transfer none)(nullable): the object to be used for
 *   checking constraints of type %WP_CONSTRAINT_TYPE_G_PROPERTY
 * @pw_props: (transfer none)(nullable): the properties to be used for
 *   checking constraints of type %WP_CONSTRAINT_TYPE_PW_PROPERTY
 * @pw_global_props: (transfer none)(nullable): the properties to be used for
 *   checking constraints of type %WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY
 *
 * A low-level version of wp_object_interest_matches(). In this version,
 * the object's type is directly given in @object_type and is not inferred
 * from the @object. @object is only used to check for constraints against
 * #GObject properties.
 *
 * @pw_props and @pw_global_props are used to check constraints against
 * PipeWire object properties and global properties, respectively.
 *
 * @object, @pw_props and @pw_global_props may be %NULL, but in case there
 * are any constraints that require them, the match will fail.
 * As a special case, if @object is not %NULL and is a subclass of #WpProxy,
 * then @pw_props and @pw_global_props, if required, will be internally
 * retrieved from @object by calling wp_proxy_get_properties() and
 * wp_proxy_get_global_properties() respectively.
 *
 * Returns: %TRUE if the the type matches this interest and the properties
 *   match the constraints, %FALSE otherwise
 */
gboolean
wp_object_interest_matches_full (WpObjectInterest * self,
    GType object_type, gpointer object, WpProperties * pw_props,
    WpProperties * pw_global_props)
{
  g_autoptr (WpProperties) props = NULL;
  g_autoptr (WpProperties) global_props = NULL;
  g_autoptr (GError) error = NULL;
  struct constraint *c;

  g_return_val_if_fail (self != NULL, FALSE);

  if (G_UNLIKELY (!wp_object_interest_validate (self, &error))) {
    wp_critical_boxed (WP_TYPE_OBJECT_INTEREST, self, "validation failed: %s",
        error->message);
    return FALSE;
  }

  /* check if the GType matches */
  if (!g_type_is_a (object_type, self->gtype))
    return FALSE;

  /* prepare for constraint lookups on proxy properties */
  if (object && g_type_is_a (object_type, WP_TYPE_PROXY)) {
    WpProxy *p = WP_PROXY (object);

    if (!pw_global_props)
      pw_global_props = global_props = wp_proxy_get_global_properties (p);

    if (!pw_props && wp_proxy_get_features (p) & WP_PROXY_FEATURE_INFO)
      pw_props = props = wp_proxy_get_properties (p);
  }

  /* check all constraints; if any of them fails at any point, fail the match */
  pw_array_for_each (c, &self->constraints) {
    WpProperties *lookup_props = pw_global_props;
    g_auto (GValue) value = G_VALUE_INIT;
    gboolean exists = FALSE;

    /* collect, check & convert the subject property */
    switch (c->type) {
      case WP_CONSTRAINT_TYPE_PW_PROPERTY:
        lookup_props = pw_props;
        G_GNUC_FALLTHROUGH;

      case WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY: {
        const gchar *lookup_str = NULL;

        if (lookup_props)
          exists = !!(lookup_str = wp_properties_get (lookup_props, c->subject));

        if (exists && c->subject_type)
          property_string_to_gvalue (c->subject_type, lookup_str, &value);
        break;
      }
      case WP_CONSTRAINT_TYPE_G_PROPERTY: {
        GType value_type;

        if (object)
          exists = !!g_object_class_find_property (G_OBJECT_GET_CLASS (object), c->subject);

        if (exists && c->subject_type) {
          g_object_get_property (object, c->subject, &value);
          value_type = G_VALUE_TYPE (&value);

          /* transform if not compatible */
          if (value_type != subject_type_to_gtype (c->subject_type)) {
            if (g_value_type_transformable (value_type,
                    subject_type_to_gtype (c->subject_type))) {
              g_auto (GValue) orig = G_VALUE_INIT;
              g_value_init (&orig, value_type);
              g_value_copy (&value, &orig);
              g_value_unset (&value);
              g_value_init (&value, subject_type_to_gtype (c->subject_type));
              g_value_transform (&orig, &value);
            }
            else
              return FALSE;
          }
        }

        break;
      }
      default:
        g_return_val_if_reached (FALSE);
    }

    /* match the subject to the constraint's value,
       according to the operation defined by the verb */
    switch (c->verb) {
      case WP_CONSTRAINT_VERB_EQUALS:
        if (!exists ||
            !constraint_verb_equals (c->subject_type, &value, c->value))
          return FALSE;
        break;
      case WP_CONSTRAINT_VERB_MATCHES:
        if (!exists ||
            !constraint_verb_matches (c->subject_type, &value, c->value))
          return FALSE;
        break;
      case WP_CONSTRAINT_VERB_IN_LIST:
        if (!exists ||
            !constraint_verb_in_list (c->subject_type, &value, c->value))
          return FALSE;
        break;
      case WP_CONSTRAINT_VERB_IN_RANGE:
        if (!exists ||
            !constraint_verb_in_range (c->subject_type, &value, c->value))
          return FALSE;
        break;
      case WP_CONSTRAINT_VERB_IS_PRESENT:
        if (!exists)
          return FALSE;
        break;
      case WP_CONSTRAINT_VERB_IS_ABSENT:
        if (exists)
          return FALSE;
        break;
      default:
        g_return_val_if_reached (FALSE);
    }
  }
  return TRUE;
}