/* GStreamer
 *
 * unit test for theoraenc
 *
 * Copyright (C) 2006 Andy Wingo <wingo at pobox.com>
 *
 * 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., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */

#include <gst/check/gstcheck.h>
#include <gst/check/gstbufferstraw.h>

#include <theora/theora.h>

#ifndef GST_DISABLE_PARSE

#define TIMESTAMP_OFFSET G_GINT64_CONSTANT(3249870963)
#define FRAMERATE 10

/* I know all of these have a shift of 6 bits */
#define GRANULEPOS_SHIFT 6


#define check_buffer_is_header(buffer,is_header) \
  fail_unless (GST_BUFFER_FLAG_IS_SET (buffer,   \
          GST_BUFFER_FLAG_IN_CAPS) == is_header, \
      "GST_BUFFER_IN_CAPS is set to %d but expected %d", \
      GST_BUFFER_FLAG_IS_SET (buffer, GST_BUFFER_FLAG_IN_CAPS), is_header)

#define check_buffer_timestamp(buffer,timestamp) \
  fail_unless (GST_BUFFER_TIMESTAMP (buffer) == timestamp, \
      "expected timestamp %" GST_TIME_FORMAT \
      ", but got timestamp %" GST_TIME_FORMAT, \
      GST_TIME_ARGS (timestamp), GST_TIME_ARGS (GST_BUFFER_TIMESTAMP (buffer)))

#define check_buffer_duration(buffer,duration) \
  fail_unless (GST_BUFFER_DURATION (buffer) == duration, \
      "expected duration %" GST_TIME_FORMAT \
      ", but got duration %" GST_TIME_FORMAT, \
      GST_TIME_ARGS (duration), GST_TIME_ARGS (GST_BUFFER_DURATION (buffer)))

static gboolean old_libtheora;

static void
check_libtheora (void)
{
  old_libtheora = (theora_version_number () <= 0x00030200);
}

static void
check_buffer_granulepos (GstBuffer * buffer, gint64 granulepos)
{
  GstClockTime clocktime;
  int framecount;

  /* With old versions of libtheora, the granulepos represented the
   * start time, not end time. Adapt for that. */
  if (old_libtheora) {
    if (granulepos >> GRANULEPOS_SHIFT)
      granulepos -= 1 << GRANULEPOS_SHIFT;
    else if (granulepos)
      granulepos -= 1;
  }

  fail_unless (GST_BUFFER_OFFSET_END (buffer) == granulepos,
      "expected granulepos %" G_GUINT64_FORMAT
      ", but got granulepos %" G_GUINT64_FORMAT,
      granulepos, GST_BUFFER_OFFSET_END (buffer));

  /* contrary to what we record as TIMESTAMP, we can use OFFSET to check
   * the granulepos correctly here */
  framecount = GST_BUFFER_OFFSET_END (buffer);
  framecount = granulepos >> GRANULEPOS_SHIFT;
  framecount += granulepos & ((1 << GRANULEPOS_SHIFT) - 1);
  clocktime = gst_util_uint64_scale (framecount, GST_SECOND, FRAMERATE);

  fail_unless (clocktime == GST_BUFFER_OFFSET (buffer),
      "expected OFFSET set to clocktime %" GST_TIME_FORMAT
      ", but got %" GST_TIME_FORMAT,
      GST_TIME_ARGS (clocktime), GST_TIME_ARGS (GST_BUFFER_OFFSET (buffer)));
}

/* this check is here to check that the granulepos we derive from the
   timestamp is about correct. This is "about correct" because you can't
   precisely go from timestamp to granulepos due to the downward-rounding
   characteristics of gst_util_uint64_scale, so you check if granulepos is
   equal to the number, or the number plus one. */
/* should be from_endtime, but theora's granulepos mapping is "special" */
static void
check_buffer_granulepos_from_starttime (GstBuffer * buffer,
    GstClockTime starttime)
{
  gint64 granulepos, expected, framecount;

  granulepos = GST_BUFFER_OFFSET_END (buffer);
  /* Now convert to 'granulepos for start time', depending on libtheora 
   * version */
  if (!old_libtheora) {
    if (granulepos & ((1 << GRANULEPOS_SHIFT) - 1))
      granulepos -= 1;
    else if (granulepos)
      granulepos -= 1 << GRANULEPOS_SHIFT;
  }

  framecount = granulepos >> GRANULEPOS_SHIFT;
  framecount += granulepos & ((1 << GRANULEPOS_SHIFT) - 1);
  expected = gst_util_uint64_scale (starttime, FRAMERATE, GST_SECOND);

  fail_unless (framecount == expected || framecount == expected + 1,
      "expected frame count %" G_GUINT64_FORMAT
      " or %" G_GUINT64_FORMAT
      ", but got frame count %" G_GUINT64_FORMAT,
      expected, expected + 1, framecount);
}

