/* GStreamer
 * Copyright (C) 2009 Sebastian Dröge <sebastian.droege@collabora.co.uk>
 *
 * 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., 51 Franklin St, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "gstfrei0r.h"
#include "gstfrei0rfilter.h"
#include "gstfrei0rsrc.h"
#include "gstfrei0rmixer.h"

#include <string.h>
#include <gmodule.h>

GST_DEBUG_CATEGORY (frei0r_debug);
#define GST_CAT_DEFAULT frei0r_debug

static GstStaticCaps bgra8888_caps = GST_STATIC_CAPS (GST_VIDEO_CAPS_MAKE
    ("BGRA"));
static GstStaticCaps rgba8888_caps = GST_STATIC_CAPS (GST_VIDEO_CAPS_MAKE
    ("RGBA"));
static GstStaticCaps packed32_caps = GST_STATIC_CAPS (GST_VIDEO_CAPS_MAKE
    ("{ BGRA, RGBA, ABGR, ARGB, BGRx, RGBx, xBGR, xRGB, AYUV }"));

GstCaps *
gst_frei0r_caps_from_color_model (gint color_model)
{
  switch (color_model) {
    case F0R_COLOR_MODEL_BGRA8888:
      return gst_static_caps_get (&bgra8888_caps);
    case F0R_COLOR_MODEL_RGBA8888:
      return gst_static_caps_get (&rgba8888_caps);
    case F0R_COLOR_MODEL_PACKED32:
      return gst_static_caps_get (&packed32_caps);
    default:
      break;
  }

  return NULL;
}

void
gst_frei0r_klass_install_properties (GObjectClass * gobject_class,
    GstFrei0rFuncTable * ftable, GstFrei0rProperty * properties,
    gint n_properties)
{
  gint i, count = 1;
  f0r_instance_t *instance = ftable->construct (640, 480);

  g_assert (instance);

  for (i = 0; i < n_properties; i++) {
    f0r_param_info_t *param_info = &properties[i].info;
    gchar *prop_name;

    ftable->get_param_info (param_info, i);

    if (!param_info->name) {
      GST_ERROR ("Property %d of %s without a valid name", i,
          g_type_name (G_TYPE_FROM_CLASS (gobject_class)));
      continue;
    }

    prop_name = g_ascii_strdown (param_info->name, -1);
    g_strcanon (prop_name, G_CSET_A_2_Z G_CSET_a_2_z G_CSET_DIGITS "-+", '-');
    /* satisfy glib2 (argname[0] must be [A-Za-z]) */
    if (!((prop_name[0] >= 'a' && prop_name[0] <= 'z') ||
            (prop_name[0] >= 'A' && prop_name[0] <= 'Z'))) {
      gchar *tempstr = prop_name;

      prop_name = g_strconcat ("param-", prop_name, NULL);
      g_free (tempstr);
    }

    properties[i].prop_id = count;
    properties[i].prop_idx = i;

    ftable->get_param_value (instance, &properties[i].default_value, i);
    if (param_info->type == F0R_PARAM_STRING)
      properties[i].default_value.data.s =
          g_strdup (properties[i].default_value.data.s);

    switch (param_info->type) {
      case F0R_PARAM_BOOL:
        g_object_class_install_property (gobject_class, count++,
            g_param_spec_boolean (prop_name, param_info->name,
                param_info->explanation,
                properties[i].default_value.data.b ? TRUE : FALSE,
                G_PARAM_READWRITE | GST_PARAM_CONTROLLABLE));
        properties[i].n_prop_ids = 1;
        break;
      case F0R_PARAM_DOUBLE:{
        gdouble def = properties[i].default_value.data.d;

        /* If the default is NAN, +-INF we use 0.0 */
        if (!(def >= 0.0 && def <= 1.0))
          def = 0.0;

        g_object_class_install_property (gobject_class, count++,
            g_param_spec_double (prop_name, param_info->name,
                param_info->explanation, 0.0, 1.0, def,
                G_PARAM_READWRITE | GST_PARAM_CONTROLLABLE));
        properties[i].n_prop_ids = 1;
        break;
      }
      case F0R_PARAM_STRING:
        g_object_class_install_property (gobject_class, count++,
            g_param_spec_string (prop_name, param_info->name,
                param_info->explanation, properties[i].default_value.data.s,
                G_PARAM_READWRITE | GST_PARAM_CONTROLLABLE));
        properties[i].n_prop_ids = 1;
        break;
      case F0R_PARAM_COLOR:{
        gchar *prop_name_full;
        gchar *prop_nick_full;
        gdouble def;

        def = properties[i].default_value.data.color.r;
        /* If the default is out of range we use 0.0 */
        if (!(def <= 1.0 && def >= 0.0))
          def = 0.0;
        prop_name_full = g_strconcat (prop_name, "-r", NULL);
        prop_nick_full = g_strconcat (param_info->name, " (R)", NULL);
        g_object_class_install_property (gobject_class, count++,
            g_param_spec_float (prop_name_full, prop_nick_full,
                param_info->explanation, 0.0, 1.0, def,
                G_PARAM_READWRITE | GST_PARAM_CONTROLLABLE));
        g_free (prop_name_full);
        g_free (prop_nick_full);

        def = properties[i].default_value.data.color.g;
        /* If the default is out of range we use 0.0 */
        if (!(def <= 1.0 && def >= 0.0))
          def = 0.0;
        prop_name_full = g_strconcat (prop_name, "-g", NULL);
        prop_nick_full = g_strconcat (param_info->name, " (G)", NULL);
        g_object_class_install_property (gobject_class, count++,
            g_param_spec_float (prop_name_full, prop_nick_full,
                param_info->explanation, 0.0, 1.0, def,
                G_PARAM_READWRITE | GST_PARAM_CONTROLLABLE));
        g_free (prop_name_full);
        g_free (prop_nick_full);

        def = properties[i].default_value.data.color.b;
        /* If the default is out of range we use 0.0 */
        if (!(def <= 1.0 && def >= 0.0))
          def = 0.0;
        prop_name_full = g_strconcat (prop_name, "-b", NULL);
        prop_nick_full = g_strconcat (param_info->name, " (B)", NULL);
        g_object_class_install_property (gobject_class, count++,
            g_param_spec_float (prop_name_full, prop_nick_full,
                param_info->explanation, 0.0, 1.0, def,
                G_PARAM_READWRITE | GST_PARAM_CONTROLLABLE));
        g_free (prop_name_full);
        g_free (prop_nick_full);

        properties[i].n_prop_ids = 3;
        break;
      }
      case F0R_PARAM_POSITION:{
        gchar *prop_name_full;
        gchar *prop_nick_full;
        gdouble def;

        def = properties[i].default_value.data.position.x;
        /* If the default is out of range we use 0.0 */
        if (!(def <= 1.0 && def >= 0.0))
          def = 0.0;
        prop_name_full = g_strconcat (prop_name, "-x", NULL);
        prop_nick_full = g_strconcat (param_info->name, " (X)", NULL);
        g_object_class_install_property (gobject_class, count++,
            g_param_spec_double (prop_name_full, prop_nick_full,
                param_info->explanation, 0.0, 1.0, def,
                G_PARAM_READWRITE | GST_PARAM_CONTROLLABLE));
        g_free (prop_name_full);
        g_free (prop_nick_full);

        def = properties[i].default_value.data.position.y;
        /* If the default is out of range we use 0.0 */
        if (!(def <= 1.0 && def >= 0.0))
          def = 0.0;
        prop_name_full = g_strconcat (prop_name, "-Y", NULL);
        prop_nick_full = g_strconcat (param_info->name, " (Y)", NULL);
        g_object_class_install_property (gobject_class, count++,
            g_param_spec_double (prop_name_full, prop_nick_full,
                param_info->explanation, 0.0, 1.0, def,
                G_PARAM_READWRITE | GST_PARAM_CONTROLLABLE));
        g_free (prop_name_full);
        g_free (prop_nick_full);

        properties[i].n_prop_ids = 2;
        break;
      }
      default:
        g_assert_not_reached ();
        break;
    }

    g_free (prop_name);
  }

  ftable->destruct (instance);
}

