/* OGMRip - A library for DVD ripping and encoding
 * Copyright (C) 2004-2012 Olivier Rolland <billl@users.sourceforge.net>
 *
 * 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */

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

#include "ogmrip-container.h"
#include "ogmrip-fs.h"
#include "ogmrip-mplayer.h"
#include "ogmrip-plugin.h"
#include "ogmrip-version.h"

#include "ogmjob-exec.h"
#include "ogmjob-queue.h"

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <glib/gi18n-lib.h>
#include <glib/gstdio.h>

#define PROGRAM "MP4Box"

#define OGMRIP_TYPE_MP4           (ogmrip_mp4_get_type ())
#define OGMRIP_MP4(obj)           (G_TYPE_CHECK_INSTANCE_CAST ((obj), OGMRIP_TYPE_MP4, OGMRipMp4))
#define OGMRIP_MP4_CLASS(klass)   (G_TYPE_CHECK_CLASS_CAST ((klass), OGMRIP_TYPE_MP4, OGMRipMp4Class))
#define OGMRIP_IS_MP4(obj)        (G_TYPE_CHECK_INSTANCE_TYPE ((obj), OGMRIP_TYPE_MP4))
#define OGMRIP_IS_MP4_CLASS(obj)  (G_TYPE_CHECK_CLASS_TYPE ((klass), OGMRIP_TYPE_MP4))
#define OGMRIP_MP4_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), OGMRIP_TYPE_MP4, OGMRipMp4Class))

typedef struct _OGMRipMp4      OGMRipMp4;
typedef struct _OGMRipMp4Class OGMRipMp4Class;

struct _OGMRipMp4
{
  OGMRipContainer parent_instance;

  guint nstreams;
  guint streams;
  guint old_percent;

  guint nsplits;
  guint splits;
  guint split_percent;
};

struct _OGMRipMp4Class
{
  OGMRipContainerClass parent_class;
};

GType ogmrip_mp4_get_type (void);
static gint ogmrip_mp4_run (OGMJobSpawn *spawn);

static void
ogmrip_mp4_append_audio_file (OGMRipContainer *mp4, const gchar *filename,
    gint format, gint language, GPtrArray *argv)
{
  struct stat buf;

  if (g_stat (filename, &buf) == 0 && buf.st_size > 0)
  {
    const gchar *fmt;

    switch (format)
    {
      case OGMRIP_FORMAT_AAC:
        fmt = "aac";
        break;
      case OGMRIP_FORMAT_MP3:
        fmt = "mp3";
        break;
      case OGMRIP_FORMAT_VORBIS:
        fmt = "ogg";
        break;
      case OGMRIP_FORMAT_AC3:
      case OGMRIP_FORMAT_COPY:
        fmt = "ac3";
        break;
      default:
        fmt = NULL;
        break;
    }

    if (fmt)
    {
      const gchar *iso639_2 = NULL;

      g_ptr_array_add (argv, g_strdup ("-add"));
      if (language > -1)
        iso639_2 = ogmdvd_get_language_iso639_2 (language);
      if (iso639_2)
        g_ptr_array_add (argv, g_strdup_printf ("%s:fmt=%s:lang=%s:group=1:#audio", filename, fmt, iso639_2));
      else
        g_ptr_array_add (argv, g_strdup_printf ("%s:fmt=%s:group=1:#audio", filename, fmt));
    }
  }
}

static void
ogmrip_mp4_append_subp_file (OGMRipContainer *mp4, const gchar *filename,
    gint format, gint language, GPtrArray *argv)
{
  struct stat buf;

  if (g_stat (filename, &buf) == 0 && buf.st_size > 0)
  {
    const gchar *fmt;

    switch (format)
    {
      case OGMRIP_FORMAT_SRT:
        fmt = "srt";
        break;
      case OGMRIP_FORMAT_VOBSUB:
        fmt = "vobsub";
        break;
      default:
        fmt = NULL;
        break;
    }

    if (fmt)
    {
      const gchar *iso639_2 = NULL;

      g_ptr_array_add (argv, g_strdup ("-add"));
      if (language > -1)
        iso639_2 = ogmdvd_get_language_iso639_2 (language);
      if (iso639_2)
        g_ptr_array_add (argv, g_strdup_printf ("%s:fmt=%s:lang=%s", filename, fmt, iso639_2));
      else
        g_ptr_array_add (argv, g_strdup_printf ("%s:fmt=%s", filename, fmt));
    }
  }
}

