/* GStreamer
 * Copyright (C) 2008 Sebastian Dröge <sebastian.droege@collabora.co.uk>
 *
 * gst-codec-info.c: tool to print automatic codec installation info
 *                   for a given list of plugins
 *
 * Partially based on gst-inspect from gstreamer.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */


#include <gst/gst.h>
#include <string.h>

static const gchar *virtual_packages[] = {
  "gstreamer1.0-audiosink",
  "gstreamer1.0-audiosource",
  "gstreamer1.0-videosink",
  "gstreamer1.0-videosource",
  "gstreamer1.0-visualization",
  NULL
};

static GList *elements = NULL;
static GList *uri_sources = NULL;
static GList *uri_sinks = NULL;
static GList *provides = NULL;
static GstCaps *encoders = NULL, *decoders = NULL;

static void
free_plugin_info (void)
{
  g_list_foreach (elements, (GFunc) g_free, NULL);
  g_list_foreach (uri_sources, (GFunc) g_free, NULL);
  g_list_foreach (uri_sinks, (GFunc) g_free, NULL);

  g_list_free (elements);
  g_list_free (uri_sources);
  g_list_free (uri_sinks);

  g_list_free (provides);

  gst_caps_unref (encoders);
  gst_caps_unref (decoders);
}

static void
print_plugin_info (void)
{
  GList *l;

  if (elements) {
    g_print ("gstreamer:Elements=");
    for (l = elements; l; l = l->next) {
      if (l->next)
        g_print ("%s, ", (gchar *) l->data);
      else
        g_print ("%s\n", (gchar *) l->data);
    }
  }

  if (provides) {
    g_print ("gstreamer:Provides=");
    for (l = provides; l; l = l->next) {
      if (l->next)
        g_print ("%s, ", (gchar *) l->data);
      else
        g_print ("%s\n", (gchar *) l->data);
    }
  }

  if (uri_sources) {
    g_print ("gstreamer:URISources=");
    for (l = uri_sources; l; l = l->next) {
      if (l->next)
        g_print ("%s, ", (gchar *) l->data);
      else
        g_print ("%s\n", (gchar *) l->data);
    }
  }

  if (uri_sinks) {
    g_print ("gstreamer:URISinks=");
    for (l = uri_sinks; l; l = l->next) {
      if (l->next)
        g_print ("%s, ", (gchar *) l->data);
      else
        g_print ("%s\n", (gchar *) l->data);
    }
  }

  if (!gst_caps_is_empty (encoders)) {
    gchar *caps = gst_caps_to_string (encoders);

    g_print ("gstreamer:Encoders=%s\n", caps);
    g_free (caps);
  }

  if (!gst_caps_is_empty (decoders)) {
    gchar *caps = gst_caps_to_string (decoders);

    g_print ("gstreamer:Decoders=%s\n", caps);
    g_free (caps);
  }
}

static void
remove_duplicates (GList * list, gboolean free)
{
  GList *l;
  gchar *previous;

  if (!list || !list->next)
    return;

  previous = list->data;
  l = list->next;

  while (l) {
    if (strcmp (l->data, previous) == 0) {
      GList *next = l->next;

      if (free)
        g_free (l->data);

      l = g_list_delete_link (l->prev, l);
      l = next;
    } else {
      previous = l->data;
      l = l->next;
    }
  }
}

static void
cleanup_plugin_info (void)
{
  if (encoders)
    encoders = gst_caps_simplify (encoders);

  if (decoders)
    decoders = gst_caps_simplify (decoders);

  elements = g_list_sort (elements, (GCompareFunc) strcmp);
  uri_sources = g_list_sort (uri_sources, (GCompareFunc) strcmp);
  uri_sinks = g_list_sort (uri_sinks, (GCompareFunc) strcmp);
  provides = g_list_sort (provides, (GCompareFunc) strcmp);

  remove_duplicates (elements, TRUE);
  remove_duplicates (uri_sources, TRUE);
  remove_duplicates (uri_sinks, TRUE);
  remove_duplicates (provides, FALSE);
}

static void
collect_uri_protocols (GstElementFactory * factory)
{
  const gchar *const *protocols, *const *p;

  protocols = gst_element_factory_get_uri_protocols (factory);
  if (!protocols)
    return;

  switch (gst_element_factory_get_uri_type (factory)) {
    case GST_URI_SINK:
      for (p = protocols; *p; p++)
        uri_sinks = g_list_prepend (uri_sinks, g_strdup (*p));
      break;
    case GST_URI_SRC:
      for (p = protocols; *p; p++)
        uri_sources = g_list_prepend (uri_sources, g_strdup (*p));
      break;
    default:
      break;
  }
}

