/* GStreamer
 *
 * unit test for autoconvert element
 * Copyright (C) 2009 Jan Schmidt <thaytan@noraisin.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., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */

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

#include <gst/gst.h>
#include <gst/check/gstcheck.h>
#include <gst/insertbin/gstinsertbin.h>

GstStaticPadTemplate sinkpad_template = GST_STATIC_PAD_TEMPLATE ("sink",        /* the name of the pad */
    GST_PAD_SINK,               /* the direction of the pad */
    GST_PAD_ALWAYS,             /* when this pad will be present */
    GST_STATIC_CAPS (           /* the capabilities of the padtemplate */
        "video/test")
    );

GstStaticPadTemplate srcpad_template = GST_STATIC_PAD_TEMPLATE ("src",  /* the name of the pad */
    GST_PAD_SRC,                /* the direction of the pad */
    GST_PAD_ALWAYS,             /* when this pad will be present */
    GST_STATIC_CAPS (           /* the capabilities of the padtemplate */
        "video/test")
    );

gint cb_count = 0;

GMutex lock;
GCond cond;

GThread *push_thread = NULL;
GThread *streaming_thread = NULL;
gulong block_probe_id = 0;
gboolean is_blocked = FALSE;

static void
success_cb (GstInsertBin * insertbin, GstElement * element, gboolean success,
    gpointer user_data)
{
  fail_unless (g_thread_self () == push_thread);
  fail_unless (success == TRUE);
  fail_unless (GST_IS_ELEMENT (insertbin));
  fail_unless (GST_IS_ELEMENT (element));
  cb_count++;
}

static void
fail_cb (GstInsertBin * insertbin, GstElement * element, gboolean success,
    gpointer user_data)
{
  fail_unless (GST_IS_ELEMENT (insertbin));
  fail_unless (GST_IS_ELEMENT (element));
  fail_unless (success == FALSE);
  cb_count++;
}

/*
 * This is a macro so the line number of any error is more useful
 */
#define push_buffer(srcpad, count)                              \
  {                                                             \
    fail_unless (cb_count == 0);                                \
    gst_pad_push (srcpad, gst_buffer_new ());                   \
    fail_unless (g_list_length (buffers) == 1);                 \
    gst_check_drop_buffers ();                                  \
    fail_unless (cb_count == (count));                          \
    cb_count = 0;                                               \
  }

#define check_reset_cb_count(count)                             \
  {                                                             \
    fail_unless (cb_count == (count));                          \
    cb_count = 0;                                               \
  }

static gpointer
thread_push_buffer (gpointer data)
{
  GstPad *pad = data;

  gst_pad_push (pad, gst_buffer_new ());
  return NULL;
}

static GstPadProbeReturn
got_buffer_block (GstPad * pad, GstPadProbeInfo * info, gpointer data)
{
  g_mutex_lock (&lock);
  is_blocked = TRUE;
  g_cond_broadcast (&cond);
  g_mutex_unlock (&lock);

  return GST_PAD_PROBE_OK;
}

#define block_thread()                                                  \
{                                                                       \
  fail_unless (cb_count == 0);                                          \
  fail_unless (block_probe_id == 0);                                    \
  fail_unless (is_blocked == FALSE);                                    \
  fail_unless (push_thread == NULL);                                    \
  block_probe_id = gst_pad_add_probe (sinkpad,                           \
      GST_PAD_PROBE_TYPE_BLOCK | GST_PAD_PROBE_TYPE_BUFFER,             \
      got_buffer_block, NULL, NULL);                                    \
  push_thread = g_thread_new ("push block", thread_push_buffer, srcpad); \
  fail_unless (push_thread != NULL);                                    \
  g_mutex_lock (&lock);                                                 \
  while (is_blocked == FALSE)                                           \
    g_cond_wait (&cond, &lock);                                         \
  g_mutex_unlock (&lock);                                               \
}

#define unblock_thread()                                                \
{                                                                       \
  fail_unless (cb_count == 0);                                          \
  fail_unless (push_thread != NULL);                                    \
  fail_unless (is_blocked == TRUE);                                     \
  fail_unless (block_probe_id != 0);                                    \
  gst_pad_remove_probe (sinkpad, block_probe_id);                       \
  g_thread_join (push_thread);                                          \
  fail_unless (g_list_length (buffers) == 1);                           \
  gst_check_drop_buffers ();                                            \
  block_probe_id = 0;                                                   \
  push_thread = NULL;                                                   \
  is_blocked = FALSE;                                                   \
}

