/*
 * Farstream - Farstream RTP Discovered Codecs cache
 *
 * Copyright 2007 Collabora Ltd.
 *  @author: Olivier Crete <olivier.crete@collabora.co.uk>
 * Copyright 2007 Nokia Corp.
 *
 * fs-rtp-codec-cache.c - A Farstream RTP Codec Caching gobject
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA
 */


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

#include "fs-rtp-codec-cache.h"

#include <string.h>
#ifdef HAVE_UNISTD_H
# include <unistd.h>
#endif
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>

#include <farstream/fs-conference.h>

#include "fs-rtp-conference.h"


/* Because of annoying CRTs */
#if defined (_MSC_VER) && _MSC_VER >= 1400
# include <io.h>
# define open _open
# define close _close
# define read _read
# define write _write
# define stat _stat
# define STAT_TYPE struct _stat
#else
# define STAT_TYPE struct stat
#endif

#define GST_CAT_DEFAULT fsrtpconference_disco

static gboolean codecs_cache_valid (gchar *cache_path) {
  time_t cache_ts = 0;
  time_t registry_ts = 0;
  STAT_TYPE cache_stat;
  STAT_TYPE registry_stat;
  gchar *registry_xml_path;
  gchar *registry_bin_path;

  registry_xml_path = g_strdup (g_getenv ("GST_REGISTRY"));
  if (registry_xml_path == NULL) {
    registry_bin_path = g_build_filename (g_get_home_dir (),
        ".gstreamer-" GST_API_VERSION, "registry." HOST_CPU ".bin", NULL);
    registry_xml_path = g_build_filename (g_get_home_dir (),
        ".gstreamer-" GST_API_VERSION, "registry." HOST_CPU ".xml", NULL);
  } else {
    registry_bin_path = g_strdup (registry_xml_path);
  }

  if (stat (registry_xml_path, &registry_stat) == 0) {
    registry_ts = registry_stat.st_mtime;
  }

  if (stat (registry_bin_path, &registry_stat) == 0) {
    if (registry_ts < registry_stat.st_mtime) {
      registry_ts = registry_stat.st_mtime;
    }
  }

  if (stat (cache_path, &cache_stat) == 0) {
    cache_ts = cache_stat.st_mtime;
  }

  g_free (registry_bin_path);
  g_free (registry_xml_path);

  return (registry_ts != 0 && cache_ts > registry_ts);
}

static gchar *
get_codecs_cache_path (FsMediaType media_type) {
  gchar *cache_path;

  if (media_type == FS_MEDIA_TYPE_AUDIO) {
    cache_path = g_strdup (g_getenv ("FS_AUDIO_CODECS_CACHE"));
    if (cache_path == NULL) {
      cache_path = g_build_filename (g_get_user_cache_dir (), "farstream",
          "codecs.audio." HOST_CPU ".cache", NULL);
    }
  } else if (media_type == FS_MEDIA_TYPE_VIDEO) {
    cache_path = g_strdup (g_getenv ("FS_VIDEO_CODECS_CACHE"));
    if (cache_path == NULL) {
      cache_path = g_build_filename (g_get_user_cache_dir (), "farstream",
          "codecs.video." HOST_CPU ".cache", NULL);
    }
  } else if (media_type == FS_MEDIA_TYPE_APPLICATION) {
    cache_path = g_strdup (g_getenv ("FS_APPLICATION_CODECS_CACHE"));
    if (cache_path == NULL) {
      cache_path = g_build_filename (g_get_user_cache_dir (), "farstream",
          "codecs.application." HOST_CPU ".cache", NULL);
    }
  } else {
    GST_ERROR ("Unknown media type %d for cache loading", media_type);
    return NULL;
  }

  return cache_path;
}


static gboolean
read_codec_blueprint_uint (gchar **in, gsize *size, guint *val) {
  if (*size < sizeof (guint))
    return FALSE;

  memcpy (val, *in, sizeof(guint));
  *in += sizeof (guint);
  *size -= sizeof (guint);
  return TRUE;
}

static gboolean
read_codec_blueprint_int (gchar **in, gsize *size, gint *val) {
  if (*size < sizeof (gint))
    return FALSE;

  memcpy (val, *in, sizeof(gint));
  *in += sizeof (gint);
  *size -= sizeof (gint);
  return TRUE;
}

static gboolean
read_codec_blueprint_string (gchar **in, gsize *size, gchar **str) {
  gint str_length;

  if (!read_codec_blueprint_int (in, size, &str_length))
    return FALSE;

  if (*size < str_length)
    return FALSE;

  *str = g_new0 (gchar, str_length +1);
  memcpy (*str, *in, str_length);
  *in += str_length;
  *size -= str_length;

  return TRUE;
}


#define READ_CHECK(x) if (!x) goto error;

