/* GStreamer encoding profile registry
 * Copyright (C) 2010 Edward Hervey <edward.hervey@collabora.co.uk>
 *           (C) 2010 Nokia Corporation
 *
 * 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.
 */
/**
 * SECTION:encoding-target
 *
 * On top of the notion of profiles, we implement the notion of EncodingTarget.
 * Encoding Targets are basically a higher level of abstraction to define formats
 * for specific target types. Those can define several GstEncodingProfiles with
 * different names, for example one for transcoding in full HD, another one for
 * low res, etc.. which are defined in the same encoding target.
 *
 * Basically if you want to encode a stream to send it to, say, youtube you should
 * have a Youtube encoding target defined in the "online-service" category.
 *
 * ## Encoding target serialization format
 *
 * Encoding targets are serialized in a KeyFile like files.
 *
 * |[
 * [GStreamer Encoding Target]
 * name : <name>
 * category : <category>
 * \description : <description> #translatable
 *
 * [profile-<profile1name>]
 * name : <name>
 * \description : <description> #optional
 * format : <format>
 * preset : <preset>
 *
 * [streamprofile-<id>]
 * parent : <encodingprofile.name>[,<encodingprofile.name>..]
 * \type : <type> # "audio", "video", "text"
 * format : <format>
 * preset : <preset>
 * restriction : <restriction>
 * presence : <presence>
 * pass : <pass>
 * variableframerate : <variableframerate>
 * ]|
 *
 * ## Location of encoding target files
 *
 * $GST_DATADIR/gstreamer-GST_API_VERSION/encoding-profile
 * $HOME/gstreamer-GST_API_VERSION/encoding-profile
 *
 * There also is a GST_ENCODING_TARGET_PATH environment variable
 * defining a list of folder containing encoding target files.
 *
 * ## Naming convention
 *
 * |[
 *   $(target.category)/$(target.name).gep
 * ]|
 *
 * ## Naming restrictions:
 *
 *  * lowercase ASCII letter for the first character
 *  * Same for all other characters + numerics + hyphens
 */

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

#include <locale.h>
#include <errno.h>
#include <string.h>
#include "encoding-target.h"
#include "pbutils-private.h"

/* Documented in encoding-profile.c */

#ifndef GST_DISABLE_GST_DEBUG
#define GST_CAT_DEFAULT gst_pb_utils_encoding_target_ensure_debug_category()

static GstDebugCategory *
gst_pb_utils_encoding_target_ensure_debug_category (void)
{
  static gsize cat_gonce = 0;

  if (g_once_init_enter (&cat_gonce)) {
    GstDebugCategory *cat = NULL;

    GST_DEBUG_CATEGORY_INIT (cat, "encoding-target", 0,
        "GstPbUtils encoding target");

    g_once_init_leave (&cat_gonce, (gsize) cat);
  }

  return (GstDebugCategory *) cat_gonce;
}
#endif /* GST_DISABLE_GST_DEBUG */

#define GST_ENCODING_TARGET_HEADER "GStreamer Encoding Target"
#define GST_ENCODING_TARGET_DIRECTORY "encoding-profiles"
#define GST_ENCODING_TARGET_SUFFIX ".gep"

struct _GstEncodingTarget
{
  GObject parent;

  gchar *name;
  gchar *category;
  gchar *description;
  gchar *path;
  GList *profiles;

  /*< private > */
  gchar *keyfile;
};

G_DEFINE_TYPE (GstEncodingTarget, gst_encoding_target, G_TYPE_OBJECT);

static void
gst_encoding_target_init (GstEncodingTarget * target)
{
  /* Nothing to initialize */
}

static void
gst_encoding_target_finalize (GObject * object)
{
  GstEncodingTarget *target = (GstEncodingTarget *) object;

  GST_DEBUG ("Finalizing");

  g_free (target->name);
  g_free (target->category);
  g_free (target->description);
  g_free (target->path);

  g_list_foreach (target->profiles, (GFunc) g_object_unref, NULL);
  g_list_free (target->profiles);
}

static void
gst_encoding_target_class_init (GObjectClass * klass)
{
  klass->finalize = gst_encoding_target_finalize;
}

/**
 * gst_encoding_target_get_name:
 * @target: a #GstEncodingTarget
 *
 * Returns: (transfer none): The name of the @target.
 */
const gchar *
gst_encoding_target_get_name (GstEncodingTarget * target)
{
  return target->name;
}

/**
 * gst_encoding_target_get_category:
 * @target: a #GstEncodingTarget
 *
 * Returns: (transfer none): The category of the @target. For example:
 * #GST_ENCODING_CATEGORY_DEVICE.
 */
const gchar *
gst_encoding_target_get_category (GstEncodingTarget * target)
{
  return target->category;
}

/**
 * gst_encoding_target_get_description:
 * @target: a #GstEncodingTarget
 *
 * Returns: (transfer none): The description of the @target.
 */
const gchar *
gst_encoding_target_get_description (GstEncodingTarget * target)
{
  return target->description;
}