GST_START_TEST (test_granulepos_offset)
{
  GstElement *bin;
  GstPad *pad;
  gchar *pipe_str;
  GstBuffer *buffer;
  GError *error = NULL;

  pipe_str = g_strdup_printf ("videotestsrc timestamp-offset=%" G_GUINT64_FORMAT
      " ! video/x-raw-yuv,format=(fourcc)I420,framerate=10/1"
      " ! theoraenc ! fakesink name=fs0", TIMESTAMP_OFFSET);

  bin = gst_parse_launch (pipe_str, &error);
  fail_unless (bin != NULL, "Error parsing pipeline: %s",
      error ? error->message : "(invalid error)");
  g_free (pipe_str);

  /* get the pad */
  {
    GstElement *sink = gst_bin_get_by_name (GST_BIN (bin), "fs0");

    fail_unless (sink != NULL, "Could not get fakesink out of bin");
    pad = gst_element_get_pad (sink, "sink");
    fail_unless (pad != NULL, "Could not get pad out of fakesink");
    gst_object_unref (sink);
  }

  gst_buffer_straw_start_pipeline (bin, pad);

  /* header packets should have timestamp == NONE, granulepos 0, IN_CAPS */
  buffer = gst_buffer_straw_get_buffer (bin, pad);
  check_buffer_timestamp (buffer, GST_CLOCK_TIME_NONE);
  check_buffer_duration (buffer, GST_CLOCK_TIME_NONE);
  check_buffer_granulepos (buffer, 0);
  check_buffer_is_header (buffer, TRUE);
  gst_buffer_unref (buffer);

  buffer = gst_buffer_straw_get_buffer (bin, pad);
  check_buffer_timestamp (buffer, GST_CLOCK_TIME_NONE);
  check_buffer_duration (buffer, GST_CLOCK_TIME_NONE);
  check_buffer_granulepos (buffer, 0);
  check_buffer_is_header (buffer, TRUE);
  gst_buffer_unref (buffer);

  buffer = gst_buffer_straw_get_buffer (bin, pad);
  check_buffer_timestamp (buffer, GST_CLOCK_TIME_NONE);
  check_buffer_duration (buffer, GST_CLOCK_TIME_NONE);
  check_buffer_granulepos (buffer, 0);
  check_buffer_is_header (buffer, TRUE);
  gst_buffer_unref (buffer);

  {
    GstClockTime next_timestamp;
    gint64 last_granulepos;

    /* first buffer should have timestamp of TIMESTAMP_OFFSET, granulepos to
     * match the timestamp of the end of the last sample in the output buffer.
     * Note that one cannot go timestamp->granulepos->timestamp and get the
     * same value due to loss of precision with granulepos. theoraenc does
     * take care to timestamp correctly based on the offset of the input data
     * however, so it does do sub-granulepos timestamping. */
    buffer = gst_buffer_straw_get_buffer (bin, pad);
    last_granulepos = GST_BUFFER_OFFSET_END (buffer);
    check_buffer_timestamp (buffer, TIMESTAMP_OFFSET);
    /* don't really have a good way of checking duration... */
    check_buffer_granulepos_from_starttime (buffer, TIMESTAMP_OFFSET);
    check_buffer_is_header (buffer, FALSE);

    next_timestamp = TIMESTAMP_OFFSET + GST_BUFFER_DURATION (buffer);

    gst_buffer_unref (buffer);

    /* check continuity with the next buffer */
    buffer = gst_buffer_straw_get_buffer (bin, pad);
    check_buffer_timestamp (buffer, next_timestamp);
    check_buffer_duration (buffer,
        gst_util_uint64_scale (GST_BUFFER_OFFSET_END (buffer), GST_SECOND,
            FRAMERATE)
        - gst_util_uint64_scale (last_granulepos, GST_SECOND, FRAMERATE));
    check_buffer_granulepos_from_starttime (buffer, next_timestamp);
    check_buffer_is_header (buffer, FALSE);

    gst_buffer_unref (buffer);
  }

  gst_buffer_straw_stop_pipeline (bin, pad);

  gst_object_unref (pad);
  gst_object_unref (bin);
}

GST_END_TEST;