static void
ogmrip_mp4_foreach_audio (OGMRipContainer *mp4, 
    OGMRipCodec *codec, guint demuxer, gint language, GPtrArray *argv)
{
  const gchar *input;
  gint format;

  input = ogmrip_codec_get_output (codec);
  format = ogmrip_plugin_get_audio_codec_format (G_TYPE_FROM_INSTANCE (codec));

  ogmrip_mp4_append_audio_file (mp4, input, format, language, argv);
}

static void
ogmrip_mp4_foreach_subp (OGMRipContainer *mp4, 
    OGMRipCodec *codec, guint demuxer, gint language, GPtrArray *argv)
{
  const gchar *input;
  gint format;

  input = ogmrip_codec_get_output (codec);
  format = ogmrip_plugin_get_subp_codec_format (G_TYPE_FROM_INSTANCE (codec));

  ogmrip_mp4_append_subp_file (mp4, input, format, language, argv);
}

static void
ogmrip_mp4_foreach_chapters (OGMRipContainer *mp4, 
    OGMRipCodec *codec, guint demuxer, gint language, GPtrArray *argv)
{
  const gchar *input;
  struct stat buf;

  input = ogmrip_codec_get_output (codec);
  if (g_stat (input, &buf) == 0 && buf.st_size > 0)
  {
    g_ptr_array_add (argv, g_strdup ("-chap"));
    g_ptr_array_add (argv, g_strdup (input));
  }
}

static void
ogmrip_mp4_foreach_file (OGMRipContainer *mp4, OGMRipFile *file, GPtrArray *argv)
{
  gchar *filename;

  filename = ogmrip_file_get_filename (file);
  if (filename)
  {
    gint format, language;

    format = ogmrip_file_get_format (file);
    language = ogmrip_file_get_language (file);

    switch (ogmrip_file_get_type (file))
    {
      case OGMRIP_FILE_TYPE_AUDIO:
        ogmrip_mp4_append_audio_file (mp4, filename, format, language, argv);
        break;
      case OGMRIP_FILE_TYPE_SUBP:
        ogmrip_mp4_append_subp_file (mp4, filename, format, language, argv);
        break;
      default:
        g_assert_not_reached ();
        break;
    }
  }
  g_free (filename);
}

static gdouble
ogmrip_mp4_get_output_fps (OGMRipCodec *codec)
{
  guint output_rate_numerator, output_rate_denominator;

  if (ogmrip_codec_get_telecine (codec) || ogmrip_codec_get_progressive (codec))
  {
    output_rate_numerator = 24000;
    output_rate_denominator = 1001;
  }
  else
    ogmrip_codec_get_framerate (codec, &output_rate_numerator, &output_rate_denominator);

  return output_rate_numerator / (gdouble) (output_rate_denominator * ogmrip_codec_get_framestep (codec));
}

static gchar **
ogmrip_mp4box_extract_command (OGMRipVideoCodec *video)
{
  GPtrArray *argv;
  const gchar *filename;

  argv = g_ptr_array_new ();
  g_ptr_array_add (argv, g_strdup (PROGRAM));
  g_ptr_array_add (argv, g_strdup ("-aviraw"));
  g_ptr_array_add (argv, g_strdup ("video"));

  filename = ogmrip_codec_get_output (OGMRIP_CODEC (video));
  g_ptr_array_add (argv, g_strdup (filename));

  g_ptr_array_add (argv, NULL);

  return (gchar **) g_ptr_array_free (argv, FALSE);
}

static gdouble
ogmrip_mp4box_extract_watch (OGMJobExec *exec, const gchar *buffer, OGMRipContainer *mp4)
{
  gchar *sep;
  guint percent;

  if ((sep = strrchr (buffer, '(')) && sscanf (sep, "(%u/100)", &percent) == 1)
    return percent / 100.0;

  return -1.0;
}