/**
 * gst_encoding_target_get_path:
 * @target: a #GstEncodingTarget
 *
 * Returns: (transfer none) (nullable) (type filename): The path to the @target file.
 *
 * Since: 1.18
 */
const gchar *
gst_encoding_target_get_path (GstEncodingTarget * target)
{
  g_return_val_if_fail (GST_IS_ENCODING_TARGET (target), NULL);
  return target->path;
}

/**
 * gst_encoding_target_get_profiles:
 * @target: a #GstEncodingTarget
 *
 * Returns: (transfer none) (element-type GstPbutils.EncodingProfile): A list of
 * #GstEncodingProfile(s) this @target handles.
 */
const GList *
gst_encoding_target_get_profiles (GstEncodingTarget * target)
{
  return target->profiles;
}

/**
 * gst_encoding_target_get_profile:
 * @target: a #GstEncodingTarget
 * @name: the name of the profile to retrieve
 *
 * Returns: (nullable) (transfer full): The matching #GstEncodingProfile, or %NULL.
 */
GstEncodingProfile *
gst_encoding_target_get_profile (GstEncodingTarget * target, const gchar * name)
{
  GList *tmp;

  g_return_val_if_fail (GST_IS_ENCODING_TARGET (target), NULL);
  g_return_val_if_fail (name != NULL, NULL);

  for (tmp = target->profiles; tmp; tmp = tmp->next) {
    GstEncodingProfile *tprof = (GstEncodingProfile *) tmp->data;

    if (!g_strcmp0 (gst_encoding_profile_get_name (tprof), name)) {
      gst_encoding_profile_ref (tprof);
      return tprof;
    }
  }

  return NULL;
}

static inline gboolean
validate_name (const gchar * name)
{
  guint i, len;

  len = strlen (name);
  if (len == 0)
    return FALSE;

  /* First character can only be a lower case ASCII character */
  if (!g_ascii_isalpha (name[0]) || !g_ascii_islower (name[0]))
    return FALSE;

  /* All following characters can only by:
   * either a lower case ASCII character
   * or an hyphen
   * or a numeric */
  for (i = 1; i < len; i++) {
    /* if uppercase ASCII letter, return */
    if (g_ascii_isupper (name[i]))
      return FALSE;
    /* if a digit, continue */
    if (g_ascii_isdigit (name[i]))
      continue;
    /* if an hyphen, continue */
    if (name[i] == '-')
      continue;
    /* if an ';', continue (list delimiter) */
    if (name[i] == ';') {
      continue;
    }
    /* remaining should only be ascii letters */
    if (!g_ascii_isalpha (name[i]))
      return FALSE;
  }

  return TRUE;
}

/**
 * gst_encoding_target_new:
 * @name: The name of the target.
 * @category: (transfer none): The name of the category to which this @target
 * belongs. For example: #GST_ENCODING_CATEGORY_DEVICE.
 * @description: (transfer none): A description of #GstEncodingTarget in the
 * current locale.
 * @profiles: (transfer none) (element-type GstPbutils.EncodingProfile): A #GList of
 * #GstEncodingProfile.
 *
 * Creates a new #GstEncodingTarget.
 *
 * The name and category can only consist of lowercase ASCII letters for the
 * first character, followed by either lowercase ASCII letters, digits or
 * hyphens ('-').
 *
 * The @category *should* be one of the existing
 * well-defined categories, like #GST_ENCODING_CATEGORY_DEVICE, but it
 * *can* be a application or user specific category if
 * needed.
 *
 * Returns: (transfer full) (nullable): The newly created #GstEncodingTarget or %NULL if
 * there was an error.
 */

GstEncodingTarget *
gst_encoding_target_new (const gchar * name, const gchar * category,
    const gchar * description, const GList * profiles)
{
  GstEncodingTarget *res;

  g_return_val_if_fail (name != NULL, NULL);
  g_return_val_if_fail (category != NULL, NULL);
  g_return_val_if_fail (description != NULL, NULL);

  /* Validate name */
  if (!validate_name (name))
    goto invalid_name;
  if (category && !validate_name (category))
    goto invalid_category;

  res = (GstEncodingTarget *) g_object_new (GST_TYPE_ENCODING_TARGET, NULL);
  res->name = g_strdup (name);
  res->category = g_strdup (category);
  res->description = g_strdup (description);

  while (profiles) {
    GstEncodingProfile *prof = (GstEncodingProfile *) profiles->data;

    res->profiles =
        g_list_append (res->profiles, gst_encoding_profile_ref (prof));
    profiles = profiles->next;
  }

  return res;

invalid_name:
  {
    GST_ERROR ("Invalid name for encoding target : '%s'", name);
    return NULL;
  }

invalid_category:
  {
    GST_ERROR ("Invalid name for encoding category : '%s'", category);
    return NULL;
  }
}