GstFrei0rPropertyValue *
gst_frei0r_property_cache_init (GstFrei0rProperty * properties,
    gint n_properties)
{
  gint i;
  GstFrei0rPropertyValue *ret = g_new0 (GstFrei0rPropertyValue, n_properties);

  for (i = 0; i < n_properties; i++) {
    memcpy (&ret[i].data, &properties[i].default_value,
        sizeof (GstFrei0rPropertyValue));

    if (properties[i].info.type == F0R_PARAM_STRING)
      ret[i].data.s = g_strdup (ret[i].data.s);
  }

  return ret;
}

void
gst_frei0r_property_cache_free (GstFrei0rProperty * properties,
    GstFrei0rPropertyValue * property_cache, gint n_properties)
{
  gint i;

  for (i = 0; i < n_properties; i++) {
    if (properties[i].info.type == F0R_PARAM_STRING)
      g_free (property_cache[i].data.s);
  }
  g_free (property_cache);
}

f0r_instance_t *
gst_frei0r_instance_construct (GstFrei0rFuncTable * ftable,
    GstFrei0rProperty * properties, gint n_properties,
    GstFrei0rPropertyValue * property_cache, gint width, gint height)
{
  f0r_instance_t *instance = ftable->construct (width, height);
  gint i;

  for (i = 0; i < n_properties; i++) {
    if (properties[i].info.type == F0R_PARAM_STRING)
      ftable->set_param_value (instance, property_cache[i].data.s, i);
    else
      ftable->set_param_value (instance, &property_cache[i].data, i);
  }

  return instance;
}