static gchar **
ogmrip_mencoder_extract_command (OGMRipVideoCodec *video, const gchar *output)
{
  GPtrArray *argv;
  const gchar *filename;

  argv = g_ptr_array_new ();
  g_ptr_array_add (argv, g_strdup ("mencoder"));
  g_ptr_array_add (argv, g_strdup ("-nocache"));
  g_ptr_array_add (argv, g_strdup ("-noskip"));

  if (MPLAYER_CHECK_VERSION (1,0,3,0))
  {
    g_ptr_array_add (argv, g_strdup ("-noconfig"));
    g_ptr_array_add (argv, g_strdup ("all"));
  }

  g_ptr_array_add (argv, g_strdup ("-mc"));
  g_ptr_array_add (argv, g_strdup ("0"));

  g_ptr_array_add (argv, g_strdup ("-nosound"));

  if (ogmrip_check_mplayer_nosub ())
    g_ptr_array_add (argv, g_strdup ("-nosub"));

  g_ptr_array_add (argv, g_strdup ("-ovc"));
  g_ptr_array_add (argv, g_strdup ("copy"));

  g_ptr_array_add (argv, g_strdup ("-of"));
  g_ptr_array_add (argv, g_strdup ("lavf"));
  g_ptr_array_add (argv, g_strdup ("-lavfopts"));
  g_ptr_array_add (argv, g_strdup ("format=mp4"));

  g_ptr_array_add (argv, g_strdup ("-o"));
  g_ptr_array_add (argv, g_strdup (output));

  filename = ogmrip_codec_get_output (OGMRIP_CODEC (video));
  g_ptr_array_add (argv, g_strdup (filename));

  g_ptr_array_add (argv, NULL);

  return (gchar **) g_ptr_array_free (argv, FALSE);
}

static gint
ogmrip_mp4_get_n_audio_files (OGMRipContainer *mp4)
{
  GSList *files, *file;
  guint n_audio = 0;

  files = ogmrip_container_get_files (mp4);
  for (file = files; file; file = file->next)
  {
    if (ogmrip_file_get_type (OGMRIP_FILE (file->data)) == OGMRIP_FILE_TYPE_AUDIO)
      n_audio ++;
  }

  g_slist_free (files);

  return n_audio;
}

static gint
ogmrip_mp4_get_n_subp_files (OGMRipContainer *mp4)
{
  GSList *files, *file;
  guint n_subp = 0;

  files = ogmrip_container_get_files (mp4);
  for (file = files; file; file = file->next)
  {
    if (ogmrip_file_get_type (OGMRIP_FILE (file->data)) == OGMRIP_FILE_TYPE_SUBP)
      n_subp ++;
  }

  g_slist_free (files);

  return n_subp;
}