/**
 * gst_encoding_target_add_profile:
 * @target: the #GstEncodingTarget to add a profile to
 * @profile: (transfer full): the #GstEncodingProfile to add
 *
 * Adds the given @profile to the @target. Each added profile must have
 * a unique name within the profile.
 *
 * The @target will steal a reference to the @profile. If you wish to use
 * the profile after calling this method, you should increase its reference
 * count.
 *
 * Returns: %TRUE if the profile was added, else %FALSE.
 **/

gboolean
gst_encoding_target_add_profile (GstEncodingTarget * target,
    GstEncodingProfile * profile)
{
  GList *tmp;

  g_return_val_if_fail (GST_IS_ENCODING_TARGET (target), FALSE);
  g_return_val_if_fail (GST_IS_ENCODING_PROFILE (profile), FALSE);

  /* Make sure profile isn't already controlled by this target */
  for (tmp = target->profiles; tmp; tmp = tmp->next) {
    GstEncodingProfile *prof = (GstEncodingProfile *) tmp->data;

    if (!g_strcmp0 (gst_encoding_profile_get_name (profile),
            gst_encoding_profile_get_name (prof))) {
      GST_WARNING ("Profile already present in target");
      return FALSE;
    }
  }

  target->profiles = g_list_append (target->profiles, profile);

  return TRUE;
}

static gboolean
serialize_stream_profiles (GKeyFile * out, GstEncodingProfile * sprof,
    const gchar * profilename, guint id)
{
  gchar *sprofgroupname;
  gchar *tmpc;
  GstCaps *format, *restriction;
  const gchar *preset, *name, *description;

  sprofgroupname = g_strdup_printf ("streamprofile-%s-%d", profilename, id);

  /* Write the parent profile */
  g_key_file_set_value (out, sprofgroupname, "parent", profilename);

  g_key_file_set_value (out, sprofgroupname, "type",
      gst_encoding_profile_get_type_nick (sprof));

  format = gst_encoding_profile_get_format (sprof);
  if (format) {
    tmpc = gst_caps_to_string (format);
    g_key_file_set_value (out, sprofgroupname, "format", tmpc);
    g_free (tmpc);
  }

  name = gst_encoding_profile_get_name (sprof);
  if (name)
    g_key_file_set_string (out, sprofgroupname, "name", name);

  description = gst_encoding_profile_get_description (sprof);
  if (description)
    g_key_file_set_string (out, sprofgroupname, "description", description);

  preset = gst_encoding_profile_get_preset (sprof);
  if (preset)
    g_key_file_set_string (out, sprofgroupname, "preset", preset);

  restriction = gst_encoding_profile_get_restriction (sprof);
  if (restriction) {
    tmpc = gst_caps_to_string (restriction);
    g_key_file_set_value (out, sprofgroupname, "restriction", tmpc);
    g_free (tmpc);
  }
  g_key_file_set_integer (out, sprofgroupname, "presence",
      gst_encoding_profile_get_presence (sprof));

  if (GST_IS_ENCODING_VIDEO_PROFILE (sprof)) {
    GstEncodingVideoProfile *vp = (GstEncodingVideoProfile *) sprof;

    g_key_file_set_integer (out, sprofgroupname, "pass",
        gst_encoding_video_profile_get_pass (vp));
    g_key_file_set_boolean (out, sprofgroupname, "variableframerate",
        gst_encoding_video_profile_get_variableframerate (vp));
  }

  g_free (sprofgroupname);
  if (format)
    gst_caps_unref (format);
  if (restriction)
    gst_caps_unref (restriction);
  return TRUE;
}

static gchar *
get_locale (void)
{
  const char *loc = NULL;
  gchar *ret;

  gst_pb_utils_init_locale_text_domain ();

#ifdef ENABLE_NLS
#if defined(LC_MESSAGES)
  loc = setlocale (LC_MESSAGES, NULL);
  GST_LOG ("LC_MESSAGES: %s", GST_STR_NULL (loc));
#elif defined(LC_ALL)
  loc = setlocale (LC_ALL, NULL);
  GST_LOG ("LC_ALL: %s", GST_STR_NULL (loc));
#else
  GST_LOG ("Neither LC_ALL nor LC_MESSAGES defined");
#endif
#else /* !ENABLE_NLS */
  GST_LOG ("i18n disabled");
#endif

  if (loc == NULL || g_ascii_strncasecmp (loc, "en", 2) == 0)
    return NULL;

  /* en_GB.UTF-8 => en */
  ret = g_ascii_strdown (loc, -1);
  ret = g_strcanon (ret, "abcdefghijklmnopqrstuvwxyz", '\0');
  GST_LOG ("using locale: %s", ret);
  return ret;
}

/* Serialize the top-level profiles
 * Note: They don't have to be containerprofiles */
