/* gcc -g -Wall `pkg-config --cflags --libs gtk+-2.0 gstreamer-0.10` -o seek seek.c */

#include <unistd.h>
#include <math.h>

#include <gtk/gtk.h>
#include <gst/gst.h>


gchar *filename;

GstElement *pipeline;
GstElement *filesrc;
GstElement *decodebin;
GstElement *vsink;

gint    width;
gint    height;
int     fps_num;
int     fps_den;
gint    frame_nbr;

gboolean got_signal = FALSE;
GCond   *cond = NULL;
GMutex  *mutex = NULL;


#define clip_8_bit(val)              \
{                                    \
  if (G_UNLIKELY (val < 0))          \
    val = 0;                         \
  else if (G_UNLIKELY (val > 255))   \
    val = 255;                       \
}

static void show_frame (guchar  *rgb);

static gboolean do_seek ();

static void cb_handoff(GObject * obj, GstBuffer *buffer, GstPad * pad, void * data);
static void new_decoded_pad_cb (GstElement * decodebin, GstPad * new_pad, gboolean last, GstElement * pipeline);

int
main (int argc, char *argv[])
{
  GstStateChangeReturn res;

  gst_init (&argc, &argv);

  pipeline = gst_pipeline_new ("pipeline");

  filesrc = gst_element_factory_make ("filesrc", "filesrc");
  g_assert (filesrc);

  decodebin = gst_element_factory_make ("decodebin", "decodebin");
  g_signal_connect (decodebin, "new-decoded-pad",
                    G_CALLBACK (new_decoded_pad_cb), pipeline);
  g_assert (decodebin);

  gst_bin_add_many (GST_BIN (pipeline), filesrc, decodebin, NULL);
  gst_element_link (filesrc, decodebin);

  if (argc < 3) {
    g_print ("usage: %s <uri> frame_nbr\n", argv[0]);
    exit (-1);
  }
  filename = argv[1];
  g_object_set (G_OBJECT (filesrc), "location", filename, NULL);
  frame_nbr = atoi (argv[2]);

  g_print ("pause..\n");
  res = gst_element_set_state (pipeline, GST_STATE_PAUSED);
  if (res == GST_STATE_CHANGE_FAILURE) {
    g_print ("could not pause\n");
    exit (-1);
  }
  g_print ("waiting..\n");
  res = gst_element_get_state( pipeline, NULL, NULL, GST_CLOCK_TIME_NONE );
  if (res != GST_STATE_CHANGE_SUCCESS) {
    g_print ("could not complete pause\n");
    exit (-1);
  }
  g_print ("stats..\n");

  {
    GstIterator *it;
    gpointer data;
    
    it = gst_element_iterate_src_pads (decodebin);
    while (gst_iterator_next (it, &data) == GST_ITERATOR_OK) {
      GstPad *pad = GST_PAD (data);
      GstCaps *caps;
      gchar *str;
      GstQuery *query;

      g_print ("stream %s:\n", GST_OBJECT_NAME (pad));
      
      caps = gst_pad_get_caps (pad);
      str = gst_caps_to_string (caps);
      g_print (" caps: %s\n\n", str);
      if (g_str_has_prefix (str, "video/"))
        {
          GstPad       *videopad;
          GstStructure *structure;
          guint32       fourcc;
          const GValue *val;
          
          videopad = gst_pad_get_peer (pad);
          vsink = gst_pad_get_parent_element (videopad);
          g_object_set (G_OBJECT (vsink), "signal-handoffs", TRUE, NULL);
          g_signal_connect (vsink, "handoff",
                            G_CALLBACK (cb_handoff), pipeline);
/*           videopad = gst_element_get_pad (vsink, "sink"); */
/*           gst_pad_link (pad, videopad); */
/*           gst_element_set_state (GST_ELEMENT (vsink), GST_STATE_PAUSED); */

          structure = gst_caps_get_structure (GST_CAPS (caps), 0);
          gst_structure_get_int (structure, "width", &width);
          gst_structure_get_int (structure, "height", &height);
          val = gst_structure_get_value (structure, "framerate");
          fps_num = gst_value_get_fraction_numerator (val);
          fps_den = gst_value_get_fraction_denominator (val);
          gst_structure_get_fourcc (structure, "format", &fourcc);
          printf ("  size      : %dx%d\n", width, height);
          printf ("  framerate : %d/%d\n", fps_num, fps_den);
          //          printf ("  fourcc    : " GST_FOURCC_FORMAT "\n", GST_FOURCC_ARGS (fourcc));
        }
      g_free (str);
      
      query = gst_query_new_duration (GST_FORMAT_TIME);
      if (gst_pad_query (pad, query)) {
        gint64 duration;
        
        gst_query_parse_duration (query, NULL, &duration);
        
        g_print ("  duration  : %" GST_TIME_FORMAT "\n\n", GST_TIME_ARGS (duration));
      }
      gst_query_unref (query);
      
      gst_object_unref (pad);
    }
    gst_iterator_free (it);
  }

  /* seek */
  gst_element_set_state (pipeline, GST_STATE_PLAYING);

  if (!do_seek ())
    {
      g_print ("bad seek, exiting...\n");
      exit (0);
    }

  gst_element_set_state (pipeline, GST_STATE_NULL);

  gst_object_unref (GST_OBJECT (pipeline));

  return 1;
}