static void
remove_min_max_fields (GstStructure * s)
{
  gint i, n;
  gboolean removed_field = FALSE;


  do {
    n = gst_structure_n_fields (s);
    removed_field = FALSE;
    for (i = 0; i < n; i++) {
      const gchar *field_name = gst_structure_nth_field_name (s, i);
      const GValue *field;

      field = gst_structure_get_value (s, field_name);

      if (GST_VALUE_HOLDS_INT_RANGE (field)) {
        gint min, max;

        min = gst_value_get_int_range_min (field);
        max = gst_value_get_int_range_max (field);

        if (min == 0 && max == G_MAXINT) {
          gst_structure_remove_field (s, field_name);
          removed_field = TRUE;
          break;
        }
      } else if (GST_VALUE_HOLDS_LIST (field)) {
        gint n2 = gst_value_list_get_size (field);

        if (n2 == 2) {
          const GValue *val1 = gst_value_list_get_value (field, 0);
          const GValue *val2 = gst_value_list_get_value (field, 1);

          if (G_VALUE_TYPE (val1) == G_TYPE_BOOLEAN
              && G_VALUE_TYPE (val2) == G_TYPE_BOOLEAN
              && ((g_value_get_boolean (val1) && !g_value_get_boolean (val2))
                  || (!g_value_get_boolean (val1)
                      && g_value_get_boolean (val2)))) {
            gst_structure_remove_field (s, field_name);
            removed_field = TRUE;
            break;
          }
        }
      } else if (GST_VALUE_HOLDS_ARRAY (field)) {
        gint n2 = gst_value_array_get_size (field);

        if (n2 == 2) {
          const GValue *val1 = gst_value_array_get_value (field, 0);
          const GValue *val2 = gst_value_array_get_value (field, 1);

          if (G_VALUE_TYPE (val1) == G_TYPE_BOOLEAN
              && G_VALUE_TYPE (val2) == G_TYPE_BOOLEAN
              && ((g_value_get_boolean (val1) && !g_value_get_boolean (val2))
                  || (!g_value_get_boolean (val1)
                      && g_value_get_boolean (val2)))) {
            gst_structure_remove_field (s, field_name);
            removed_field = TRUE;
            break;
          }
        }
      }
    }
  } while (removed_field);
}