static gboolean
serialize_encoding_profile (GKeyFile * out, GstEncodingProfile * prof)
{
  gchar *profgroupname;
  const GList *tmp;
  guint i;
  const gchar *profname, *profdesc, *profpreset, *proftype;
  GstCaps *profformat;

  profname = gst_encoding_profile_get_name (prof);
  profdesc = gst_encoding_profile_get_description (prof);
  profformat = gst_encoding_profile_get_format (prof);
  profpreset = gst_encoding_profile_get_preset (prof);
  proftype = gst_encoding_profile_get_type_nick (prof);

  profgroupname = g_strdup_printf ("profile-%s", profname);

  g_key_file_set_string (out, profgroupname, "name", profname);

  g_key_file_set_value (out, profgroupname, "type", proftype);

  if (profdesc) {
    gchar *locale;

    locale = get_locale ();
    if (locale != NULL) {
      g_key_file_set_locale_string (out, profgroupname, "description",
          locale, profdesc);
      g_free (locale);
    } else {
      g_key_file_set_string (out, profgroupname, "description", profdesc);
    }
  }
  if (profformat) {
    gchar *tmpc = gst_caps_to_string (profformat);
    g_key_file_set_string (out, profgroupname, "format", tmpc);
    g_free (tmpc);
  }
  if (profpreset)
    g_key_file_set_string (out, profgroupname, "preset", profpreset);

  /* stream profiles */
  if (GST_IS_ENCODING_CONTAINER_PROFILE (prof)) {
    for (tmp =
        gst_encoding_container_profile_get_profiles
        (GST_ENCODING_CONTAINER_PROFILE (prof)), i = 0; tmp;
        tmp = tmp->next, i++) {
      GstEncodingProfile *sprof = (GstEncodingProfile *) tmp->data;

      if (!serialize_stream_profiles (out, sprof, profname, i))
        return FALSE;
    }
  }
  if (profformat)
    gst_caps_unref (profformat);
  g_free (profgroupname);
  return TRUE;
}

static gboolean
serialize_target (GKeyFile * out, GstEncodingTarget * target)
{
  GList *tmp;

  g_key_file_set_string (out, GST_ENCODING_TARGET_HEADER, "name", target->name);
  g_key_file_set_string (out, GST_ENCODING_TARGET_HEADER, "category",
      target->category);
  g_key_file_set_string (out, GST_ENCODING_TARGET_HEADER, "description",
      target->description);

  for (tmp = target->profiles; tmp; tmp = tmp->next) {
    GstEncodingProfile *prof = (GstEncodingProfile *) tmp->data;
    if (!serialize_encoding_profile (out, prof))
      return FALSE;
  }

  return TRUE;
}

/**
 * parse_encoding_profile:
 * @in: a #GKeyFile
 * @parentprofilename: the parent profile name (including 'profile-' or 'streamprofile-' header)
 * @profilename: the profile name group to parse
 * @nbgroups: the number of top-level groups
 * @groups: the top-level groups
 */
static GstEncodingProfile *
parse_encoding_profile (GKeyFile * in, gchar * parentprofilename,
    gchar * profilename, gsize nbgroups, gchar ** groups)
{
  GstEncodingProfile *sprof = NULL;
  gchar **parent;
  gchar *proftype, *format, *preset, *restriction, *pname, *description,
      *locale;
  GstCaps *formatcaps = NULL;
  GstCaps *restrictioncaps = NULL;
  gboolean variableframerate;
  gint pass, presence;
  gsize i, nbencprofiles;

  GST_DEBUG ("parentprofilename : %s , profilename : %s",
      parentprofilename, profilename);

  if (parentprofilename) {
    gboolean found = FALSE;

    parent =
        g_key_file_get_string_list (in, profilename, "parent",
        &nbencprofiles, NULL);
    if (!parent || !nbencprofiles) {
      return NULL;
    }

    /* Check if this streamprofile is used in <profilename> */
    for (i = 0; i < nbencprofiles; i++) {
      if (!g_strcmp0 (parent[i], parentprofilename)) {
        found = TRUE;
        break;
      }
    }
    g_strfreev (parent);

    if (!found) {
      GST_DEBUG ("Stream profile '%s' isn't used in profile '%s'",
          profilename, parentprofilename);
      return NULL;
    }
  }

  pname = g_key_file_get_value (in, profilename, "name", NULL);

  locale = get_locale ();
  /* will try to fall back to untranslated string if no translation found */
  description = g_key_file_get_locale_string (in, profilename,
      "description", locale, NULL);
  g_free (locale);

  /* Note: a missing description is normal for non-container profiles */
  if (description == NULL) {
    GST_LOG ("Missing 'description' field for streamprofile %s", profilename);
  }

  /* Parse the remaining fields */
  proftype = g_key_file_get_value (in, profilename, "type", NULL);
  if (!proftype) {
    GST_WARNING ("Missing 'type' field for streamprofile %s", profilename);
    g_free (pname);
    g_free (description);
    return NULL;
  }

  format = g_key_file_get_value (in, profilename, "format", NULL);
  if (format) {
    formatcaps = gst_caps_from_string (format);
    g_free (format);
  }

  preset = g_key_file_get_value (in, profilename, "preset", NULL);

  restriction = g_key_file_get_value (in, profilename, "restriction", NULL);
  if (restriction) {
    restrictioncaps = gst_caps_from_string (restriction);
    g_free (restriction);
  }

  presence = g_key_file_get_integer (in, profilename, "presence", NULL);
  pass = g_key_file_get_integer (in, profilename, "pass", NULL);
  variableframerate =
      g_key_file_get_boolean (in, profilename, "variableframerate", NULL);

  /* Build the streamprofile ! */
  if (!g_strcmp0 (proftype, "container")) {
    GstEncodingProfile *pprof;

    sprof =
        (GstEncodingProfile *) gst_encoding_container_profile_new (pname,
        description, formatcaps, preset);
    /* Now look for the stream profiles */
    for (i = 0; i < nbgroups; i++) {
      if (!g_ascii_strncasecmp (groups[i], "streamprofile-", 13)) {
        pprof = parse_encoding_profile (in, pname, groups[i], nbgroups, groups);
        if (pprof) {
          gst_encoding_container_profile_add_profile (
              (GstEncodingContainerProfile *) sprof, pprof);
        }
      }
    }
  } else if (!g_strcmp0 (proftype, "video")) {
    sprof =
        (GstEncodingProfile *) gst_encoding_video_profile_new (formatcaps,
        preset, restrictioncaps, presence);
    gst_encoding_video_profile_set_variableframerate ((GstEncodingVideoProfile
            *) sprof, variableframerate);
    gst_encoding_video_profile_set_pass ((GstEncodingVideoProfile *) sprof,
        pass);
    gst_encoding_profile_set_name (sprof, pname);
    gst_encoding_profile_set_description (sprof, description);
  } else if (!g_strcmp0 (proftype, "audio")) {
    sprof =
        (GstEncodingProfile *) gst_encoding_audio_profile_new (formatcaps,
        preset, restrictioncaps, presence);
    gst_encoding_profile_set_name (sprof, pname);
    gst_encoding_profile_set_description (sprof, description);
  } else
    GST_ERROR ("Unknown profile format '%s'", proftype);

  if (restrictioncaps)
    gst_caps_unref (restrictioncaps);
  if (formatcaps)
    gst_caps_unref (formatcaps);

  g_free (pname);
  g_free (description);
  g_free (preset);
  g_free (proftype);

  return sprof;
}