gboolean
gst_frei0r_get_property (f0r_instance_t * instance, GstFrei0rFuncTable * ftable,
    GstFrei0rProperty * properties, gint n_properties,
    GstFrei0rPropertyValue * property_cache, guint prop_id, GValue * value)
{
  gint i;
  GstFrei0rProperty *prop = NULL;

  for (i = 0; i < n_properties; i++) {
    if (properties[i].prop_id <= prop_id &&
        properties[i].prop_id + properties[i].n_prop_ids > prop_id) {
      prop = &properties[i];
      break;
    }
  }

  if (!prop)
    return FALSE;

  switch (prop->info.type) {
    case F0R_PARAM_BOOL:{
      gdouble d;

      if (instance)
        ftable->get_param_value (instance, &d, prop->prop_idx);
      else
        d = property_cache[prop->prop_idx].data.b;

      g_value_set_boolean (value, (d < 0.5) ? FALSE : TRUE);
      break;
    }
    case F0R_PARAM_DOUBLE:{
      gdouble d;

      if (instance)
        ftable->get_param_value (instance, &d, prop->prop_idx);
      else
        d = property_cache[prop->prop_idx].data.d;

      g_value_set_double (value, d);
      break;
    }
    case F0R_PARAM_STRING:{
      const gchar *s;

      if (instance)
        ftable->get_param_value (instance, &s, prop->prop_idx);
      else
        s = property_cache[prop->prop_idx].data.s;
      g_value_set_string (value, s);
      break;
    }
    case F0R_PARAM_COLOR:{
      f0r_param_color_t color;

      if (instance)
        ftable->get_param_value (instance, &color, prop->prop_idx);
      else
        color = property_cache[prop->prop_idx].data.color;

      switch (prop_id - prop->prop_id) {
        case 0:
          g_value_set_float (value, color.r);
          break;
        case 1:
          g_value_set_float (value, color.g);
          break;
        case 2:
          g_value_set_float (value, color.b);
          break;
      }
      break;
    }
    case F0R_PARAM_POSITION:{
      f0r_param_position_t position;

      if (instance)
        ftable->get_param_value (instance, &position, prop->prop_idx);
      else
        position = property_cache[prop->prop_idx].data.position;

      switch (prop_id - prop->prop_id) {
        case 0:
          g_value_set_double (value, position.x);
          break;
        case 1:
          g_value_set_double (value, position.y);
          break;
      }
      break;
    }
    default:
      g_assert_not_reached ();
      break;
  }

  return TRUE;
}