static void
collect_codecs (GstElementFactory * factory)
{
  GstPadDirection direction;
  gboolean encoder;
  const gchar *klass;
  const GList *static_templates, *l;
  GstCaps *caps = NULL;
  gint i, n;

  klass = gst_element_factory_get_klass (factory);
  g_return_if_fail (klass);

  if (strstr (klass, "Demuxer") ||
      strstr (klass, "Decoder") ||
      strstr (klass, "Depay") || strstr (klass, "Parser")) {

    /* Ignore decoders with a less than marginal rank as they're
     * not autoplugged by playbin/decodebin */
    if (gst_plugin_feature_get_rank (GST_PLUGIN_FEATURE (factory)) <
        GST_RANK_MARGINAL)
      return;

    encoder = FALSE;
    direction = GST_PAD_SINK;
  } else if (strstr (klass, "Muxer") ||
      strstr (klass, "Encoder") || strstr (klass, "Pay")) {
    encoder = TRUE;
    direction = GST_PAD_SRC;
  } else if (strcmp (klass, "Sink/Audio") == 0) {
    provides = g_list_prepend (provides, (gchar *) virtual_packages[0]);
    return;
  } else if (strcmp (klass, "Source/Audio") == 0) {
    provides = g_list_prepend (provides, (gchar *) virtual_packages[1]);
    return;
  } else if (strcmp (klass, "Sink/Video") == 0) {
    provides = g_list_prepend (provides, (gchar *) virtual_packages[2]);
    return;
  } else if (strcmp (klass, "Source/Video") == 0) {
    provides = g_list_prepend (provides, (gchar *) virtual_packages[3]);
    return;
  } else if (strcmp (klass, "Visualization") == 0) {
    provides = g_list_prepend (provides, (gchar *) virtual_packages[4]);
    return;
  } else {
    return;
  }

  /* decoder/demuxer sink pads should always be static and there should only
   * be one, the same applies to encoders/muxers and source pads */
  static_templates = gst_element_factory_get_static_pad_templates (factory);
  for (l = static_templates; l; l = l->next) {
    GstStaticPadTemplate *tmpl = l->data;

    if (tmpl->direction == direction) {
      caps = gst_static_pad_template_get_caps (tmpl);
      break;
    }
  }

  if (caps == NULL) {
    g_printerr ("W: Couldn't find static pad template for '%s'\n",
        GST_OBJECT_NAME (factory));
    return;
  }

  caps = gst_caps_make_writable (caps);
  n = gst_caps_get_size (caps);
  for (i = 0; i < n; i++) {
    GstStructure *s = gst_caps_get_structure (caps, i);

    /* make caps easier to interpret, remove common fields that are likely
     * to be irrelevant for determining the right plugin (ie. mostly fields
     * where template caps usually have the standard MIN - MAX range as value) */
    gst_structure_remove_field (s, "codec_data");
    gst_structure_remove_field (s, "palette_data");
    gst_structure_remove_field (s, "pixel-aspect-ratio");
    gst_structure_remove_field (s, "framerate");
    gst_structure_remove_field (s, "leaf_size");
    gst_structure_remove_field (s, "packet_size");
    gst_structure_remove_field (s, "block_align");
    gst_structure_remove_field (s, "metadata-interval");        /* icy caps */
    /* decoders/encoders almost always handle the usual width/height/channel/rate
     * range (and if we don't remove this then the app will have a much harder
     * time blacklisting formats it has unsuccessfully tried to install before) */
    gst_structure_remove_field (s, "width");
    gst_structure_remove_field (s, "depth");
    gst_structure_remove_field (s, "height");
    gst_structure_remove_field (s, "channels");
    gst_structure_remove_field (s, "rate");
    /* rtp fields */
    gst_structure_remove_field (s, "config");
    gst_structure_remove_field (s, "clock-rate");
    gst_structure_remove_field (s, "clock-base");
    gst_structure_remove_field (s, "maxps");
    gst_structure_remove_field (s, "seqnum-base");
    gst_structure_remove_field (s, "npt-start");
    gst_structure_remove_field (s, "npt-stop");
    gst_structure_remove_field (s, "play-speed");
    gst_structure_remove_field (s, "play-scale");
    gst_structure_remove_field (s, "dynamic_range");

    remove_min_max_fields (s);

    gst_caps_append_structure ((encoder) ? encoders : decoders,
        gst_structure_copy (s));
  }

  gst_caps_unref (caps);
}

static void
collect_plugin_info (GstPlugin * plugin)
{
  GList *features, *l;
  const gchar *plugin_name;

  plugin_name = gst_plugin_get_name (plugin);

  features = gst_registry_get_feature_list (gst_registry_get (),
      GST_TYPE_ELEMENT_FACTORY);

  for (l = features; l; l = l->next) {
    GstPluginFeature *feature = GST_PLUGIN_FEATURE (l->data);
    GstElementFactory *factory = GST_ELEMENT_FACTORY (feature);
    GstPlugin *f_plugin = gst_plugin_feature_get_plugin (feature);

    if (!f_plugin)
      continue;
    if (!g_str_equal (plugin_name, gst_plugin_get_name (f_plugin))) {
      gst_object_unref (f_plugin);
      continue;
    }
    gst_object_unref (f_plugin);

    elements =
        g_list_prepend (elements,
        g_strdup (gst_plugin_feature_get_name (feature)));
    collect_uri_protocols (factory);
    collect_codecs (factory);
  }

  g_list_foreach (features, (GFunc) gst_object_unref, NULL);
  g_list_free (features);
}

int
main (int argc, char **argv)
{
  guint major, minor, micro, nano;
  gint i;

  gst_init (&argc, &argv);

  gst_version (&major, &minor, &micro, &nano);

  if (argc == 1)
    return 0;

  encoders = gst_caps_new_empty ();
  decoders = gst_caps_new_empty ();

  for (i = 1; i < argc; i++) {
    GstPlugin *plugin = NULL;
    GError *error = NULL;

    if (argv[i] == NULL ||
        !g_file_test (argv[i], G_FILE_TEST_EXISTS) ||
        !g_str_has_suffix (argv[i], G_MODULE_SUFFIX)) {
      g_printerr ("W: '%s' is no valid plugin filename\n", argv[i]);
      continue;
    }

    plugin = gst_plugin_load_file (argv[i], &error);

    if (!plugin) {
      g_printerr ("W: Could not load '%s': %s\n", argv[i], error->message);
      g_error_free (error);
      continue;
    }

    collect_plugin_info (plugin);
  }

  if (elements)
    g_print ("gstreamer:Version=%u.%u\n", major, minor);

  cleanup_plugin_info ();
  print_plugin_info ();
  free_plugin_info ();

  return 0;
}