static GstEncodingTarget *
parse_keyfile (GKeyFile * in, gchar * targetname, gchar * categoryname,
    gchar * description)
{
  GstEncodingTarget *res = NULL;
  GstEncodingProfile *prof;
  gchar **groups;
  gsize i, nbgroups;

  res = gst_encoding_target_new (targetname, categoryname, description, NULL);

  /* Figure out the various profiles */
  groups = g_key_file_get_groups (in, &nbgroups);
  for (i = 0; i < nbgroups; i++) {
    if (!g_ascii_strncasecmp (groups[i], "profile-", 8)) {
      prof = parse_encoding_profile (in, NULL, groups[i], nbgroups, groups);
      if (prof)
        gst_encoding_target_add_profile (res, prof);
    }
  }

  g_strfreev (groups);

  g_free (targetname);
  g_free (categoryname);
  g_free (description);

  return res;
}

static GKeyFile *
load_file_and_read_header (const gchar * path, gchar ** targetname,
    gchar ** categoryname, gchar ** description, GError ** error)
{
  GKeyFile *in;
  gboolean res;
  GError *key_error = NULL;

  g_return_val_if_fail (error == NULL || *error == NULL, NULL);

  in = g_key_file_new ();

  GST_DEBUG ("path:%s", path);

  res =
      g_key_file_load_from_file (in, path,
      G_KEY_FILE_KEEP_COMMENTS | G_KEY_FILE_KEEP_TRANSLATIONS, &key_error);
  if (!res || key_error != NULL)
    goto load_error;

  key_error = NULL;
  *targetname =
      g_key_file_get_value (in, GST_ENCODING_TARGET_HEADER, "name", &key_error);
  if (!*targetname)
    goto empty_name;

  *categoryname =
      g_key_file_get_value (in, GST_ENCODING_TARGET_HEADER, "category", NULL);
  *description =
      g_key_file_get_value (in, GST_ENCODING_TARGET_HEADER, "description",
      NULL);

  return in;

load_error:
  {
    GST_WARNING ("Unable to read GstEncodingTarget file %s: %s",
        path, key_error->message);
    g_propagate_error (error, key_error);
    g_key_file_free (in);
    return NULL;
  }

empty_name:
  {
    GST_WARNING ("Wrong header in file %s: %s", path, key_error->message);
    g_propagate_error (error, key_error);
    g_key_file_free (in);
    return NULL;
  }
}

/**
 * gst_encoding_target_load_from_file:
 * @filepath: (type filename): The file location to load the #GstEncodingTarget from
 * @error: If an error occurred, this field will be filled in.
 *
 * Opens the provided file and returns the contained #GstEncodingTarget.
 *
 * Returns: (transfer full): The #GstEncodingTarget contained in the file, else
 * %NULL
 */