static void
new_decoded_pad_cb (GstElement * decodebin,
                    GstPad * new_pad,
                    gboolean last,
                    GstElement * pipeline)
{
  GstElement *fakesink;
  GstPad *sinkpad;

  fakesink = gst_element_factory_make ("fakesink", NULL);
  gst_bin_add (GST_BIN (pipeline), fakesink);

  sinkpad = gst_element_get_pad (fakesink, "sink");
  if (GST_PAD_LINK_FAILED (gst_pad_link (new_pad, sinkpad))) {
    g_warning ("Failed to link %s:%s to %s:%s", GST_DEBUG_PAD_NAME (new_pad),
        GST_DEBUG_PAD_NAME (sinkpad));
    gst_bin_remove (GST_BIN (pipeline), fakesink);
  } else {
    gst_element_set_state (fakesink, GST_STATE_PAUSED);
  }
}

static void
show_frame (guchar  *rgb)
{
    GtkWidget *win, *img;
    GdkPixbuf *pix;
    gchar buf[4096];

    if (!rgb)
      return;

    gtk_init (NULL, NULL);

    win = gtk_window_new (GTK_WINDOW_TOPLEVEL);
    snprintf (buf, 4096, "%s - frame #%d - %.2fs",
              filename,
              frame_nbr,
              (gdouble)(frame_nbr*fps_den) / fps_num);
    gtk_window_set_title (GTK_WINDOW (win), buf);
    img = gtk_image_new ();
    gtk_container_add (GTK_CONTAINER (win), img);
    g_signal_connect (win, "delete-event", gtk_main_quit, NULL);

    pix = gdk_pixbuf_new_from_data (rgb,
				    GDK_COLORSPACE_RGB, FALSE,
				    8, width, height, 3 * width,
				    (GdkPixbufDestroyNotify)g_free,
                                    rgb);

    gtk_image_set_from_pixbuf (GTK_IMAGE (img), pix);
    g_object_unref (pix);

    gtk_widget_show_all (win);

    gtk_main ();

}

gboolean
do_seek ()
{
  guint64  time;
  gboolean result = TRUE;

  g_print ("seeking to second %.4f, frame num. %d\n",
	   (double)(frame_nbr * fps_den) / fps_num, frame_nbr);

  cond = g_cond_new ();
  mutex = g_mutex_new ();
  g_mutex_lock (mutex);

  g_print ("%d  %d\n", fps_num, fps_den);
  time = floor ((GST_SECOND * frame_nbr * fps_den) / fps_num);
  if (!gst_element_seek(vsink, 1.0,
                        GST_FORMAT_TIME,
                        GST_SEEK_FLAG_ACCURATE | GST_SEEK_FLAG_FLUSH,
                        GST_SEEK_TYPE_CUR,
                        time,
                        GST_SEEK_TYPE_NONE,
                        -1))
    {
      g_print ("not good...\n");
      result = FALSE;
      goto do_seek_end;
    }

  while (!got_signal)
    g_cond_wait (cond, mutex);

  g_mutex_unlock (mutex);
  g_mutex_free (mutex);
  g_cond_free (cond);

 do_seek_end:

  return result;
}

/* These macros are adapted from videotestsrc.c 
 *  and/or gst-plugins/gst/games/gstvideoimage.c */
#define I420_Y_ROWSTRIDE(width) (GST_ROUND_UP_4(width))
#define I420_U_ROWSTRIDE(width) (GST_ROUND_UP_8(width)/2)
#define I420_V_ROWSTRIDE(width) ((GST_ROUND_UP_8(I420_Y_ROWSTRIDE(width)))/2)