static CodecBlueprint *
load_codec_blueprint (FsMediaType media_type, gchar **in, gsize *size) {
  CodecBlueprint *codec_blueprint = g_slice_new0 (CodecBlueprint);
  gchar *tmp;
  gint tmp_size;
  int i;
  gint id;
  gchar *encoding_name = NULL;
  guint clock_rate;

  READ_CHECK (read_codec_blueprint_int
      (in, size, &(id)));
  READ_CHECK (read_codec_blueprint_string
      (in, size, &(encoding_name)));
  READ_CHECK (read_codec_blueprint_uint
      (in, size, &(clock_rate)));
  codec_blueprint->codec = fs_codec_new (id, encoding_name, media_type,
      clock_rate);
  g_free (encoding_name);
  READ_CHECK (read_codec_blueprint_uint
      (in, size, &(codec_blueprint->codec->channels)));

  READ_CHECK (read_codec_blueprint_int (in, size, &tmp_size));
  for (i = 0; i < tmp_size; i++) {
    gchar *name, *value;
    READ_CHECK (read_codec_blueprint_string (in, size, &(name)));
    READ_CHECK (read_codec_blueprint_string (in, size, &(value)));
    fs_codec_add_optional_parameter (codec_blueprint->codec, name, value);
    g_free (name);
    g_free (value);
  }

  READ_CHECK (read_codec_blueprint_string (in, size, &tmp));
  codec_blueprint->media_caps = gst_caps_from_string (tmp);
  g_free (tmp);

  READ_CHECK (read_codec_blueprint_string (in, size, &tmp));
  codec_blueprint->rtp_caps = gst_caps_from_string (tmp);
  g_free (tmp);

  READ_CHECK (read_codec_blueprint_string (in, size, &tmp));
  codec_blueprint->input_caps = gst_caps_from_string (tmp);
  g_free (tmp);

  READ_CHECK (read_codec_blueprint_string (in, size, &tmp));
  codec_blueprint->output_caps = gst_caps_from_string (tmp);
  g_free (tmp);

  READ_CHECK (read_codec_blueprint_int (in, size, &tmp_size));
  for (i = 0; i < tmp_size; i++) {
    int j, tmp_size2;
    GList *tmplist = NULL;

    READ_CHECK (read_codec_blueprint_int (in, size, &tmp_size2));
    for (j = 0; j < tmp_size2; j++) {
      GstElementFactory *fact = NULL;
      READ_CHECK (read_codec_blueprint_string (in, size, &(tmp)));
      fact = gst_element_factory_find (tmp);
      g_free (tmp);
      if (!fact)
        goto error;
      tmplist = g_list_append (tmplist, fact);
    }
    codec_blueprint->send_pipeline_factory =
      g_list_append (codec_blueprint->send_pipeline_factory, tmplist);
  }

  READ_CHECK (read_codec_blueprint_int (in, size, &tmp_size));
  for (i = 0; i < tmp_size; i++) {
    int j, tmp_size2;
    GList *tmplist = NULL;

    READ_CHECK (read_codec_blueprint_int (in, size, &tmp_size2));
    for (j = 0; j < tmp_size2; j++) {
      GstElementFactory *fact = NULL;
      READ_CHECK (read_codec_blueprint_string (in, size, &(tmp)));
      fact = gst_element_factory_find (tmp);
      g_free (tmp);
      if (!fact)
        goto error;
      tmplist = g_list_append (tmplist, fact);
    }
    codec_blueprint->receive_pipeline_factory =
      g_list_append (codec_blueprint->receive_pipeline_factory, tmplist);
  }

  GST_DEBUG ("adding codec %s with pt %d, send_pipeline %p, receive_pipeline %p",
      codec_blueprint->codec->encoding_name, codec_blueprint->codec->id,
      codec_blueprint->send_pipeline_factory,
      codec_blueprint->receive_pipeline_factory);

  return codec_blueprint;

 error:
  codec_blueprint_destroy (codec_blueprint);

  return NULL;
}


/**
 * load_codecs_cache
 * @media_type: a #FsMediaType
 *
 * Will load the codecs blueprints from the cache.
 *
 * Returns: TRUE if successful, FALSE if error, or cache outdated
 *
 */