GST_START_TEST (test_continuity)
{
  GstElement *bin;
  GstPad *pad;
  gchar *pipe_str;
  GstBuffer *buffer;
  GError *error = NULL;

  pipe_str = g_strdup_printf ("videotestsrc"
      " ! video/x-raw-yuv,format=(fourcc)I420,framerate=10/1"
      " ! theoraenc ! fakesink name=fs0");

  bin = gst_parse_launch (pipe_str, &error);
  fail_unless (bin != NULL, "Error parsing pipeline: %s",
      error ? error->message : "(invalid error)");
  g_free (pipe_str);

  /* get the pad */
  {
    GstElement *sink = gst_bin_get_by_name (GST_BIN (bin), "fs0");

    fail_unless (sink != NULL, "Could not get fakesink out of bin");
    pad = gst_element_get_pad (sink, "sink");
    fail_unless (pad != NULL, "Could not get pad out of fakesink");
    gst_object_unref (sink);
  }

  gst_buffer_straw_start_pipeline (bin, pad);

  /* header packets should have timestamp == NONE, granulepos 0 */
  buffer = gst_buffer_straw_get_buffer (bin, pad);
  check_buffer_timestamp (buffer, GST_CLOCK_TIME_NONE);
  check_buffer_duration (buffer, GST_CLOCK_TIME_NONE);
  check_buffer_granulepos (buffer, 0);
  check_buffer_is_header (buffer, TRUE);
  gst_buffer_unref (buffer);

  buffer = gst_buffer_straw_get_buffer (bin, pad);
  check_buffer_timestamp (buffer, GST_CLOCK_TIME_NONE);
  check_buffer_duration (buffer, GST_CLOCK_TIME_NONE);
  check_buffer_granulepos (buffer, 0);
  check_buffer_is_header (buffer, TRUE);
  gst_buffer_unref (buffer);

  buffer = gst_buffer_straw_get_buffer (bin, pad);
  check_buffer_timestamp (buffer, GST_CLOCK_TIME_NONE);
  check_buffer_duration (buffer, GST_CLOCK_TIME_NONE);
  check_buffer_granulepos (buffer, 0);
  check_buffer_is_header (buffer, TRUE);
  gst_buffer_unref (buffer);

  {
    GstClockTime next_timestamp;
    gint64 last_granulepos;

    /* first buffer should have timestamp of TIMESTAMP_OFFSET, granulepos to
     * match the timestamp of the end of the last sample in the output buffer.
     * Note that one cannot go timestamp->granulepos->timestamp and get the
     * same value due to loss of precision with granulepos. theoraenc does
     * take care to timestamp correctly based on the offset of the input data
     * however, so it does do sub-granulepos timestamping. */
    buffer = gst_buffer_straw_get_buffer (bin, pad);
    last_granulepos = GST_BUFFER_OFFSET_END (buffer);
    check_buffer_timestamp (buffer, 0);
    /* plain division because I know the answer is exact */
    check_buffer_duration (buffer, GST_SECOND / 10);
    check_buffer_granulepos (buffer, 1 << GRANULEPOS_SHIFT);
    check_buffer_is_header (buffer, FALSE);

    next_timestamp = GST_BUFFER_DURATION (buffer);

    gst_buffer_unref (buffer);

    /* check continuity with the next buffer */
    buffer = gst_buffer_straw_get_buffer (bin, pad);
    check_buffer_timestamp (buffer, next_timestamp);
    check_buffer_duration (buffer, GST_SECOND / 10);
    check_buffer_granulepos (buffer, (1 << GRANULEPOS_SHIFT) | 1);
    check_buffer_is_header (buffer, FALSE);

    gst_buffer_unref (buffer);
  }

  gst_buffer_straw_stop_pipeline (bin, pad);

  gst_object_unref (pad);
  gst_object_unref (bin);
}

GST_END_TEST;

static gboolean
drop_second_data_buffer (GstPad * droppad, GstBuffer * buffer, gpointer unused)
{
  return !(GST_BUFFER_OFFSET (buffer) == 1);
}

