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

#define G_LOG_DOMAIN "wp-spa-type"

#include "spa-type.h"

#include <spa/utils/type-info.h>
#include <spa/debug/types.h>
#include <pipewire/pipewire.h>

/*! \defgroup wpspatype WpSpaType
 *
 * Spa has a type system that is represented by a set of arrays that contain
 * `spa_type_info` structures. This type system is simple, yet complex to
 * work with for a couple of reasons.
 *
 * WirePlumber uses this API to access the spa type system, which makes some
 * things easier to understand and work with. The main benefit of using this
 * API is that it makes it easy to work with string representations of the
 * types, allowing easier access from script bindings.
 *
 * \b Type \b hierarchy
 *
 * On the top level, there is a list of types like Int, Bool, String, Id, Object.
 * These are called fundamental types (terms borrowed from GType).
 * Fundamental types can be derived and therefore we can have other types
 * that represent specific objects, for instance.
 *
 * Enum and flag types are all represented with `SPA_TYPE_Id`. These types
 * may have a list of possible values that one can select from (enums)
 * or combine (flags). These values are accessed with the WpSpaIdTable API.
 *
 * Object types can have fields. All objects always have a special "id" field,
 * which is an enum. Its possible values can be given by
 * wp_spa_type_get_object_id_values_table(). Optionally, objects can also have
 * other object-specific fields, which can be accessed with
 * wp_spa_type_get_values_table().
 *
 * Every object field or enum value is represented by a WpSpaIdValue. In the
 * case of object fields, each field can be of a specific type, which is
 * returned by wp_spa_id_value_get_value_type().
 */

static const WpSpaType SPA_TYPE_VENDOR_WirePlumber = 0x03000000;
static GArray *extra_types = NULL;
static GArray *extra_id_tables = NULL;

typedef struct {
  const char *name;
  const struct spa_type_info *values;
} WpSpaIdTableInfo;

static const WpSpaIdTableInfo static_id_tables[] = {
  { SPA_TYPE_INFO_Choice, spa_type_choice },
  { SPA_TYPE_INFO_Direction, spa_type_direction },
  { SPA_TYPE_INFO_ParamId, spa_type_param },
  { SPA_TYPE_INFO_MediaType, spa_type_media_type },
  { SPA_TYPE_INFO_MediaSubtype, spa_type_media_subtype },
  { SPA_TYPE_INFO_ParamAvailability, spa_type_param_availability },
  { SPA_TYPE_INFO_ParamPortConfigMode, spa_type_param_port_config_mode },
  { SPA_TYPE_INFO_VideoFormat, spa_type_video_format },
  { SPA_TYPE_INFO_AudioFormat, spa_type_audio_format },
  { SPA_TYPE_INFO_AudioFlags, spa_type_audio_flags },
  { SPA_TYPE_INFO_AudioChannel, spa_type_audio_channel },
  { SPA_TYPE_INFO_AudioIEC958Codec, spa_type_audio_iec958_codec },
  { SPA_TYPE_INFO_IO, spa_type_io },
  { SPA_TYPE_INFO_Control, spa_type_control },
  { SPA_TYPE_INFO_Data, spa_type_data_type },
  { SPA_TYPE_INFO_Meta, spa_type_meta_type },
  { SPA_TYPE_INFO_DeviceEventId, spa_type_device_event_id },
  { SPA_TYPE_INFO_NodeEvent, spa_type_node_event_id },
  { SPA_TYPE_INFO_NodeCommand, spa_type_node_command_id },
  { NULL, NULL }
};

GType wp_spa_type_get_type (void)
{
  static gsize id__volatile = 0;
  if (g_once_init_enter (&id__volatile)) {
    GType id = g_type_register_static_simple (
        G_TYPE_UINT, g_intern_static_string ("WpSpaType"),
        0, NULL, 0, NULL, 0);
    g_once_init_leave (&id__volatile, id);
  }
  return id__volatile;
}

G_DEFINE_POINTER_TYPE (WpSpaIdTable, wp_spa_id_table)

G_DEFINE_POINTER_TYPE (WpSpaIdValue, wp_spa_id_value)