static gchar **
ogmrip_mp4_create_command (OGMRipContainer *mp4, const gchar *input, const gchar *output)
{
  GPtrArray *argv;
  OGMRipVideoCodec *video;
  const gchar *label, *fmt = NULL;
  gchar fps[8];

  if ((video = ogmrip_container_get_video (mp4)))
  {
    gint format;

    format = ogmrip_plugin_get_video_codec_format (G_TYPE_FROM_INSTANCE (video));
    switch (format)
    {
      case OGMRIP_FORMAT_MPEG4:
        fmt = "mpeg4-video";
        break;
      case OGMRIP_FORMAT_MPEG2:
        fmt = "mpeg2-video";
        break;
      case OGMRIP_FORMAT_H264:
        fmt = "h264";
        break;
      case OGMRIP_FORMAT_THEORA:
        fmt = "ogg";
        break;
      default:
        fmt = NULL;
        break;
    }

    if (!fmt)
      return NULL;
  }

  argv = g_ptr_array_new ();
  g_ptr_array_add (argv, g_strdup (PROGRAM));

  if (ogmrip_container_get_n_audio (mp4) + ogmrip_mp4_get_n_audio_files (mp4) <= 1 &&
      ogmrip_container_get_n_subp (mp4) + ogmrip_mp4_get_n_subp_files (mp4) < 1)
    g_ptr_array_add (argv, g_strdup ("-isma"));

  g_ptr_array_add (argv, g_strdup ("-nodrop"));
  g_ptr_array_add (argv, g_strdup ("-new"));

  g_ptr_array_add (argv, g_strdup ("-brand"));
  g_ptr_array_add (argv, g_strdup ("mp42"));

  g_ptr_array_add (argv, g_strdup ("-tmp"));
  g_ptr_array_add (argv, g_strdup (ogmrip_fs_get_tmp_dir ()));

  label = ogmrip_container_get_label (mp4);
  if (label)
  {
    g_ptr_array_add (argv, g_strdup ("-itags"));
    g_ptr_array_add (argv, g_strdup_printf ("name=%s", label));
  }

  if (fmt)
  {
    if (!input)
      input = ogmrip_codec_get_output (OGMRIP_CODEC (video));

    g_ascii_formatd (fps, 8, "%.3f",
        ogmrip_mp4_get_output_fps (OGMRIP_CODEC (video)));

    g_ptr_array_add (argv, g_strdup ("-add"));
    g_ptr_array_add (argv, g_strdup_printf ("%s:fmt=%s:fps=%s#video", input, fmt, fps));
  }

  ogmrip_container_foreach_audio (mp4, 
      (OGMRipContainerCodecFunc) ogmrip_mp4_foreach_audio, argv);
  ogmrip_container_foreach_subp (mp4, 
      (OGMRipContainerCodecFunc) ogmrip_mp4_foreach_subp, argv);
  ogmrip_container_foreach_chapters (mp4, 
      (OGMRipContainerCodecFunc) ogmrip_mp4_foreach_chapters, argv);
  ogmrip_container_foreach_file (mp4, 
      (OGMRipContainerFileFunc) ogmrip_mp4_foreach_file, argv);

  g_ptr_array_add (argv, g_strdup (output));

  g_ptr_array_add (argv, NULL);

  return (gchar **) g_ptr_array_free (argv, FALSE);
}

static gdouble
ogmrip_mp4_create_watch (OGMJobExec *exec, const gchar *buffer, OGMRipMp4 *mp4)
{
  guint percent;
  gchar *sep;

  if ((sep = strrchr (buffer, '(')) && sscanf (sep, "(%u/100)", &percent) == 1)
  {
    if (percent < mp4->old_percent)
      mp4->streams ++;

    mp4->old_percent = percent;

    return mp4->streams / (gdouble) mp4->nstreams + percent / (mp4->nstreams * 100.0);
  }

  return -1.0;
}

static gchar **
ogmrip_mp4_split_command (OGMRipContainer *mp4, const gchar *input)
{
  GPtrArray *argv;
  guint tsize;

  argv = g_ptr_array_new ();
  g_ptr_array_add (argv, g_strdup (PROGRAM));

  g_ptr_array_add (argv, g_strdup ("-tmp"));
  g_ptr_array_add (argv, g_strdup (ogmrip_fs_get_tmp_dir ()));

  ogmrip_container_get_split (OGMRIP_CONTAINER (mp4), NULL, &tsize);
  g_ptr_array_add (argv, g_strdup ("-splits"));
  g_ptr_array_add (argv, g_strdup_printf ("%d", tsize));

  g_ptr_array_add (argv, g_strdup (input));

  g_ptr_array_add (argv, NULL);

  return (gchar **) g_ptr_array_free (argv, FALSE);
}

static gdouble
ogmrip_mp4_split_watch (OGMJobExec *exec, const gchar *buffer, OGMRipMp4 *mp4)
{
  gchar *sep;
  guint percent;

  if ((sep = strrchr (buffer, '(')) && sscanf (sep, "(%u/100)", &percent) == 1)
  {
    if (g_str_has_prefix (buffer, "Splitting:"))
    {
      mp4->split_percent = percent;

      return (percent + 100 * mp4->splits) / (100.0 * (mp4->nsplits + 1));
    }
    else if (g_str_has_prefix (buffer, "ISO File Writing:"))
    {
      if (percent < mp4->split_percent)
        mp4->splits ++;

      return (percent + mp4->split_percent + 100 * mp4->splits) / (100.0 * (mp4->nsplits + 1));
    }
  }

  return -1.0;
}