GST_START_TEST (test_discontinuity)
{
  GstElement *bin;
  GstPad *pad, *droppad;
  gchar *pipe_str;
  GstBuffer *buffer;
  GError *error = NULL;
  guint drop_id;

  pipe_str = g_strdup_printf ("videotestsrc"
      " ! video/x-raw-yuv,format=(fourcc)I420,framerate=10/1"
      " ! theoraenc ! fakesink name=fs0");

  bin = gst_parse_launch (pipe_str, &error);
  fail_unless (bin != NULL, "Error parsing pipeline: %s",
      error ? error->message : "(invalid error)");
  g_free (pipe_str);

  /* the plan: same as test_continuity, but dropping a buffer and seeing if
     theoraenc correctly notes the discontinuity */

  /* get the pad to use to drop buffers */
  {
    GstElement *sink = gst_bin_get_by_name (GST_BIN (bin), "theoraenc0");

    fail_unless (sink != NULL, "Could not get theoraenc out of bin");
    droppad = gst_element_get_pad (sink, "sink");
    fail_unless (droppad != NULL, "Could not get pad out of theoraenc");
    gst_object_unref (sink);
  }

  /* get the pad */
  {
    GstElement *sink = gst_bin_get_by_name (GST_BIN (bin), "fs0");

    fail_unless (sink != NULL, "Could not get fakesink out of bin");
    pad = gst_element_get_pad (sink, "sink");
    fail_unless (pad != NULL, "Could not get pad out of fakesink");
    gst_object_unref (sink);
  }

  drop_id = gst_pad_add_buffer_probe (droppad,
      G_CALLBACK (drop_second_data_buffer), NULL);
  gst_buffer_straw_start_pipeline (bin, pad);

  /* header packets should have timestamp == NONE, granulepos 0 */
  buffer = gst_buffer_straw_get_buffer (bin, pad);
  check_buffer_timestamp (buffer, GST_CLOCK_TIME_NONE);
  check_buffer_duration (buffer, GST_CLOCK_TIME_NONE);
  check_buffer_granulepos (buffer, 0);
  check_buffer_is_header (buffer, TRUE);
  gst_buffer_unref (buffer);

  buffer = gst_buffer_straw_get_buffer (bin, pad);
  check_buffer_timestamp (buffer, GST_CLOCK_TIME_NONE);
  check_buffer_duration (buffer, GST_CLOCK_TIME_NONE);
  check_buffer_granulepos (buffer, 0);
  check_buffer_is_header (buffer, TRUE);
  gst_buffer_unref (buffer);

  buffer = gst_buffer_straw_get_buffer (bin, pad);
  check_buffer_timestamp (buffer, GST_CLOCK_TIME_NONE);
  check_buffer_duration (buffer, GST_CLOCK_TIME_NONE);
  check_buffer_granulepos (buffer, 0);
  check_buffer_is_header (buffer, TRUE);
  gst_buffer_unref (buffer);

  {
    buffer = gst_buffer_straw_get_buffer (bin, pad);
    check_buffer_timestamp (buffer, 0);
    /* plain division because I know the answer is exact */
    check_buffer_duration (buffer, GST_SECOND / 10);
    check_buffer_granulepos (buffer, 1 << GRANULEPOS_SHIFT);
    check_buffer_is_header (buffer, FALSE);
    fail_if (GST_BUFFER_IS_DISCONT (buffer), "expected continuous buffer yo");
    gst_buffer_unref (buffer);

    /* check discontinuity with the next buffer */
    buffer = gst_buffer_straw_get_buffer (bin, pad);
    check_buffer_duration (buffer, GST_SECOND / 10);
    /* After a discont, we'll always get a keyframe, so this one should be 
     * 3<<GRANULEPOS_SHIFT */
    check_buffer_granulepos (buffer, 3 << GRANULEPOS_SHIFT);
    check_buffer_is_header (buffer, FALSE);
    fail_unless (GST_BUFFER_IS_DISCONT (buffer),
        "expected discontinuous buffer yo");
    gst_buffer_unref (buffer);

    /* Then the buffer after that should be continuous */
    buffer = gst_buffer_straw_get_buffer (bin, pad);
    fail_if (GST_BUFFER_IS_DISCONT (buffer), "expected continuous buffer yo");
    /* plain division because I know the answer is exact */
    check_buffer_duration (buffer, GST_SECOND / 10);
    check_buffer_granulepos (buffer, (3 << GRANULEPOS_SHIFT) | 1);
    check_buffer_is_header (buffer, FALSE);
    gst_buffer_unref (buffer);
  }

  gst_buffer_straw_stop_pipeline (bin, pad);
  gst_pad_remove_buffer_probe (droppad, drop_id);

  gst_object_unref (droppad);
  gst_object_unref (pad);
  gst_object_unref (bin);
}

GST_END_TEST;

#endif /* #ifndef GST_DISABLE_PARSE */

static Suite *
theoraenc_suite (void)
{
  Suite *s = suite_create ("theoraenc");
  TCase *tc_chain = tcase_create ("general");

  suite_add_tcase (s, tc_chain);

  check_libtheora ();

#ifndef GST_DISABLE_PARSE
  tcase_add_test (tc_chain, test_granulepos_offset);
  tcase_add_test (tc_chain, test_continuity);
  tcase_add_test (tc_chain, test_discontinuity);
#endif

  return s;
}

int
main (int argc, char **argv)
{
  int nf;

  Suite *s = theoraenc_suite ();
  SRunner *sr = srunner_create (s);

  gst_check_init (&argc, &argv);

  srunner_run_all (sr, CK_NORMAL);
  nf = srunner_ntests_failed (sr);
  srunner_free (sr);

  return nf;
}