static const struct spa_type_info *
wp_spa_type_info_find_by_type (WpSpaType type)
{
  const struct spa_type_info *info;

  g_return_val_if_fail (type != WP_SPA_TYPE_INVALID, NULL);
  g_return_val_if_fail (type != 0, NULL);

  if (extra_types)
    info = spa_debug_type_find (
        (const struct spa_type_info *) extra_types->data, type);
  else
    info = spa_debug_type_find (SPA_TYPE_ROOT, type);

  return info;
}

/* similar to spa_debug_type_find() and unlike spa_debug_type_find_type(),
   which steps into id values / object fields */
static const struct spa_type_info *
_spa_type_find_by_name (const struct spa_type_info * info, const char * name)
{
  const struct spa_type_info * res;

  while (info->name) {
    if (info->type == SPA_ID_INVALID) {
      if (info->values && (res = _spa_type_find_by_name (info->values, name)))
        return res;
    }
    if (strcmp (info->name, name) == 0)
      return info;
    info++;
  }
  return NULL;
}

static const struct spa_type_info *
wp_spa_type_info_find_by_name (const gchar *name)
{
  const struct spa_type_info *info = NULL;

  g_return_val_if_fail (name != NULL, NULL);

  if (extra_types)
    info = _spa_type_find_by_name (
        (const struct spa_type_info *) extra_types->data, name);
  else
    info = _spa_type_find_by_name (SPA_TYPE_ROOT, name);

  return info;
}

/*!
 * \brief Looks up the type id from a given type name
 *
 * \ingroup wpspatype
 * \param name the name to look up
 * \returns (transfer none): the corresponding type id or WP_SPA_TYPE_INVALID
 *   if not found
 */
WpSpaType
wp_spa_type_from_name (const gchar *name)
{
  const struct spa_type_info *info = wp_spa_type_info_find_by_name (name);
  return info ? info->type : WP_SPA_TYPE_INVALID;
}

/*!
 * \brief Gets the parent type of an SPA type
 *
 * \ingroup wpspatype
 * \param type a type id
 * \returns (transfer none): the direct parent type of the given \a type; if the
 *   type is fundamental (i.e. has no parent), the returned type is the same
 *   as \a type
 */
WpSpaType
wp_spa_type_parent (WpSpaType type)
{
  const struct spa_type_info *info = wp_spa_type_info_find_by_type (type);
  return info ? info->parent : WP_SPA_TYPE_INVALID;
}

/*!
 * \brief Gets the name of an SPA type
 *
 * \ingroup wpspatype
 * \param type a type id
 * \returns the complete name of the given \a type or NULL if \a type is invalid
 */
const gchar *
wp_spa_type_name (WpSpaType type)
{
  const struct spa_type_info *info = wp_spa_type_info_find_by_type (type);
  return info ? info->name : NULL;
}

/*!
 * \brief Checks if an SPA type is a fundamental type
 *
 * \ingroup wpspatype
 * \param type a type id
 * \returns TRUE if the \a type has no parent, FALSE otherwise
 */
gboolean
wp_spa_type_is_fundamental (WpSpaType type)
{
  const struct spa_type_info *info = wp_spa_type_info_find_by_type (type);
  return info ? (info->type == info->parent) : FALSE;
}

/*!
 * \brief Checks if an SPA type is an Id type
 *
 * \ingroup wpspatype
 * \param type a type id
 * \returns TRUE if the \a type is a SPA_TYPE_Id, FALSE otherwise
 */
gboolean
wp_spa_type_is_id (WpSpaType type)
{
  const struct spa_type_info *info = wp_spa_type_info_find_by_type (type);
  return info ? (info->parent == SPA_TYPE_Id) : FALSE;
}

/*!
 * \brief Checks if an SPA type is an Object type
 *
 * \ingroup wpspatype
 * \param type a type id
 * \returns TRUE if the \a type is a SPA_TYPE_Object, FALSE otherwise
 */
gboolean
wp_spa_type_is_object (WpSpaType type)
{
  const struct spa_type_info *info = wp_spa_type_info_find_by_type (type);
  return info ? (info->parent == SPA_TYPE_Object) : FALSE;
}