gboolean
gst_frei0r_set_property (f0r_instance_t * instance, GstFrei0rFuncTable * ftable,
    GstFrei0rProperty * properties, gint n_properties,
    GstFrei0rPropertyValue * property_cache, guint prop_id,
    const GValue * value)
{
  GstFrei0rProperty *prop = NULL;
  gint i;

  for (i = 0; i < n_properties; i++) {
    if (properties[i].prop_id <= prop_id &&
        properties[i].prop_id + properties[i].n_prop_ids > prop_id) {
      prop = &properties[i];
      break;
    }
  }

  if (!prop)
    return FALSE;

  switch (prop->info.type) {
    case F0R_PARAM_BOOL:{
      gboolean b = g_value_get_boolean (value);
      gdouble d = b ? 1.0 : 0.0;

      if (instance)
        ftable->set_param_value (instance, &d, prop->prop_idx);
      property_cache[prop->prop_idx].data.b = d;
      break;
    }
    case F0R_PARAM_DOUBLE:{
      gdouble d = g_value_get_double (value);

      if (instance)
        ftable->set_param_value (instance, &d, prop->prop_idx);
      property_cache[prop->prop_idx].data.d = d;
      break;
    }
    case F0R_PARAM_STRING:{
      gchar *s = g_value_dup_string (value);

      /* Copies the string */
      if (instance)
        ftable->set_param_value (instance, s, prop->prop_idx);
      property_cache[prop->prop_idx].data.s = s;
      break;
    }
    case F0R_PARAM_COLOR:{
      gfloat f = g_value_get_float (value);
      f0r_param_color_t *color = &property_cache[prop->prop_idx].data.color;

      switch (prop_id - prop->prop_id) {
        case 0:
          color->r = f;
          break;
        case 1:
          color->g = f;
          break;
        case 2:
          color->b = f;
          break;
        default:
          g_assert_not_reached ();
      }

      if (instance)
        ftable->set_param_value (instance, color, prop->prop_idx);
      break;
    }
    case F0R_PARAM_POSITION:{
      gdouble d = g_value_get_double (value);
      f0r_param_position_t *position =
          &property_cache[prop->prop_idx].data.position;

      switch (prop_id - prop->prop_id) {
        case 0:
          position->x = d;
          break;
        case 1:
          position->y = d;
          break;
        default:
          g_assert_not_reached ();
      }
      if (instance)
        ftable->set_param_value (instance, position, prop->prop_idx);
      break;
    }
    default:
      g_assert_not_reached ();
      break;
  }

  return TRUE;
}