GstEncodingTarget *
gst_encoding_target_load_from_file (const gchar * filepath, GError ** error)
{
  GKeyFile *in;
  gchar *targetname, *categoryname, *description;
  GstEncodingTarget *res = NULL;

  in = load_file_and_read_header (filepath, &targetname, &categoryname,
      &description, error);
  if (!in)
    goto beach;

  res = parse_keyfile (in, targetname, categoryname, description);

  g_key_file_free (in);

beach:
  return res;
}

/*
 * returned list contents must be freed
 */
static GList *
get_matching_filenames (gchar * path, gchar * filename)
{
  GList *res = NULL;
  GDir *topdir;
  const gchar *subdirname;
  gchar *tmp;

  topdir = g_dir_open (path, 0, NULL);
  if (G_UNLIKELY (topdir == NULL))
    return NULL;

  tmp = g_build_filename (path, filename, NULL);
  if (g_file_test (tmp, G_FILE_TEST_EXISTS))
    res = g_list_append (res, tmp);
  else
    g_free (tmp);

  while ((subdirname = g_dir_read_name (topdir))) {
    gchar *ltmp = g_build_filename (path, subdirname, NULL);

    if (g_file_test (ltmp, G_FILE_TEST_IS_DIR)) {
      gchar *tmp = g_build_filename (path, subdirname, filename, NULL);
      /* Test to see if we have a file named like that in that directory */
      if (g_file_test (tmp, G_FILE_TEST_EXISTS))
        res = g_list_append (res, tmp);
      else
        g_free (tmp);
    }
    g_free (ltmp);
  }

  g_dir_close (topdir);

  return res;
}

static GstEncodingTarget *
gst_encoding_target_subload (gchar * path, const gchar * category,
    gchar * lfilename, GError ** error)
{
  GstEncodingTarget *target = NULL;

  if (category) {
    gchar *filename;

    filename = g_build_filename (path, category, lfilename, NULL);
    target = gst_encoding_target_load_from_file (filename, error);
    g_free (filename);
  } else {
    GList *tmp, *tries = get_matching_filenames (path, lfilename);

    /* Try to find a file named %s.gstprofile in any subdirectories */
    for (tmp = tries; tmp; tmp = tmp->next) {
      target = gst_encoding_target_load_from_file ((gchar *) tmp->data, NULL);
      if (target)
        break;
    }
    g_list_foreach (tries, (GFunc) g_free, NULL);
    if (tries)
      g_list_free (tries);
  }

  return target;
}

/**
 * gst_encoding_target_load:
 * @name: the name of the #GstEncodingTarget to load (automatically
 * converted to lower case internally as capital letters are not
 * valid for target names).
 * @category: (nullable): the name of the target category, like
 * #GST_ENCODING_CATEGORY_DEVICE. Can be %NULL
 * @error: If an error occurred, this field will be filled in.
 *
 * Searches for the #GstEncodingTarget with the given name, loads it
 * and returns it.
 *
 * If the category name is specified only targets from that category will be
 * searched for.
 *
 * Returns: (transfer full): The #GstEncodingTarget if available, else %NULL.
 */
GstEncodingTarget *
gst_encoding_target_load (const gchar * name, const gchar * category,
    GError ** error)
{
  gint i;
  gchar *p, *lname, *lfilename = NULL, *tldir, **encoding_target_dirs;
  const gchar *envvar;
  GstEncodingTarget *target = NULL;

  g_return_val_if_fail (name != NULL, NULL);

  p = lname = g_str_to_ascii (name, NULL);
  for (; *p; ++p)
    *p = g_ascii_tolower (*p);

  if (!validate_name (lname))
    goto invalid_name;

  if (category && !validate_name (category))
    goto invalid_category;

  lfilename = g_strdup_printf ("%s" GST_ENCODING_TARGET_SUFFIX, lname);

  envvar = g_getenv ("GST_ENCODING_TARGET_PATH");
  if (envvar) {
    encoding_target_dirs = g_strsplit (envvar, G_SEARCHPATH_SEPARATOR_S, -1);
    for (i = 0; encoding_target_dirs[i]; i++) {
      target = gst_encoding_target_subload (encoding_target_dirs[i],
          category, lfilename, error);

      if (target)
        break;
    }
    g_strfreev (encoding_target_dirs);
    if (target)
      goto done;
  }

  /* Try from local profiles */

  tldir =
      g_build_filename (g_get_user_data_dir (), "gstreamer-" GST_API_VERSION,
      GST_ENCODING_TARGET_DIRECTORY, NULL);
  target = gst_encoding_target_subload (tldir, category, lfilename, error);
  g_free (tldir);

  if (target == NULL) {
    /* Try from system-wide profiles */
    tldir =
        g_build_filename (GST_DATADIR, "gstreamer-" GST_API_VERSION,
        GST_ENCODING_TARGET_DIRECTORY, NULL);
    target = gst_encoding_target_subload (tldir, category, lfilename, error);
    g_free (tldir);
  }

  if (!target) {
    GList *tmp, *targets = gst_encoding_list_all_targets (NULL);

    for (tmp = targets; tmp; tmp = tmp->next) {
      gint i;
      GstEncodingTarget *tmptarget = tmp->data;
      gchar **names = g_strsplit (tmptarget->name, ";", -1);

      for (i = 0; names[i]; i++) {
        if (!g_strcmp0 (names[i], lname) && (!category ||
                !g_strcmp0 (tmptarget->category, category))) {
          target = gst_object_ref (tmptarget);

          break;
        }
      }
      g_strfreev (names);

      if (target)
        break;
    }

    g_list_free_full (targets, gst_object_unref);
  }


done:
  g_free (lfilename);
  g_free (lname);

  return target;

invalid_name:
  {
    GST_INFO ("Invalid name for encoding target : '%s'", name);
    goto done;
  }
invalid_category:
  {
    GST_INFO ("Invalid name for encoding category : '%s'", category);
    goto done;
  }
}