G_DEFINE_TYPE (OGMRipMp4, ogmrip_mp4, OGMRIP_TYPE_CONTAINER)

static void
ogmrip_mp4_class_init (OGMRipMp4Class *klass)
{
  OGMJobSpawnClass *spawn_class;

  spawn_class = OGMJOB_SPAWN_CLASS (klass);
  spawn_class->run = ogmrip_mp4_run;
}

static void
ogmrip_mp4_init (OGMRipMp4 *mp4)
{
}

static gchar *
ogmrip_mp4_get_h264_filename (OGMRipVideoCodec *video)
{
  const gchar *name;
  gchar *dot, *filename;

  name = ogmrip_codec_get_output (OGMRIP_CODEC (video));
  dot = strrchr (name, '.');

  filename = g_new0 (gchar, dot - name + 12);
  strncpy (filename, name, dot - name);
  strcat (filename, "_video.h264");

  return filename;
}

static void
ogmrip_mp4_get_n_vobsub (OGMRipContainer *container, OGMRipCodec *codec, guint demuxer, gint language, gint *nvobsub)
{
  if (ogmrip_plugin_get_subp_codec_format (G_TYPE_FROM_INSTANCE (codec)) == OGMRIP_FORMAT_VOBSUB)
    (*nvobsub) ++;
}

static gint
ogmrip_mp4_run (OGMJobSpawn *spawn)
{
  OGMJobSpawn *queue, *child;
  OGMRipVideoCodec *video;
  OGMRipMp4 *mp4;

  gchar **argv, *filename = NULL;
  const gchar *output;

  gint result = OGMJOB_RESULT_ERROR;

  g_return_val_if_fail (OGMRIP_IS_MP4 (spawn), OGMJOB_RESULT_ERROR);

  mp4 = OGMRIP_MP4 (spawn);

  output = ogmrip_container_get_output (OGMRIP_CONTAINER (spawn));
  ogmrip_container_get_split (OGMRIP_CONTAINER (spawn), &mp4->nsplits, NULL);

  queue = ogmjob_queue_new ();
  ogmjob_container_add (OGMJOB_CONTAINER (spawn), queue);
  g_object_unref (queue);

  video = ogmrip_container_get_video (OGMRIP_CONTAINER (spawn));
  if (ogmrip_plugin_get_video_codec_format (G_TYPE_FROM_INSTANCE (video)) == OGMRIP_FORMAT_H264)
  {
    gboolean global_header = FALSE;

    if (g_object_class_find_property (G_OBJECT_GET_CLASS (video), "global_header"))
      g_object_get (video, "global_header", &global_header, NULL);

    if (global_header)
    {
      filename = ogmrip_fs_mktemp ("video.XXXXXX", NULL);

      argv = ogmrip_mencoder_extract_command (video, filename);
      if (!argv)
      {
        g_free (filename);
        return OGMJOB_RESULT_ERROR;
      }

      child = ogmjob_exec_newv (argv);
      ogmjob_exec_add_watch_full (OGMJOB_EXEC (child), (OGMJobWatch) ogmrip_mencoder_container_watch, spawn, TRUE, FALSE, FALSE);
    }
    else
    {
      argv = ogmrip_mp4box_extract_command (video);
      if (!argv)
        return OGMJOB_RESULT_ERROR;

      child = ogmjob_exec_newv (argv);
      ogmjob_exec_add_watch_full (OGMJOB_EXEC (child), (OGMJobWatch) ogmrip_mp4box_extract_watch, spawn, TRUE, FALSE, FALSE);

      filename = ogmrip_mp4_get_h264_filename (video);
    }

    ogmjob_container_add (OGMJOB_CONTAINER (queue), child);
    g_object_unref (child);
  }

  argv = ogmrip_mp4_create_command (OGMRIP_CONTAINER (spawn), filename, output);
  if (argv)
  {
    gint nvobsub = 0;

    ogmrip_container_foreach_subp (OGMRIP_CONTAINER (spawn), (OGMRipContainerCodecFunc) ogmrip_mp4_get_n_vobsub, &nvobsub);

    mp4->old_percent = 0;
    mp4->nstreams = 2 + ogmrip_container_get_n_audio (OGMRIP_CONTAINER (spawn)) + nvobsub;
    mp4->streams = 0;

    child = ogmjob_exec_newv (argv);
    ogmjob_exec_add_watch_full (OGMJOB_EXEC (child), (OGMJobWatch) ogmrip_mp4_create_watch, spawn, TRUE, FALSE, FALSE);
    ogmjob_container_add (OGMJOB_CONTAINER (queue), child);
    g_object_unref (child);

    if (mp4->nsplits > 1 && result == OGMJOB_RESULT_SUCCESS)
    {
      argv = ogmrip_mp4_split_command (OGMRIP_CONTAINER (spawn), output);
      if (argv)
      {
        mp4->split_percent = 0;
        mp4->splits = 0;

        child = ogmjob_exec_newv (argv);
        ogmjob_exec_add_watch_full (OGMJOB_EXEC (child), (OGMJobWatch) ogmrip_mp4_split_watch, spawn, TRUE, FALSE, FALSE);
        ogmjob_container_add (OGMJOB_CONTAINER (queue), child);
        g_object_unref (child);
      }
    }

    result = OGMJOB_SPAWN_CLASS (ogmrip_mp4_parent_class)->run (spawn);
  }

  ogmjob_container_remove (OGMJOB_CONTAINER (spawn), queue);

  if (filename)
    ogmrip_fs_unref (filename, TRUE);

  if (mp4->nsplits > 1)
    ogmrip_fs_unref (g_strdup (output), TRUE);

  return result;
}