/*!
 * \brief Gets the table with the values that can be stored in the special "id"
 *   field of an object of the given \a type
 *
 * Object pods (see WpSpaPod) always have a special "id" field along with
 * other fields that can be defined. This "id" field can only store values
 * of a specific `SPA_TYPE_Id` type. This function returns the table that
 * contains the possible values for that field.
 *
 * \ingroup wpspatype
 * \param type the type id of an object type
 * \returns the table with the values that can be stored in the special "id"
 *   field of an object of the given \a type
 */
WpSpaIdTable
wp_spa_type_get_object_id_values_table (WpSpaType type)
{
  const struct spa_type_info *info = wp_spa_type_info_find_by_type (type);

  g_return_val_if_fail (info != NULL, NULL);
  g_return_val_if_fail (info->parent == SPA_TYPE_Object, NULL);
  g_return_val_if_fail (info->values != NULL, NULL);
  g_return_val_if_fail (info->values->name != NULL, NULL);
  g_return_val_if_fail (info->values->parent == SPA_TYPE_Id, NULL);

  return info->values->values;
}

/*!
 * \brief Gets the values table of an SPA type
 *
 * \ingroup wpspatype
 * \param type a type id
 * \returns the associated WpSpaIdTable that contains possible
 *   values or object fields for this type, or NULL
 */
WpSpaIdTable
wp_spa_type_get_values_table (WpSpaType type)
{
  const struct spa_type_info *info = wp_spa_type_info_find_by_type (type);

  g_return_val_if_fail (info != NULL, NULL);
  return info->values;
}

struct spa_type_info_iterator_data
{
  const struct spa_type_info *base;
  const struct spa_type_info *cur;
};

static void
spa_type_info_iterator_reset (WpIterator *it)
{
  struct spa_type_info_iterator_data *it_data = wp_iterator_get_user_data (it);
  it_data->cur = it_data->base;
}

static gboolean
spa_type_info_iterator_next (WpIterator *it, GValue *item)
{
  struct spa_type_info_iterator_data *it_data = wp_iterator_get_user_data (it);

  if (it_data->cur->name) {
    g_value_init (item, WP_TYPE_SPA_ID_VALUE);
    g_value_set_pointer (item, (gpointer) it_data->cur);
    it_data->cur++;
    return TRUE;
  }
  return FALSE;
}

static gboolean
spa_type_info_iterator_fold (WpIterator *it, WpIteratorFoldFunc func,
    GValue *ret, gpointer data)
{
  struct spa_type_info_iterator_data *it_data = wp_iterator_get_user_data (it);
  const struct spa_type_info *cur, *base;

  cur = base = it_data->base;

  while (cur->name) {
    g_auto (GValue) item = G_VALUE_INIT;
    g_value_init (&item, WP_TYPE_SPA_ID_VALUE);
    g_value_set_pointer (&item, (gpointer) cur);
    if (!func (&item, ret, data))
      return FALSE;
    cur++;
  }
  return TRUE;
}

static const WpIteratorMethods spa_type_info_iterator_methods = {
  .version = WP_ITERATOR_METHODS_VERSION,
  .reset = spa_type_info_iterator_reset,
  .next = spa_type_info_iterator_next,
  .fold = spa_type_info_iterator_fold,
};

/*!
 * \brief Finds a WpSpaIdTable given its name.
 *
 * This name can either be the full type name of an object type,
 * or the name of an enum (which is \b not(!!) a type).
 * For example, "Spa:Pod:Object:Param:Format" and "Spa:Enum:ParamId" are
 * both valid table names.
 *
 * \ingroup wpspatype
 * \param name the full name of an id table
 * \returns (nullable): the associated table, or NULL
 */