/**
 * gst_encoding_target_save_to_file:
 * @target: a #GstEncodingTarget
 * @filepath: (type filename): the location to store the @target at.
 * @error: If an error occurred, this field will be filled in.
 *
 * Saves the @target to the provided file location.
 *
 * Returns: %TRUE if the target was correctly saved, else %FALSE.
 **/

gboolean
gst_encoding_target_save_to_file (GstEncodingTarget * target,
    const gchar * filepath, GError ** error)
{
  GKeyFile *out;
  gchar *data;
  gsize data_size;

  g_return_val_if_fail (GST_IS_ENCODING_TARGET (target), FALSE);
  g_return_val_if_fail (filepath != NULL, FALSE);

  /* FIXME : Check filepath is valid and writable
   * FIXME : Strip out profiles already present in system target */

  /* Get unique name... */

  /* Create output GKeyFile */
  out = g_key_file_new ();

  if (!serialize_target (out, target))
    goto serialize_failure;

  if (!(data = g_key_file_to_data (out, &data_size, error)))
    goto convert_failed;

  if (!g_file_set_contents (filepath, data, data_size, error))
    goto write_failed;

  target->path = g_strdup (filepath);

  g_key_file_free (out);
  g_free (data);

  return TRUE;

serialize_failure:
  {
    GST_ERROR ("Failure serializing target");
    g_key_file_free (out);
    return FALSE;
  }

convert_failed:
  {
    GST_ERROR ("Failure converting keyfile: %s", (*error)->message);
    g_key_file_free (out);
    g_free (data);
    return FALSE;
  }

write_failed:
  {
    GST_ERROR ("Unable to write file %s: %s", filepath, (*error)->message);
    g_key_file_free (out);
    g_free (data);
    return FALSE;
  }
}

/**
 * gst_encoding_target_save:
 * @target: a #GstEncodingTarget
 * @error: If an error occurred, this field will be filled in.
 *
 * Saves the @target to a default user-local directory.
 *
 * Returns: %TRUE if the target was correctly saved, else %FALSE.
 **/

gboolean
gst_encoding_target_save (GstEncodingTarget * target, GError ** error)
{
  gchar *filename;
  gchar *lfilename;
  gchar *dirname;

  g_return_val_if_fail (GST_IS_ENCODING_TARGET (target), FALSE);
  g_return_val_if_fail (target->category != NULL, FALSE);

  lfilename = g_strdup_printf ("%s" GST_ENCODING_TARGET_SUFFIX, target->name);
  dirname =
      g_build_filename (g_get_user_data_dir (), "gstreamer-" GST_API_VERSION,
      GST_ENCODING_TARGET_DIRECTORY, target->category, NULL);
  errno = 0;
  if (g_mkdir_with_parents (dirname, 0755)) {
    GST_ERROR_OBJECT (target, "Could not create directory to save %s into: %s",
        target->name, g_strerror (errno));

    return FALSE;
  }
  filename = g_build_filename (dirname, lfilename, NULL);
  g_free (dirname);
  g_free (lfilename);

  gst_encoding_target_save_to_file (target, filename, error);
  g_free (filename);

  return TRUE;
}

static GList *
get_categories (gchar * path)
{
  GList *res = NULL;
  GDir *topdir;
  const gchar *subdirname;

  topdir = g_dir_open (path, 0, NULL);
  if (G_UNLIKELY (topdir == NULL))
    return NULL;

  while ((subdirname = g_dir_read_name (topdir))) {
    gchar *ltmp = g_build_filename (path, subdirname, NULL);

    if (g_file_test (ltmp, G_FILE_TEST_IS_DIR)) {
      res = g_list_append (res, (gpointer) g_strdup (subdirname));
    }
    g_free (ltmp);
  }

  g_dir_close (topdir);

  return res;
}

/**
 * gst_encoding_list_available_categories:
 *
 * Lists all #GstEncodingTarget categories present on disk.
 *
 * Returns: (transfer full) (element-type gchar*): A list
 * of #GstEncodingTarget categories.
 */