GList *
load_codecs_cache (FsMediaType media_type)
{
  GMappedFile *mapped = NULL;
  gchar *contents = NULL;
  gchar *in = NULL;
  gsize size;
  GError *err = NULL;
  GList *blueprints = NULL;

  gchar magic[8] = {0};
  gchar magic_media = '?';
  gint num_blueprints;
  gchar *cache_path;
  int i;


  if (media_type == FS_MEDIA_TYPE_AUDIO) {
    magic_media = 'A';
  } else if (media_type == FS_MEDIA_TYPE_VIDEO) {
    magic_media = 'V';
  } else if (media_type == FS_MEDIA_TYPE_APPLICATION) {
    magic_media = 'P';
  } else {
    GST_ERROR ("Invalid media type %d", media_type);
    return NULL;
  }

  cache_path = get_codecs_cache_path (media_type);

  if (!cache_path)
    return NULL;

  if (!codecs_cache_valid (cache_path)) {
    GST_DEBUG ("Codecs cache %s is outdated or does not exist", cache_path);
    g_free (cache_path);
    return NULL;
  }

  GST_DEBUG ("Loading codecs cache %s", cache_path);

  mapped = g_mapped_file_new (cache_path, FALSE, &err);
  if (mapped == NULL) {
    GST_DEBUG ("Unable to mmap file %s : %s", cache_path,
      err ? err->message: "unknown error");
    g_clear_error (&err);

    if (!g_file_get_contents (cache_path, &contents, &size, NULL))
      goto error;
  } else {
    if ((contents = g_mapped_file_get_contents (mapped)) == NULL) {
      GST_WARNING ("Can't load file %s : %s", cache_path, g_strerror (errno));
      goto error;
    }
    /* check length for header */
    size = g_mapped_file_get_length (mapped);
  }

  /* in is a cursor pointer on the file contents */

  in = contents;

  if (size < sizeof (magic)) {
    GST_WARNING ("Cache file corrupt");
    goto error;
  }

  memcpy (magic, in, sizeof (magic));
  in += sizeof (magic);
  size -= sizeof (magic);

  if (magic[0] != 'F' ||
      magic[1] != 'S' ||
      magic[2] != magic_media ||
      magic[3] != 'C' ||
      magic[4] != '1' ||   /* This is the version number */
      magic[5] != '2') {
    GST_WARNING ("Cache file has incorrect magic header. File corrupted");
    goto error;
  }

  if (size < sizeof (gint)) {
    GST_WARNING ("Cache file corrupt (size: %"G_GSIZE_FORMAT" < sizeof (int))",
        size);
    goto error;
  }

  memcpy (&num_blueprints, in, sizeof(gint));
  in += sizeof (gint);
  size -= sizeof (gint);

  if (num_blueprints > 50)
  {
    GST_WARNING ("Impossible number of blueprints in cache %d, ignoring",
        num_blueprints);
    goto error;
  }

  for (i = 0; i < num_blueprints; i++) {
    CodecBlueprint *blueprint = load_codec_blueprint (media_type, &in, &size);
    if (!blueprint) {
      GST_WARNING ("Can not load all of the blueprints, cache corrupted");

      if (blueprints) {
        g_list_foreach (blueprints, (GFunc) codec_blueprint_destroy, NULL);
        g_list_free (blueprints);
        blueprints = NULL;
      }

      goto error;
    }
    blueprints = g_list_append (blueprints, blueprint);
  }

 error:
  if (mapped) {
#if GLIB_CHECK_VERSION(2,22,0)
    g_mapped_file_unref (mapped);
#else
    g_mapped_file_free (mapped);
#endif
  } else {
    g_free (contents);
  }
  g_free (cache_path);
  return blueprints;
}

#define WRITE_CHECK(x) if (!x) return FALSE;

static gboolean
write_codec_blueprint_int (int fd, gint val) {
  return write (fd, &val, sizeof (gint)) == sizeof (gint);
}

static gboolean
write_codec_blueprint_string (int fd, const gchar *str) {
  gint size;

  size = strlen (str);
  WRITE_CHECK (write_codec_blueprint_int (fd, size));
  return write (fd, str, size) == size;
}