#define I420_Y_OFFSET(w,h) (0)
#define I420_U_OFFSET(w,h) (I420_Y_OFFSET(w,h)+(I420_Y_ROWSTRIDE(w)*GST_ROUND_UP_2(h)))
#define I420_V_OFFSET(w,h) (I420_U_OFFSET(w,h)+(I420_U_ROWSTRIDE(w)*GST_ROUND_UP_2(h)/2))

#define I420_SIZE(w,h)     (I420_V_OFFSET(w,h)+(I420_V_ROWSTRIDE(w)*GST_ROUND_UP_2(h)/2))

void
cb_handoff(GObject * obj, GstBuffer *buffer, GstPad * pad, void * data)
{
  gint i, j;
  unsigned char *Y;
  unsigned char *U;
  unsigned char *V;
  gint offset_Y, offset_U, offset_V;
  guchar  *rgb;

  g_print ("handoff\n");

  if (GST_BUFFER_TIMESTAMP (buffer) == GST_CLOCK_TIME_NONE)
    g_print ("TIMESTAMP_NONE !!\n");

  if (GST_BUFFER_FLAG_IS_SET (buffer, GST_BUFFER_FLAG_DELTA_UNIT))
    g_print ("Delta Frame\n");
  else
    g_print ("Key Frame\n");

  g_print ("%f  %f\n",
           (gdouble)GST_BUFFER_TIMESTAMP (buffer) / GST_SECOND,
           ((gdouble)(frame_nbr * fps_den ) / fps_num));

  if ((gdouble)GST_BUFFER_TIMESTAMP (buffer) / GST_SECOND < ((gdouble)(frame_nbr * fps_den ) / fps_num))
  {
    g_print ("skipping frame with timestamp %.5fs < %.5fs\n",
	     (gdouble) GST_BUFFER_TIMESTAMP (buffer) / GST_SECOND,
	     (gdouble)(frame_nbr * fps_den ) / fps_num);
    return;
  }

  g_print ("====== handoff: got frame =====\n");


  g_print ("frame timestamp = %.3fs\n",
	   (gdouble) GST_BUFFER_TIMESTAMP (buffer) / GST_SECOND);
  g_print ("frame offset    = %" G_GINT64_FORMAT "\n",
	   (gint64) GST_BUFFER_OFFSET (buffer));

  g_print ("size : %d  %d\n", width*height + width*height / 2, GST_BUFFER_SIZE(buffer));

  rgb = g_malloc0 (width * height * 3);
  
  /* the video is in I420 */
  Y = GST_BUFFER_DATA (buffer) + I420_Y_OFFSET(width,height);
  U = GST_BUFFER_DATA (buffer) + I420_U_OFFSET(width,height);
  V = GST_BUFFER_DATA (buffer) + I420_V_OFFSET(width,height);

  if (GST_BUFFER_CAPS (buffer)) {
    gchar *s = gst_caps_to_string (GST_BUFFER_CAPS (buffer));
    g_print ("buffer caps: %s\n", s);
    g_free (s);
  }

  offset_Y = 0;
  for (j = 0 ; j < height ; j++)
    {
      //offset_Y = j * I420_Y_ROWSTRIDE (width);
      for (i = 0 ; i < width ; i++, offset_Y++)
        {
          gint  r, g, b;

          offset_U = ((j >> 1) * I420_U_ROWSTRIDE (width)) + (i >> 1);
          offset_V = ((j >> 1) * I420_V_ROWSTRIDE (width)) + (i >> 1);

          r = 1.164 * (Y[offset_Y] - 16) + 1.596 * (V[offset_U] - 128);
          g = 1.164 * (Y[offset_Y] - 16) - 0.813 * (V[offset_U] - 128) - 0.391 * (U[offset_U] - 128);
          b = 1.164 * (Y[offset_Y] - 16) + 2.018 * (U[offset_U] - 128);
	
          clip_8_bit (r);
          clip_8_bit (g);
          clip_8_bit (b);
	
          rgb[offset_Y * 3 + 0] = r;
          rgb[offset_Y * 3 + 1] = g;
          rgb[offset_Y * 3 + 2] = b;
        }
    }

  show_frame (rgb);

  g_mutex_lock (mutex);
  got_signal = TRUE;
  g_cond_broadcast (cond);
  g_mutex_unlock (mutex);
}
