/* GStreamer interactive test for the gdkpixbufsink element
 * Copyright (C) 2008 Tim-Philipp Müller <tim centricular net>
 *
 * 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 <gst/gst.h>
#include <gtk/gtk.h>

typedef struct
{
  GstElement *pipe;
  GstElement *sink;
  gboolean got_video;

  GtkWidget *win;
  GtkWidget *img;
  GtkWidget *slider;
  GtkWidget *accurate_cb;

  gboolean accurate;            /* whether to try accurate seeks */

  gint64 cur_pos;
  gboolean prerolled;
} AppInfo;

static void seek_to (AppInfo * info, gdouble percent);

static GstElement *
create_element (const gchar * factory_name)
{
  GstElement *element;

  element = gst_element_factory_make (factory_name, NULL);

  if (element == NULL)
    g_error ("Failed to create '%s' element", factory_name);

  return element;
}

static void
new_decoded_pad (GstElement * dec, GstPad * new_pad, AppInfo * info)
{
  const gchar *sname;
  GstElement *csp, *scale, *filter;
  GstStructure *s;
  GstCaps *caps;
  GstPad *sinkpad;

  /* already found a video stream? */
  if (info->got_video)
    return;

  /* FIXME: is this racy or does decodebin make sure caps are always
   * negotiated at this point? */
  caps = gst_pad_query_caps (new_pad, NULL);
  g_return_if_fail (caps != NULL);

  s = gst_caps_get_structure (caps, 0);
  sname = gst_structure_get_name (s);
  if (!g_str_has_prefix (sname, "video/x-raw"))
    goto not_video;

  csp = create_element ("videoconvert");
  scale = create_element ("videoscale");
  filter = create_element ("capsfilter");
  info->sink = create_element ("gdkpixbufsink");
  g_object_set (info->sink, "qos", FALSE, "max-lateness", (gint64) - 1, NULL);

  gst_bin_add_many (GST_BIN (info->pipe), csp, scale, filter, info->sink, NULL);

  sinkpad = gst_element_get_static_pad (csp, "sink");
  if (GST_PAD_LINK_FAILED (gst_pad_link (new_pad, sinkpad)))
    g_error ("Can't link new decoded pad to videoconvert's sink pad");
  gst_object_unref (sinkpad);

  if (!gst_element_link (csp, scale))
    g_error ("Can't link videoconvert to videoscale");
  if (!gst_element_link (scale, filter))
    g_error ("Can't link videoscale to capsfilter");
  if (!gst_element_link (filter, info->sink))
    g_error ("Can't link capsfilter to gdkpixbufsink");

  gst_element_set_state (info->sink, GST_STATE_PAUSED);
  gst_element_set_state (filter, GST_STATE_PAUSED);
  gst_element_set_state (scale, GST_STATE_PAUSED);
  gst_element_set_state (csp, GST_STATE_PAUSED);

  info->got_video = TRUE;
  return;

not_video:
  return;
}

static void
no_more_pads (GstElement * decodebin, AppInfo * info)
{
  if (!info->got_video) {
    g_error ("This file does not contain a video track, or you do not have "
        "the necessary decoder(s) installed");
  }
}

static void
bus_message_cb (GstBus * bus, GstMessage * msg, AppInfo * info)
{
  switch (GST_MESSAGE_TYPE (msg)) {
    case GST_MESSAGE_ASYNC_DONE:{
      /* only interested in async-done messages from the top-level pipeline */
      if (msg->src != GST_OBJECT_CAST (info->pipe))
        break;

      if (!info->prerolled) {
        /* make slider visible if it's not visible already */
        gtk_widget_show (info->slider);

        /* initial frame is often black, so seek to beginning plus a bit */
        seek_to (info, 0.001);
        info->prerolled = TRUE;
      }

      /* update position */
      if (!gst_element_query_position (info->pipe, GST_FORMAT_TIME,
              &info->cur_pos))
        info->cur_pos = -1;
      break;
    }
    case GST_MESSAGE_ELEMENT:{
      const GValue *val;
      GdkPixbuf *pixbuf = NULL;
      const GstStructure *structure;

      /* only interested in element messages from our gdkpixbufsink */
      if (msg->src != GST_OBJECT_CAST (info->sink))
        break;

      /* only interested in these two messages */
      if (!gst_message_has_name (msg, "preroll-pixbuf") &&
          !gst_message_has_name (msg, "pixbuf")) {
        break;
      }

      g_print ("pixbuf\n");
      structure = gst_message_get_structure (msg);
      val = gst_structure_get_value (structure, "pixbuf");
      g_return_if_fail (val != NULL);

      pixbuf = GDK_PIXBUF (g_value_dup_object (val));
      gtk_image_set_from_pixbuf (GTK_IMAGE (info->img), pixbuf);
      g_object_unref (pixbuf);
      break;
    }
    case GST_MESSAGE_ERROR:{
      GError *err = NULL;
      gchar *dbg = NULL;

      gst_message_parse_error (msg, &err, &dbg);
      g_error ("Error: %s\n%s\n", err->message, (dbg) ? dbg : "");
      g_error_free (err);
      g_free (dbg);
      break;
    }
    default:
      break;
  }
}