WpSpaIdTable
wp_spa_id_table_from_name (const gchar *name)
{
  g_return_val_if_fail (name != NULL, NULL);
  const WpSpaIdTableInfo *info = NULL;

  /* first look in dynamic id tables */
  if (extra_id_tables) {
    info = (const WpSpaIdTableInfo *) extra_id_tables->data;
    while (info && info->name) {
      if (strcmp (info->name, name) == 0)
        return info->values;
      info++;
    }
  }

  /* then look at the well-known static ones */
  info = static_id_tables;
  while (info && info->name) {
    if (strcmp (info->name, name) == 0)
      return info->values;
    info++;
  }

  /* then look into types, hoping to find an object type */
  const struct spa_type_info *tinfo = wp_spa_type_info_find_by_name (name);
  return tinfo ? tinfo->values : NULL;
}

/*!
 * \brief This function returns an iterator that allows you to iterate
 * through the values associated with this table.
 *
 * The items in the iterator are of type WpSpaIdValue.
 *
 * \ingroup wpspatype
 * \param table the id table
 * \returns a WpIterator that iterates over WpSpaIdValue items
 */
WpIterator *
wp_spa_id_table_new_iterator (WpSpaIdTable table)
{
  g_return_val_if_fail (table != NULL, NULL);

  WpIterator *it = wp_iterator_new (&spa_type_info_iterator_methods,
      sizeof (struct spa_type_info_iterator_data));
  struct spa_type_info_iterator_data *it_data = wp_iterator_get_user_data (it);
  it_data->base = (const struct spa_type_info *) table;
  it_data->cur = it_data->base;
  return it;
}

/*!
 * \brief Finds a value in an SPA Id table
 *
 * \ingroup wpspatype
 * \param table the id table
 * \param value a numeric value that is contained in the table
 * \returns (nullable): the WpSpaIdValue associated with \a value, or NULL
 */
WpSpaIdValue
wp_spa_id_table_find_value (WpSpaIdTable table, guint value)
{
  g_return_val_if_fail (table != NULL, NULL);

  const struct spa_type_info *info = table;
  while (info && info->name) {
    if (info->type == value)
      return info;
    info++;
  }
  return NULL;
}

/*!
 * \brief Finds a named value in an SPA Id table
 *
 * \ingroup wpspatype
 * \param table the id table
 * \param name the full name of a value that is contained in the table
 * \returns (nullable): the WpSpaIdValue associated with \a name, or NULL
 */
WpSpaIdValue
wp_spa_id_table_find_value_from_name (WpSpaIdTable table, const gchar * name)
{
  g_return_val_if_fail (table != NULL, NULL);

  const struct spa_type_info *info = table;
  while (info && info->name) {
    if (!strcmp (info->name, name))
      return info;
    info++;
  }
  return NULL;
}

/*!
 * \brief Finds a short named value in an SPA Id table
 *
 * \ingroup wpspatype
 * \param table the id table
 * \param short_name the short name of a value that is contained in the table
 * \returns (nullable): the WpSpaIdValue associated with \a short_name, or NULL
 */
WpSpaIdValue
wp_spa_id_table_find_value_from_short_name (WpSpaIdTable table,
    const gchar * short_name)
{
  g_return_val_if_fail (table != NULL, NULL);

  const struct spa_type_info *info = table;
  while (info && info->name) {
    if (!strcmp (spa_debug_type_short_name (info->name), short_name))
      return info;
    info++;
  }
  return NULL;
}

static WpSpaIdTable
wp_spa_id_name_find_id_table (const gchar * name)
{
  WpSpaIdTable table = NULL;
  g_autofree gchar *parent_name = g_strdup (name);
  gchar *h;

  if ((h = strrchr(parent_name, ':')) != NULL) {
    /* chop the enum name to get the type, ex:
       Spa:Enum:Direction:Input -> Spa:Enum:Direction */
    *h = '\0';
    table = wp_spa_id_table_from_name (parent_name);

    /* in some cases, the parent name is one layer further up, ex:
       Spa:Pod:Object:Param:Format:Audio:rate -> Spa:Pod:Object:Param:Format */
    if (!table && (h = strrchr(parent_name, ':')) != NULL) {
      *h = '\0';
      table = wp_spa_id_table_from_name (parent_name);
    }
  }
  return table;
}