static gboolean
register_plugin (GstPlugin * plugin, const gchar * vendor,
    const gchar * filename)
{
  GModule *module;
  GstFrei0rPluginRegisterReturn ret = GST_FREI0R_PLUGIN_REGISTER_RETURN_FAILED;
  GstFrei0rFuncTable ftable = { NULL, };
  gint i;
  f0r_plugin_info_t info = { NULL, };
  f0r_instance_t *instance = NULL;

  GST_DEBUG ("Registering plugin '%s'", filename);

  module = g_module_open (filename, G_MODULE_BIND_LAZY | G_MODULE_BIND_LOCAL);
  if (!module) {
    GST_WARNING ("Failed to load plugin");
    return FALSE;
  }

  if (!g_module_symbol (module, "f0r_init", (gpointer *) & ftable.init)) {
    GST_INFO ("No frei0r plugin");
    g_module_close (module);
    return FALSE;
  }

  if (!g_module_symbol (module, "f0r_deinit", (gpointer *) & ftable.deinit) ||
      !g_module_symbol (module, "f0r_construct",
          (gpointer *) & ftable.construct)
      || !g_module_symbol (module, "f0r_destruct",
          (gpointer *) & ftable.destruct)
      || !g_module_symbol (module, "f0r_get_plugin_info",
          (gpointer *) & ftable.get_plugin_info)
      || !g_module_symbol (module, "f0r_get_param_info",
          (gpointer *) & ftable.get_param_info)
      || !g_module_symbol (module, "f0r_set_param_value",
          (gpointer *) & ftable.set_param_value)
      || !g_module_symbol (module, "f0r_get_param_value",
          (gpointer *) & ftable.get_param_value))
    goto invalid_frei0r_plugin;

  /* One of these must exist */
  g_module_symbol (module, "f0r_update", (gpointer *) & ftable.update);
  g_module_symbol (module, "f0r_update2", (gpointer *) & ftable.update2);

  if (!ftable.init ()) {
    GST_WARNING ("Failed to initialize plugin");
    g_module_close (module);
    return FALSE;
  }

  if (!ftable.update && !ftable.update2)
    goto invalid_frei0r_plugin;

  ftable.get_plugin_info (&info);

  if (info.frei0r_version > 1) {
    GST_WARNING ("Unsupported frei0r version %d", info.frei0r_version);
    ftable.deinit ();
    g_module_close (module);
    return FALSE;
  }

  if (info.color_model > F0R_COLOR_MODEL_PACKED32) {
    GST_WARNING ("Unsupported color model %d", info.color_model);
    ftable.deinit ();
    g_module_close (module);
    return FALSE;
  }

  for (i = 0; i < info.num_params; i++) {
    f0r_param_info_t pinfo = { NULL, };

    ftable.get_param_info (&pinfo, i);
    if (pinfo.type > F0R_PARAM_STRING) {
      GST_WARNING ("Unsupported parameter type %d", pinfo.type);
      ftable.deinit ();
      g_module_close (module);
      return FALSE;
    }
  }

  instance = ftable.construct (640, 480);
  if (!instance) {
    GST_WARNING ("Failed to instanciate plugin '%s'", info.name);
    ftable.deinit ();
    g_module_close (module);
    return FALSE;
  }
  ftable.destruct (instance);

  switch (info.plugin_type) {
    case F0R_PLUGIN_TYPE_FILTER:
      ret = gst_frei0r_filter_register (plugin, vendor, &info, &ftable);
      break;
    case F0R_PLUGIN_TYPE_SOURCE:
      ret = gst_frei0r_src_register (plugin, vendor, &info, &ftable);
      break;
    case F0R_PLUGIN_TYPE_MIXER2:
    case F0R_PLUGIN_TYPE_MIXER3:
      ret = gst_frei0r_mixer_register (plugin, vendor, &info, &ftable);
      break;
    default:
      break;
  }

  switch (ret) {
    case GST_FREI0R_PLUGIN_REGISTER_RETURN_OK:
      return TRUE;
    case GST_FREI0R_PLUGIN_REGISTER_RETURN_FAILED:
      GST_ERROR ("Failed to register frei0r plugin");
      ftable.deinit ();
      g_module_close (module);
      return FALSE;
    case GST_FREI0R_PLUGIN_REGISTER_RETURN_ALREADY_REGISTERED:
      GST_DEBUG ("frei0r plugin already registered");
      ftable.deinit ();
      g_module_close (module);
      return TRUE;
    default:
      g_return_val_if_reached (FALSE);
  }

  g_return_val_if_reached (FALSE);

invalid_frei0r_plugin:
  GST_ERROR ("Invalid frei0r plugin");
  ftable.deinit ();
  g_module_close (module);

  return FALSE;
}