static gboolean
create_pipeline (AppInfo * info, const gchar * filename)
{
  GstElement *src, *dec;
  GstBus *bus;

  info->pipe = gst_pipeline_new ("pipeline");
  src = create_element ("filesrc");
  g_object_set (src, "location", filename, NULL);

  dec = create_element ("decodebin");

  gst_bin_add_many (GST_BIN (info->pipe), src, dec, NULL);
  if (!gst_element_link (src, dec))
    g_error ("Can't link filesrc to decodebin");

  g_signal_connect (dec, "pad-added", G_CALLBACK (new_decoded_pad), info);

  g_signal_connect (dec, "no-more-pads", G_CALLBACK (no_more_pads), info);

  /* set up bus */
  bus = gst_element_get_bus (info->pipe);
  gst_bus_add_signal_watch (bus);
  g_signal_connect (bus, "message", G_CALLBACK (bus_message_cb), info);
  gst_object_unref (bus);

  return TRUE;
}

static void
seek_to (AppInfo * info, gdouble percent)
{
  GstSeekFlags seek_flags;
  gint64 seek_pos, dur = -1;

  if (!gst_element_query_duration (info->pipe, GST_FORMAT_TIME, &dur)
      || dur <= 0) {
    g_printerr ("Could not query duration\n");
    return;
  }

  seek_pos = gst_gdouble_to_guint64 (gst_guint64_to_gdouble (dur) * percent);
  g_print ("Seeking to %" GST_TIME_FORMAT ", accurate: %d\n",
      GST_TIME_ARGS (seek_pos), info->accurate);

  seek_flags = GST_SEEK_FLAG_FLUSH;

  if (info->accurate)
    seek_flags |= GST_SEEK_FLAG_ACCURATE;
  else
    seek_flags |= GST_SEEK_FLAG_KEY_UNIT;

  if (!gst_element_seek_simple (info->pipe, GST_FORMAT_TIME, seek_flags,
          seek_pos)) {
    g_printerr ("Seek failed.\n");
    return;
  }
}

static void
slider_cb (GtkRange * range, AppInfo * info)
{
  gdouble val;

  val = gtk_range_get_value (range);
  seek_to (info, val);
}

static gchar *
slider_format_value_cb (GtkScale * scale, gdouble value, AppInfo * info)
{
  gchar s[64];

  if (info->cur_pos < 0)
    return g_strdup_printf ("%0.1g%%", value * 100.0);

  g_snprintf (s, 64, "%" GST_TIME_FORMAT, GST_TIME_ARGS (info->cur_pos));
  s[10] = '\0';
  return g_strdup (s);
}

static void
accurate_toggled_cb (GtkToggleButton * toggle, AppInfo * info)
{
  info->accurate = gtk_toggle_button_get_active (toggle);
}

static void
run_gui (const gchar * filename)
{
  GtkWidget *vbox, *hbox;
  AppInfo *info;

  info = g_new0 (AppInfo, 1);

  /* create pipeline */
  if (!create_pipeline (info, filename))
    goto done;

  /* create window */
  info->win = gtk_window_new (GTK_WINDOW_TOPLEVEL);
  g_signal_connect (info->win, "delete-event", G_CALLBACK (gtk_main_quit),
      NULL);

  vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6);
  gtk_container_set_border_width (GTK_CONTAINER (vbox), 12);
  gtk_container_add (GTK_CONTAINER (info->win), vbox);

  info->img = gtk_image_new ();
  gtk_box_pack_start (GTK_BOX (vbox), info->img, FALSE, FALSE, 6);

  hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6);
  gtk_box_pack_start (GTK_BOX (vbox), hbox, FALSE, FALSE, 6);

  info->accurate_cb = gtk_check_button_new_with_label ("accurate seek "
      "(might not work reliably with all demuxers)");
  gtk_box_pack_start (GTK_BOX (hbox), info->accurate_cb, FALSE, FALSE, 6);
  g_signal_connect (info->accurate_cb, "toggled",
      G_CALLBACK (accurate_toggled_cb), info);

  info->slider = gtk_scale_new_with_range (GTK_ORIENTATION_HORIZONTAL,
      0.0, 1.0, 0.001);
  gtk_box_pack_start (GTK_BOX (vbox), info->slider, FALSE, FALSE, 6);
  g_signal_connect (info->slider, "value-changed",
      G_CALLBACK (slider_cb), info);
  g_signal_connect (info->slider, "format-value",
      G_CALLBACK (slider_format_value_cb), info);

  /* and go! */
  gst_element_set_state (info->pipe, GST_STATE_PAUSED);

  gtk_widget_show_all (info->win);
  gtk_widget_hide (info->slider);       /* hide until we're prerolled */
  gtk_main ();

done:

  g_free (info);
}

static gchar **filenames = NULL;

int
main (int argc, char **argv)
{
  static const GOptionEntry test_goptions[] = {
    {G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &filenames, NULL},
    {NULL, '\0', 0, 0, NULL, NULL, NULL}
  };
  GOptionContext *ctx;
  GError *opt_err = NULL;

  gtk_init (&argc, &argv);

  /* command line option parsing */
  ctx = g_option_context_new (" VIDEOFILE");
  g_option_context_add_group (ctx, gst_init_get_option_group ());
  g_option_context_add_main_entries (ctx, test_goptions, NULL);

  if (!g_option_context_parse (ctx, &argc, &argv, &opt_err)) {
    g_error ("Error parsing command line options: %s", opt_err->message);
    return -1;
  }

  if (filenames == NULL || filenames[0] == NULL || filenames[0][0] == '\0') {
    g_printerr ("Please specify a path to a video file\n\n");
    return -1;
  }

  run_gui (filenames[0]);

  g_free (filenames);
  g_option_context_free (ctx);

  return 0;
}