GST_START_TEST (test_insertbin_simple)
{
  GstElement *insertbin;
  GstElement *elem;
  GstElement *elem2;
  GstElement *elem3;
  GstElement *elem4;
  GstPad *srcpad;
  GstPad *sinkpad;
  GstCaps *caps;

  g_mutex_init (&lock);
  g_cond_init (&cond);

  insertbin = gst_insert_bin_new (NULL);
  fail_unless (insertbin != NULL);
  ASSERT_OBJECT_REFCOUNT (insertbin, insertbin, 1);
  srcpad = gst_check_setup_src_pad (insertbin, &srcpad_template);
  sinkpad = gst_check_setup_sink_pad (insertbin, &sinkpad_template);

  g_assert_nonnull (srcpad);
  g_assert_nonnull (sinkpad);

  ASSERT_CRITICAL (gst_insert_bin_append (GST_INSERT_BIN (insertbin), NULL,
          NULL, NULL));
  ASSERT_CRITICAL (gst_insert_bin_append (GST_INSERT_BIN (insertbin), NULL,
          fail_cb, NULL));
  fail_unless (cb_count == 0);

  /* insertbin is stopped and pads are idle, should be called immediately
   * from this same thread */
  push_thread = g_thread_self ();
  elem = gst_element_factory_make ("identity", NULL);
  gst_insert_bin_append (GST_INSERT_BIN (insertbin), elem, success_cb, NULL);
  check_reset_cb_count (1);

  gst_insert_bin_remove (GST_INSERT_BIN (insertbin), elem, success_cb, NULL);
  check_reset_cb_count (1);

  fail_unless (gst_pad_set_active (srcpad, TRUE));
  fail_unless (gst_pad_set_active (sinkpad, TRUE));
  fail_unless (gst_element_set_state (insertbin,
          GST_STATE_PLAYING) == GST_STATE_CHANGE_SUCCESS);

  caps = gst_caps_new_empty_simple ("video/test");
  gst_check_setup_events (srcpad, insertbin, caps, GST_FORMAT_BYTES);
  gst_caps_unref (caps);

  fail_unless (cb_count == 0);
  fail_unless (buffers == NULL);

  push_thread = g_thread_self ();
  push_buffer (srcpad, 0);

  /* now the pad should be active, the change should come from the
   * 'streaming thread' */
  push_thread = NULL;
  block_thread ();
  elem = gst_element_factory_make ("identity", NULL);
  gst_insert_bin_prepend (GST_INSERT_BIN (insertbin), elem, success_cb, NULL);
  unblock_thread ();
  check_reset_cb_count (1);

  /* can't add the same element twice */
  block_thread ();
  gst_insert_bin_append (GST_INSERT_BIN (insertbin), elem, fail_cb, NULL);
  check_reset_cb_count (1);
  unblock_thread ();
  push_buffer (srcpad, 0);

  /* remove the element */
  block_thread ();
  gst_insert_bin_remove (GST_INSERT_BIN (insertbin), elem, success_cb, NULL);
  unblock_thread ();
  check_reset_cb_count (1);
  push_buffer (srcpad, 0);

  /* try adding multiple elements, one at a time */
  block_thread ();
  elem = gst_element_factory_make ("identity", NULL);
  gst_insert_bin_append (GST_INSERT_BIN (insertbin), elem, success_cb, NULL);
  unblock_thread ();
  check_reset_cb_count (1);
  push_buffer (srcpad, 0);

  block_thread ();
  elem2 = gst_element_factory_make ("identity", NULL);
  gst_insert_bin_append (GST_INSERT_BIN (insertbin), elem2, success_cb, NULL);
  unblock_thread ();
  check_reset_cb_count (1);
  push_buffer (srcpad, 0);

  block_thread ();
  elem3 = gst_element_factory_make ("identity", NULL);
  gst_insert_bin_append (GST_INSERT_BIN (insertbin), elem3, success_cb, NULL);
  unblock_thread ();
  check_reset_cb_count (1);
  push_buffer (srcpad, 0);

  block_thread ();
  elem4 = gst_element_factory_make ("identity", NULL);
  gst_insert_bin_prepend (GST_INSERT_BIN (insertbin), elem4, success_cb, NULL);
  unblock_thread ();
  check_reset_cb_count (1);
  push_buffer (srcpad, 0);

  /* remove 2 of those elements at once */
  block_thread ();
  gst_insert_bin_remove (GST_INSERT_BIN (insertbin), elem3, success_cb, NULL);
  gst_insert_bin_remove (GST_INSERT_BIN (insertbin), elem2, success_cb, NULL);
  unblock_thread ();
  check_reset_cb_count (2);
  push_buffer (srcpad, 0);

  /* add another 2 elements at once */
  block_thread ();
  elem2 = gst_element_factory_make ("identity", NULL);
  elem3 = gst_element_factory_make ("identity", NULL);
  gst_insert_bin_insert_after (GST_INSERT_BIN (insertbin), elem2, elem,
      success_cb, NULL);
  gst_insert_bin_insert_before (GST_INSERT_BIN (insertbin), elem3, elem4,
      success_cb, NULL);
  unblock_thread ();
  check_reset_cb_count (2);
  push_buffer (srcpad, 0);

  /* remove 2 elements */
  block_thread ();
  gst_insert_bin_remove (GST_INSERT_BIN (insertbin), elem3, success_cb, NULL);
  gst_insert_bin_remove (GST_INSERT_BIN (insertbin), elem2, success_cb, NULL);
  unblock_thread ();
  check_reset_cb_count (2);
  push_buffer (srcpad, 0);

  /* and add again */
  block_thread ();
  elem2 = gst_element_factory_make ("identity", NULL);
  elem3 = gst_element_factory_make ("identity", NULL);
  gst_insert_bin_insert_before (GST_INSERT_BIN (insertbin), elem3, elem4,
      success_cb, NULL);
  gst_insert_bin_insert_after (GST_INSERT_BIN (insertbin), elem2, elem,
      success_cb, NULL);
  unblock_thread ();
  check_reset_cb_count (2);
  push_buffer (srcpad, 0);

  /* try to add an element that has no pads */
  block_thread ();
  elem = gst_bin_new (NULL);
  gst_insert_bin_append (GST_INSERT_BIN (insertbin), elem, fail_cb, NULL);
  check_reset_cb_count (1);
  unblock_thread ();

  /* try to add an element that has a parent */
  block_thread ();
  elem = gst_bin_new (NULL);
  elem2 = gst_element_factory_make ("identity", NULL);
  gst_bin_add (GST_BIN (elem), elem2);
  gst_insert_bin_append (GST_INSERT_BIN (insertbin), elem2, fail_cb, NULL);
  check_reset_cb_count (1);
  gst_insert_bin_remove (GST_INSERT_BIN (insertbin), elem2, fail_cb, NULL);
  check_reset_cb_count (1);
  unblock_thread ();
  gst_object_unref (elem);
  push_buffer (srcpad, 0);

  /* when removing an element insertbin will look at the pending operations list
   * and check if that element is pending and remove it before adding.
   * So we check that the callback count happened before the end, and it
   * also happens from this same main thread. So we need to store the
   * streaming thread to restore it after the check */
  elem = gst_element_factory_make ("identity", NULL);
  elem2 = gst_element_factory_make ("identity", NULL);
  block_thread ();
  gst_insert_bin_append (GST_INSERT_BIN (insertbin), elem, success_cb, NULL);
  gst_insert_bin_append (GST_INSERT_BIN (insertbin), elem2, success_cb, NULL);
  streaming_thread = push_thread;
  push_thread = g_thread_self ();
  gst_insert_bin_remove (GST_INSERT_BIN (insertbin), elem2, success_cb, NULL);
  push_thread = streaming_thread;
  check_reset_cb_count (2);
  unblock_thread ();
  check_reset_cb_count (1);
  push_buffer (srcpad, 0);

  /* fail when trying to add an element before another that isn't in
   * insertbin */
  block_thread ();
  elem = gst_element_factory_make ("identity", NULL);
  elem2 = gst_element_factory_make ("identity", NULL);
  gst_insert_bin_insert_before (GST_INSERT_BIN (insertbin), elem, elem2,
      fail_cb, NULL);
  check_reset_cb_count (1);
  unblock_thread ();
  push_buffer (srcpad, 0);
  gst_object_unref (elem2);

  fail_unless (gst_element_set_state (insertbin,
          GST_STATE_NULL) == GST_STATE_CHANGE_SUCCESS);
  gst_pad_set_active (srcpad, FALSE);
  gst_pad_set_active (sinkpad, FALSE);

  cb_count = 0;
  push_thread = g_thread_self ();
  elem = gst_element_factory_make ("identity", NULL);
  gst_insert_bin_remove (GST_INSERT_BIN (insertbin), elem, fail_cb, NULL);
  check_reset_cb_count (1);

  gst_insert_bin_append (GST_INSERT_BIN (insertbin), elem, success_cb, NULL);
  check_reset_cb_count (1);

  gst_check_teardown_sink_pad (insertbin);
  gst_check_teardown_src_pad (insertbin);
  gst_check_teardown_element (insertbin);

  fail_unless (cb_count == 0);

  g_mutex_clear (&lock);
  g_cond_clear (&cond);
}

GST_END_TEST;


static Suite *
insert_bin_suite (void)
{
  Suite *s = suite_create ("insertbin");
  TCase *tc_basic = tcase_create ("general");

  suite_add_tcase (s, tc_basic);
  tcase_add_test (tc_basic, test_insertbin_simple);

  return s;
}


GST_CHECK_MAIN (insert_bin);