static gboolean
register_plugins (GstPlugin * plugin, GHashTable * plugin_names,
    const gchar * path, const gchar * base_path)
{
  GDir *dir;
  gchar *filename;
  const gchar *entry_name;
  gboolean ret = TRUE;

  GST_DEBUG ("Scanning directory '%s' for frei0r plugins", path);

  dir = g_dir_open (path, 0, NULL);
  if (!dir)
    return FALSE;

  while ((entry_name = g_dir_read_name (dir))) {
    gchar *tmp, *vendor = NULL;
    gchar *hashtable_name;

    tmp = g_strdup (path + strlen (base_path));
    if (*tmp == G_DIR_SEPARATOR && *(tmp + 1))
      vendor = tmp + 1;
    else if (*tmp)
      vendor = tmp;

    if (vendor)
      hashtable_name = g_strconcat (vendor, "-", entry_name, NULL);
    else
      hashtable_name = g_strdup (entry_name);

    if (g_hash_table_lookup_extended (plugin_names, hashtable_name, NULL, NULL)) {
      g_free (hashtable_name);
      continue;
    }

    filename = g_build_filename (path, entry_name, NULL);
    if ((g_str_has_suffix (filename, G_MODULE_SUFFIX)
#ifdef GST_EXTRA_MODULE_SUFFIX
            || g_str_has_suffix (filename, GST_EXTRA_MODULE_SUFFIX)
#endif
        ) && g_file_test (filename, G_FILE_TEST_IS_REGULAR)) {
      gboolean this_ret;

      this_ret = register_plugin (plugin, vendor, filename);
      if (this_ret)
        g_hash_table_insert (plugin_names, g_strdup (hashtable_name), NULL);

      ret = ret && this_ret;
    } else if (g_file_test (filename, G_FILE_TEST_IS_DIR)) {
      ret = ret && register_plugins (plugin, plugin_names, filename, base_path);
    }
    g_free (filename);
    g_free (hashtable_name);
    g_free (tmp);
  }
  g_dir_close (dir);

  return ret;
}

static gboolean
plugin_init (GstPlugin * plugin)
{
  const gchar *homedir;
  gchar *path, *libdir_path;
  GHashTable *plugin_names;
  const gchar *frei0r_path;

  GST_DEBUG_CATEGORY_INIT (frei0r_debug, "frei0r", 0, "frei0r");

  gst_plugin_add_dependency_simple (plugin,
      "FREI0R_PATH:HOME/.frei0r-1/lib",
      LIBDIR "/frei0r-1:"
      "/usr/lib/frei0r-1:/usr/local/lib/frei0r-1:"
      "/usr/lib32/frei0r-1:/usr/local/lib32/frei0r-1:"
      "/usr/lib64/frei0r-1:/usr/local/lib64/frei0r-1",
      NULL, GST_PLUGIN_DEPENDENCY_FLAG_RECURSE);

  plugin_names =
      g_hash_table_new_full ((GHashFunc) g_str_hash, (GEqualFunc) g_str_equal,
      (GDestroyNotify) g_free, NULL);

  frei0r_path = g_getenv ("FREI0R_PATH");
  if (frei0r_path && *frei0r_path) {
    gchar **p, **paths = g_strsplit (frei0r_path, ":", -1);

    for (p = paths; *p; p++) {
      register_plugins (plugin, plugin_names, *p, *p);
    }

    g_strfreev (paths);
  } else {
#define register_plugins2(plugin, pn, p) register_plugins(plugin, pn, p, p)
    homedir = g_get_home_dir ();
    path = g_build_filename (homedir, ".frei0r-1", "lib", NULL);
    libdir_path = g_build_filename (LIBDIR, "frei0r-1", NULL);
    register_plugins2 (plugin, plugin_names, path);
    g_free (path);
    register_plugins2 (plugin, plugin_names, libdir_path);
    g_free (libdir_path);
    register_plugins2 (plugin, plugin_names, "/usr/local/lib/frei0r-1");
    register_plugins2 (plugin, plugin_names, "/usr/lib/frei0r-1");
    register_plugins2 (plugin, plugin_names, "/usr/local/lib32/frei0r-1");
    register_plugins2 (plugin, plugin_names, "/usr/lib32/frei0r-1");
    register_plugins2 (plugin, plugin_names, "/usr/local/lib64/frei0r-1");
    register_plugins2 (plugin, plugin_names, "/usr/lib64/frei0r-1");
#undef register_plugins2
  }

  g_hash_table_unref (plugin_names);

  return TRUE;
}

GST_PLUGIN_DEFINE (GST_VERSION_MAJOR,
    GST_VERSION_MINOR,
    frei0r,
    "frei0r plugin library",
    plugin_init, VERSION, "LGPL", GST_PACKAGE_NAME, GST_PACKAGE_ORIGIN)