static gboolean
save_codec_blueprint (int fd, CodecBlueprint *codec_blueprint) {
  gchar *caps;
  const gchar *factory_name;
  GList *walk;
  gint size;

  WRITE_CHECK (write_codec_blueprint_int
      (fd, codec_blueprint->codec->id));
  WRITE_CHECK (write_codec_blueprint_string
      (fd, codec_blueprint->codec->encoding_name));
  WRITE_CHECK (write_codec_blueprint_int
      (fd, codec_blueprint->codec->clock_rate));
  WRITE_CHECK (write_codec_blueprint_int
      (fd, codec_blueprint->codec->channels));

  size = g_list_length (codec_blueprint->codec->optional_params);
  WRITE_CHECK (write_codec_blueprint_int (fd, size));
  for (walk = codec_blueprint->codec->optional_params; walk;
       walk = g_list_next (walk)) {
    FsCodecParameter *param = walk->data;
    WRITE_CHECK (write_codec_blueprint_string (fd, param->name));
    WRITE_CHECK (write_codec_blueprint_string (fd, param->value));
  }

  caps = gst_caps_to_string (codec_blueprint->media_caps);
  WRITE_CHECK (write_codec_blueprint_string (fd, caps));
  g_free (caps);

  caps = gst_caps_to_string (codec_blueprint->rtp_caps);
  WRITE_CHECK (write_codec_blueprint_string (fd, caps));
  g_free (caps);

  caps = gst_caps_to_string (codec_blueprint->input_caps);
  WRITE_CHECK (write_codec_blueprint_string (fd, caps));
  g_free (caps);

  caps = gst_caps_to_string (codec_blueprint->output_caps);
  WRITE_CHECK (write_codec_blueprint_string (fd, caps));
  g_free (caps);

  walk = codec_blueprint->send_pipeline_factory;
  size = g_list_length (walk);
  if (write (fd, &size, sizeof (gint)) != sizeof (gint))
    return FALSE;

  for (; walk; walk = g_list_next (walk)) {
    GList *walk2 = walk->data;
    size = g_list_length (walk2);
    if (write (fd, &size, sizeof (gint)) != sizeof (gint))
      return FALSE;
    for (; walk2; walk2 = g_list_next (walk2)) {
      GstElementFactory *fact = walk2->data;
      factory_name = gst_plugin_feature_get_name (GST_PLUGIN_FEATURE (fact));
      WRITE_CHECK (write_codec_blueprint_string (fd, factory_name));
    }
  }

  walk = codec_blueprint->receive_pipeline_factory;
  size = g_list_length (walk);
  if (write (fd, &size, sizeof (gint)) != sizeof (gint))
    return FALSE;

  for (; walk; walk = g_list_next (walk)) {
    GList *walk2 = walk->data;
    size = g_list_length (walk2);
    if (write (fd, &size, sizeof (gint)) != sizeof (gint))
      return FALSE;
    for (; walk2; walk2 = g_list_next (walk2)) {
      GstElementFactory *fact = walk2->data;
      factory_name = gst_plugin_feature_get_name (GST_PLUGIN_FEATURE (fact));
      WRITE_CHECK (write_codec_blueprint_string (fd, factory_name));
    }
  }

  return TRUE;
}


gboolean
save_codecs_cache (FsMediaType media_type, GList *blueprints)
{
  gchar *cache_path;
  GList *item;
  gchar *tmp_path;
  int fd;
  int size;
  gchar magic[8] = {0};

  cache_path = get_codecs_cache_path (media_type);
  if (!cache_path)
    return FALSE;


  GST_DEBUG ("Saving codecs cache to %s", cache_path);

  tmp_path = g_strconcat (cache_path, ".tmpXXXXXX", NULL);
  fd = g_mkstemp (tmp_path);
  if (fd == -1) {
    gchar *dir;

    /* oops, I bet the directory doesn't exist */
    dir = g_path_get_dirname (cache_path);
    g_mkdir_with_parents (dir, 0777);
    g_free (dir);

    /* the previous g_mkstemp call overwrote the XXXXXX placeholder ... */
    g_free (tmp_path);
    tmp_path = g_strconcat (cache_path, ".tmpXXXXXX", NULL);
    fd = g_mkstemp (tmp_path);

    if (fd == -1) {
      GST_DEBUG ("Unable to save codecs cache. g_mkstemp () failed: %s",
          g_strerror (errno));
      g_free (tmp_path);
      g_free (cache_path);
      return FALSE;
    }
  }

  magic[0] = 'F';
  magic[1] = 'S';
  magic[2] = '?';
  magic[3] = 'C';

  if (media_type == FS_MEDIA_TYPE_AUDIO) {
    magic[2] = 'A';
  } else if (media_type == FS_MEDIA_TYPE_VIDEO) {
    magic[2] = 'V';
  } else if (media_type == FS_MEDIA_TYPE_APPLICATION) {
    magic[2] = 'P';
  }

  /* version of the binary format */
  magic[4] = '1';
  magic[5] = '2';

  if (write (fd, magic, 8) != 8)
    return FALSE;


  size = g_list_length (blueprints);
  if (write (fd, &size, sizeof (gint)) != sizeof (gint))
    return FALSE;


  for (item = g_list_first (blueprints);
       item;
       item = g_list_next (item)) {
    CodecBlueprint *codec_blueprint = item->data;
    if (!save_codec_blueprint (fd, codec_blueprint)) {
      GST_WARNING ("Unable to save codec cache");
      close (fd);
      g_free (tmp_path);
      g_free (cache_path);
      return FALSE;
    }
  }


  if (close (fd) < 0) {
    GST_DEBUG ("Can't close codecs cache file : %s", g_strerror (errno));
      g_free (tmp_path);
      g_free (cache_path);
      return FALSE;
  }

  if (g_file_test (tmp_path, G_FILE_TEST_EXISTS)) {
#ifdef WIN32
    remove (cache_path);
#endif
    rename (tmp_path, cache_path);
  }

  g_free (tmp_path);
  g_free (cache_path);
  GST_DEBUG ("Wrote binary codecs cache");
  return TRUE;
}