/*!
 * \brief Looks up an id value (enum, flag or object field) directly from its
 * full name.
 *
 * For instance, "Spa:Enum:Direction:Input" will resolve to the
 * id value that represents "Input" in the "Spa:Enum:Direction" enum.
 *
 * \ingroup wpspatype
 * \param name the full name of an id value
 * \returns the id value for \a name, or NULL if no such id value was found
 */
WpSpaIdValue
wp_spa_id_value_from_name (const gchar * name)
{
  g_return_val_if_fail (name != NULL, NULL);

  WpSpaIdTable table = wp_spa_id_name_find_id_table (name);
  return wp_spa_id_table_find_value_from_name (table, name);
}

/*!
 * \brief Looks up an id value given its container \a table_name and its
 * \a short_name
 *
 * \ingroup wpspatype
 * \param table_name the name of the WpSpaIdTable to look up the value in
 * \param short_name the short name of the value to look up
 * \returns the id value or NULL if it was not found
 */
WpSpaIdValue
wp_spa_id_value_from_short_name (const gchar * table_name,
    const gchar * short_name)
{
  g_return_val_if_fail (table_name != NULL, NULL);
  g_return_val_if_fail (short_name != NULL, NULL);

  WpSpaIdTable table = wp_spa_id_table_from_name (table_name);
  return wp_spa_id_table_find_value_from_short_name (table, short_name);
}

/*!
 * \brief Looks up an id value given its container \a table_name and its numeric
 * representation, \a id
 *
 * \ingroup wpspatype
 * \param table_name the name of the WpSpaIdTable to look up the value in
 * \param id the numeric representation of the value to look up
 * \returns the id value or NULL if it was not found
 */
WpSpaIdValue
wp_spa_id_value_from_number (const gchar * table_name, guint id)
{
  g_return_val_if_fail (table_name != NULL, NULL);

  WpSpaIdTable table = wp_spa_id_table_from_name (table_name);
  return wp_spa_id_table_find_value (table, id);
}

/*!
 * \brief Gets the numeric value of an id value
 *
 * \ingroup wpspatype
 * \param id an id value
 * \returns the numeric representation of this id value
 */
guint
wp_spa_id_value_number (WpSpaIdValue id)
{
  g_return_val_if_fail (id != NULL, -1);

  const struct spa_type_info *info = id;
  return info->type;
}

/*!
 * \brief Gets the name of an id value
 *
 * \ingroup wpspatype
 * \param id an id value
 * \returns the full name of this id value
 */
const gchar *
wp_spa_id_value_name (WpSpaIdValue id)
{
  g_return_val_if_fail (id != NULL, NULL);

  const struct spa_type_info *info = id;
  return info->name;
}

/*!
 * \brief Gets the short name of an id value
 *
 * \ingroup wpspatype
 * \param id an id value
 * \returns the short name of this id value
 */
const gchar *
wp_spa_id_value_short_name (WpSpaIdValue id)
{
  g_return_val_if_fail (id != NULL, NULL);

  const struct spa_type_info *info = id;
  return spa_debug_type_short_name (info->name);
}

/*!
 * \brief Returns the value type associated with this WpSpaIdValue.
 *
 * This information is useful when \a id represents an object field,
 * which can take a value of an arbitrary type.
 *
 * When the returned type is (or is derived from) `SPA_TYPE_Id` or
 * `SPA_TYPE_Object`, \a table is set to point to the WpSpaIdTable
 * that contains the possible Id values / object fields.
 *
 * \ingroup wpspatype
 * \param id an id value
 * \param table (out) (optional): the associated WpSpaIdTable
 * \returns (transfer none): the value type associated with \a id
 */
WpSpaType
wp_spa_id_value_get_value_type (WpSpaIdValue id, WpSpaIdTable * table)
{
  g_return_val_if_fail (id != NULL, WP_SPA_TYPE_INVALID);

  const struct spa_type_info *info = id;

  if (table) {
    /* info->values has different semantics on Array types */
    if (info->values && info->parent != SPA_TYPE_Array) {
      *table = info->values;
    }
    /* derived object types normally don't have info->values directly set,
       so we need to look them up */
    else if (wp_spa_type_is_object (info->parent)) {
      WpSpaIdTable t = wp_spa_type_get_values_table (info->parent);
      if (t) *table = t;
    }
  }

  return info->parent;
}