GList *
gst_encoding_list_available_categories (void)
{
  GList *res = NULL;
  GList *tmp1, *tmp2;
  gchar *topdir;

  /* First try user-local categories */
  topdir =
      g_build_filename (g_get_user_data_dir (), "gstreamer-" GST_API_VERSION,
      GST_ENCODING_TARGET_DIRECTORY, NULL);
  res = get_categories (topdir);
  g_free (topdir);

  /* Extend with system-wide categories */
  topdir = g_build_filename (GST_DATADIR, "gstreamer-" GST_API_VERSION,
      GST_ENCODING_TARGET_DIRECTORY, NULL);
  tmp1 = get_categories (topdir);
  g_free (topdir);

  for (tmp2 = tmp1; tmp2; tmp2 = tmp2->next) {
    gchar *name = (gchar *) tmp2->data;
    if (!g_list_find_custom (res, name, (GCompareFunc) g_strcmp0))
      res = g_list_append (res, (gpointer) name);
    else
      g_free (name);
  }
  g_list_free (tmp1);

  return res;
}

static inline GList *
sub_get_all_targets (gchar * subdir)
{
  GList *res = NULL;
  const gchar *filename;
  GDir *dir;
  GstEncodingTarget *target;

  dir = g_dir_open (subdir, 0, NULL);
  if (G_UNLIKELY (dir == NULL))
    return NULL;

  while ((filename = g_dir_read_name (dir))) {
    gchar *fullname;

    /* Only try files ending with .gstprofile */
    if (!g_str_has_suffix (filename, GST_ENCODING_TARGET_SUFFIX))
      continue;

    fullname = g_build_filename (subdir, filename, NULL);
    target = gst_encoding_target_load_from_file (fullname, NULL);
    if (target) {
      target->path = fullname;
      res = g_list_append (res, target);
    } else
      GST_WARNING ("Failed to get a target from %s", fullname);
  }
  g_dir_close (dir);

  return res;
}

static inline GList *
get_all_targets (gchar * topdir, const gchar * categoryname)
{
  GList *res = NULL;

  if (categoryname) {
    gchar *subdir = g_build_filename (topdir, categoryname, NULL);
    /* Try to open the directory */
    res = sub_get_all_targets (subdir);
    g_free (subdir);
  } else {
    const gchar *subdirname;
    GDir *dir = g_dir_open (topdir, 0, NULL);

    if (G_UNLIKELY (dir == NULL))
      return NULL;

    while ((subdirname = g_dir_read_name (dir))) {
      gchar *ltmp = g_build_filename (topdir, subdirname, NULL);

      if (g_file_test (ltmp, G_FILE_TEST_IS_DIR)) {
        res = g_list_concat (res, sub_get_all_targets (ltmp));
      }
      g_free (ltmp);
    }
    g_dir_close (dir);
  }

  return res;
}

static guint
compare_targets (const GstEncodingTarget * ta, const GstEncodingTarget * tb)
{
  if (g_strcmp0 (ta->name, tb->name)
      || g_strcmp0 (ta->category, tb->category))
    return -1;

  return 0;
}

static GList *
merge_targets (GList * res, GList * extra)
{
  GList *tmp;

  /* FIXME : We should merge the system-wide profiles into the user-locals
   * instead of stopping at identical target names */
  for (tmp = extra; tmp; tmp = tmp->next) {
    GstEncodingTarget *target = (GstEncodingTarget *) tmp->data;
    if (g_list_find_custom (res, target, (GCompareFunc) compare_targets))
      gst_encoding_target_unref (target);
    else
      res = g_list_append (res, target);
  }

  g_list_free (extra);

  return res;
}

/**
 * gst_encoding_list_all_targets:
 * @categoryname: (nullable): The category, for ex: #GST_ENCODING_CATEGORY_DEVICE.
 * Can be %NULL.
 *
 * List all available #GstEncodingTarget for the specified category, or all categories
 * if @categoryname is %NULL.
 *
 * Returns: (transfer full) (element-type GstEncodingTarget): The list of #GstEncodingTarget
 */
GList *
gst_encoding_list_all_targets (const gchar * categoryname)
{
  GList *res = NULL;
  gchar *topdir;
  gchar **encoding_target_dirs;

  const gchar *envvar = g_getenv ("GST_ENCODING_TARGET_PATH");
  if (envvar) {
    gint i;

    encoding_target_dirs = g_strsplit (envvar, G_SEARCHPATH_SEPARATOR_S, -1);
    for (i = 0; encoding_target_dirs[i]; i++)
      res =
          merge_targets (res, get_all_targets (encoding_target_dirs[i],
              categoryname));

    g_strfreev (encoding_target_dirs);
  }

  /* Get user-locals */
  topdir =
      g_build_filename (g_get_user_data_dir (), "gstreamer-" GST_API_VERSION,
      GST_ENCODING_TARGET_DIRECTORY, NULL);
  res = merge_targets (res, get_all_targets (topdir, categoryname));
  g_free (topdir);

  /* Get system-wide */
  topdir = g_build_filename (GST_DATADIR, "gstreamer-" GST_API_VERSION,
      GST_ENCODING_TARGET_DIRECTORY, NULL);
  res = merge_targets (res, get_all_targets (topdir, categoryname));
  g_free (topdir);

  return res;
}