static OGMRipContainerPlugin mp4_plugin =
{
  NULL,
  G_TYPE_NONE,
  "mp4",
  N_("Mpeg-4 Media (MP4)"),
  FALSE,
  TRUE,
  G_MAXINT,
  G_MAXINT,
  NULL
};

static gint formats[] = 
{
  OGMRIP_FORMAT_MPEG4,
  OGMRIP_FORMAT_MPEG2,
  OGMRIP_FORMAT_H264,
  OGMRIP_FORMAT_THEORA,
  OGMRIP_FORMAT_AAC,
  OGMRIP_FORMAT_MP3,
  OGMRIP_FORMAT_VORBIS,
  OGMRIP_FORMAT_SRT,
  OGMRIP_FORMAT_VOBSUB,
  -1,
  -1,
  -1
};

OGMRipContainerPlugin *
ogmrip_init_plugin (GError **error)
{
  gchar *output;
  gint major_version = 0, minor_version = 0, micro_version = 0;

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

  if (!g_spawn_command_line_sync (PROGRAM " -version", &output, NULL, NULL, NULL))
  {
    g_set_error (error, OGMRIP_PLUGIN_ERROR, OGMRIP_PLUGIN_ERROR_REQ, _("MP4Box is missing"));
    return NULL;
  }

  if (g_str_has_prefix (output, "MP4Box - GPAC version "))
  {
    gchar *end;

    errno = 0;
    major_version = strtoul (output + 22, &end, 10);
    if (!errno && *end == '.')
      minor_version = strtoul (end + 1, NULL, 10);
    if (!errno && *end == '.')
      micro_version = strtoul (end + 1, NULL, 10);
  }
  g_free (output);

  if ((major_version > 0) ||
      (major_version == 0 && minor_version > 4) ||
      (major_version == 0 && minor_version == 4 && micro_version >= 5))
  {
    guint i = 0;

    while (formats[i] != -1)
      i++;

    formats[i] = OGMRIP_FORMAT_AC3;
    formats[i+1] = OGMRIP_FORMAT_COPY;
  }

  mp4_plugin.type = OGMRIP_TYPE_MP4;
  mp4_plugin.formats = formats;

  return &mp4_plugin;
}