/*!
 * \brief If the value type of \a id is `SPA_TYPE_Array`, this function
 * returns the type that is allowed to be contained inside the array.
 *
 * When the returned type is (or is derived from) `SPA_TYPE_Id` or
 * `SPA_TYPE_Object`, \a table is set to point to the WpSpaIdTable
 * that contains the possible Id values / object fields.
 *
 * \ingroup wpspatype
 * \param id an id value
 * \param table (out) (optional): the associated WpSpaIdTable
 * \returns (transfer none): the type that is allowed in the array, if \a id
 *   represents an object field that takes an array as value
 */
WpSpaType
wp_spa_id_value_array_get_item_type (WpSpaIdValue id, WpSpaIdTable * table)
{
  g_return_val_if_fail (id != NULL, WP_SPA_TYPE_INVALID);

  const struct spa_type_info *info = id;
  g_return_val_if_fail (info->parent == SPA_TYPE_Array, WP_SPA_TYPE_INVALID);

  return info->values ?
      wp_spa_id_value_get_value_type (info->values, table) :
      WP_SPA_TYPE_INVALID;
}

/*!
 * \brief Initializes the spa dynamic type registry.
 *
 * This allows registering new spa types at runtime. The spa type system
 * still works if this function is not called.
 *
 * Normally called by wp_init() when WP_INIT_SPA_TYPES is passed in its flags.
 *
 * \ingroup wpspatype
 */
void
wp_spa_dynamic_type_init (void)
{
  extra_types = g_array_new (TRUE, FALSE, sizeof (struct spa_type_info));
  extra_id_tables = g_array_new (TRUE, FALSE, sizeof (WpSpaIdTableInfo));

  /* init to chain up to spa types */
  struct spa_type_info info = {
      SPA_ID_INVALID, SPA_ID_INVALID, "spa_types", SPA_TYPE_ROOT
  };
  g_array_append_val (extra_types, info);
}

/*!
 * \brief Deinitializes the spa type registry.
 *
 * You do not need to ever call this, unless you want to free memory at the
 * end of the execution of a test, so that it doesn't show as leaked in
 * the memory profiler.
 *
 * \ingroup wpspatype
 */
void
wp_spa_dynamic_type_deinit (void)
{
  g_clear_pointer (&extra_types, g_array_unref);
  g_clear_pointer (&extra_id_tables, g_array_unref);
}

/*!
 * \brief Registers an additional type in the spa type system.
 *
 * This is useful to add a custom pod object type.
 *
 * Note that both \a name and \a values must be statically allocated, or
 * otherwise guaranteed to be kept in memory until wp_spa_dynamic_type_deinit()
 * is called. No memory copy is done by this function.
 *
 * \ingroup wpspatype
 * \param name the name of the type
 * \param parent the parent type
 * \param values an array of `spa_type_info` that contains the values of the type,
 *   used only for Object types
 * \returns (transfer none): the new type
 */
WpSpaType
wp_spa_dynamic_type_register (const gchar *name, WpSpaType parent,
    const struct spa_type_info * values)
{
  struct spa_type_info info;
  info.type = SPA_TYPE_VENDOR_WirePlumber + extra_types->len;
  info.name = name;
  info.parent = parent;
  info.values = values;
  g_array_append_val (extra_types, info);
  return info.type;
}

/*!
 * \brief Registers an additional WpSpaIdTable in the spa type system.
 *
 * This is useful to add custom enumeration types.
 *
 * Note that both \a name and \a values must be statically allocated, or
 * otherwise guaranteed to be kept in memory until wp_spa_dynamic_type_deinit()
 * is called. No memory copy is done by this function.
 *
 * \ingroup wpspatype
 * \param name the name of the id table
 * \param values an array of `spa_type_info` that contains the values of the table
 * \returns the new table
 */
WpSpaIdTable
wp_spa_dynamic_id_table_register (const gchar *name,
    const struct spa_type_info * values)
{
  WpSpaIdTableInfo info;
  info.name = name;
  info.values = values;
  g_array_append_val (extra_id_tables, info);
  return values;
}
