#include <gst/gst.h>

typedef struct
{
  GThread * swap_thread;
  GThread * push_thread;
  GThread * pad_alloc_thread;
  GThread * event_thread;

  GstElement * output_selector;

  GstPad * selector_srcpad1;
  GstPad * selector_srcpad2;
  GstPad * selector_sinkpad;

  GstPad * loose_srcpad;
  GstPad * loose_sinkpad1;
  GstPad * loose_sinkpad2;

  GstPad * active_pushpad;
  GstPad * active_allocpad;
  GstPad * active_eventpad;

  gboolean running;
} TestData;

static GstCaps *
pad_getcaps_cb (GstPad * pad)
{
  return gst_caps_new_any ();
}

static gboolean
pad_setcaps_cb (GstPad * pad,
                GstCaps * caps)
{
  return TRUE;
}

static GstFlowReturn
pad_chain_cb (GstPad * pad,
              GstBuffer * buffer)
{
  TestData * data = g_object_get_data (G_OBJECT (pad), "test-data");
  data->active_pushpad = pad;

  gst_buffer_unref (buffer);

  return GST_FLOW_OK;
}

static GstFlowReturn
pad_bufalloc_cb (GstPad *pad,
                 guint64 offset,
                 guint size,
                 GstCaps *caps,
                 GstBuffer **buf)
{
  TestData * data = g_object_get_data (G_OBJECT (pad), "test-data");
  data->active_allocpad = pad;

  *buf = gst_buffer_new_and_alloc (size);
  gst_buffer_set_caps (*buf, caps);

  return GST_FLOW_OK;
}

static gboolean
pad_event_cb (GstPad *pad,
              GstEvent * event)
{
  TestData * data = g_object_get_data (G_OBJECT (pad), "test-data");
  data->active_eventpad = pad;

  gst_event_unref (event);

  return TRUE;
}

static gpointer
push_func (gpointer user_data)
{
  TestData * data = (TestData *)user_data;
  GstBuffer * buf = gst_buffer_new ();
  while (data->running)
  {
    g_assert_cmpint (gst_pad_push (data->loose_srcpad,
      gst_buffer_ref (buf)), ==, GST_FLOW_OK);
    g_thread_yield ();
  }
  gst_buffer_unref (buf);
  return NULL;
}

static gpointer
pad_alloc_func (gpointer user_data)
{
  TestData * data = (TestData *)user_data;
  GstBuffer * buf;
  GstCaps * caps = gst_caps_new_simple ("test",
        "foo", G_TYPE_STRING, "bar",
        NULL);

  while (data->running)
  {
    g_assert_cmpint (gst_pad_alloc_buffer (data->loose_srcpad,
      0, 0, caps, &buf), ==,GST_FLOW_OK);
    gst_buffer_unref (buf);
    g_thread_yield ();
  }
  gst_caps_unref (caps);
  return NULL;
}

static gpointer
event_func (gpointer user_data)
{
  TestData * data = (TestData *)user_data;
  GstEvent * event = gst_event_new_custom (
      GST_EVENT_CUSTOM_DOWNSTREAM_OOB,
      gst_structure_new ("oob-event-coming-through", NULL));
  while (data->running)
  {
    g_assert (gst_pad_send_event (data->selector_sinkpad,
      gst_event_ref (event)));
    g_thread_yield ();
  }

  gst_event_unref (event);
  return NULL;
}

static void
wait_and_confirm_active_pads (TestData * data,
                              GstPad * selector_srcpad)
{
  GstPad * loose_sinkpad = GST_PAD_PEER (selector_srcpad);
  GstPad * first_found_pushpad = NULL;
  GstPad * first_found_allocpad = NULL;
  GstPad * first_found_eventpad = NULL;

  data->active_pushpad = NULL;
  data->active_allocpad = NULL;
  data->active_eventpad = NULL;

  while (first_found_pushpad == NULL  ||
         first_found_allocpad == NULL ||
         first_found_eventpad == NULL)
  {
    if (!data->running)
      return;

    if (first_found_pushpad == NULL && data->active_pushpad)
      first_found_pushpad = data->active_pushpad;

    if (first_found_allocpad == NULL && data->active_allocpad)
      first_found_allocpad = data->active_allocpad;

    if (first_found_eventpad == NULL && data->active_eventpad)
      first_found_eventpad = data->active_eventpad;

    g_thread_yield ();
  }

  g_assert (first_found_pushpad  == loose_sinkpad);
  g_assert (first_found_allocpad == loose_sinkpad);
  g_assert (first_found_eventpad == loose_sinkpad);
}

static gpointer
swap_func (gpointer user_data)
{
  TestData * data = (TestData *)user_data;
  while (data->running)
  {
    data->selector_srcpad2 =
      gst_element_get_request_pad (data->output_selector, "src%d");
    g_assert_cmpint (gst_pad_link (data->selector_srcpad2,
      data->loose_sinkpad2), ==, GST_PAD_LINK_OK);

    g_object_set (data->output_selector,
      "active-pad", data->selector_srcpad2, NULL);
    wait_and_confirm_active_pads (data, data->selector_srcpad2);

    g_assert (gst_pad_unlink (data->selector_srcpad1, data->loose_sinkpad1));
    gst_element_release_request_pad (data->output_selector,
      data->selector_srcpad1);
    gst_object_unref (data->selector_srcpad1);

    g_thread_yield ();

    data->selector_srcpad1 =
      gst_element_get_request_pad (data->output_selector, "src%d");
    g_assert_cmpint (gst_pad_link (data->selector_srcpad1,
      data->loose_sinkpad1), ==, GST_PAD_LINK_OK);

    g_object_set (data->output_selector,
      "active-pad", data->selector_srcpad1, NULL);
    wait_and_confirm_active_pads (data, data->selector_srcpad1);

    g_assert (gst_pad_unlink (data->selector_srcpad2, data->loose_sinkpad2));
    gst_element_release_request_pad (data->output_selector,
      data->selector_srcpad2);
    gst_object_unref (data->selector_srcpad2);

    g_thread_yield ();
  }
  return NULL;
}

static GstPad *
create_loose_sinkpad (const gchar * name, TestData * data)
{
  GstPad * pad = gst_pad_new (name, GST_PAD_SINK);
  g_object_set_data (G_OBJECT (pad), "test-data", data);
  gst_pad_set_getcaps_function     (pad, pad_getcaps_cb);
  gst_pad_set_setcaps_function     (pad, pad_setcaps_cb);
  gst_pad_set_chain_function       (pad, pad_chain_cb);
  gst_pad_set_bufferalloc_function (pad, pad_bufalloc_cb);
  gst_pad_set_event_function       (pad, pad_event_cb);
  gst_pad_set_active               (pad, TRUE);
  return pad;
}

static void
setup (TestData * data)
{
  data->loose_srcpad = gst_pad_new ("src", GST_PAD_SRC);
  gst_pad_set_getcaps_function (data->loose_srcpad, pad_getcaps_cb);
  gst_pad_set_setcaps_function (data->loose_srcpad, pad_setcaps_cb);

  data->loose_sinkpad1 = create_loose_sinkpad ("sink1", data);
  data->loose_sinkpad2 = create_loose_sinkpad ("sink2", data);

  data->output_selector = gst_element_factory_make ("output-selector", NULL);
  g_object_set (data->output_selector,
    "pad-negotiation-mode", 2,
    NULL);
  gst_element_set_state (data->output_selector, GST_STATE_PLAYING);

  data->selector_sinkpad =
    gst_element_get_static_pad (data->output_selector, "sink");
  g_assert_cmpint (gst_pad_link (data->loose_srcpad,
    data->selector_sinkpad), ==, GST_PAD_LINK_OK);

  data->selector_srcpad1 =
    gst_element_get_request_pad (data->output_selector, "src%d");
  g_assert_cmpint (gst_pad_link (data->selector_srcpad1,
    data->loose_sinkpad1), ==, GST_PAD_LINK_OK);

  data->running = TRUE;
  data->swap_thread      = g_thread_create (swap_func,      data, TRUE, NULL);
  data->push_thread      = g_thread_create (push_func,      data, TRUE, NULL);
  data->pad_alloc_thread = g_thread_create (pad_alloc_func, data, TRUE, NULL);
  data->event_thread     = g_thread_create (event_func,     data, TRUE, NULL);
}

static void
teardown (TestData * data)
{
  data->running = FALSE;
  g_thread_join (data->swap_thread);
  g_thread_join (data->push_thread);
  g_thread_join (data->pad_alloc_thread);
  g_thread_join (data->event_thread);

  gst_element_set_state (data->output_selector, GST_STATE_NULL);
  gst_object_unref (data->output_selector);

  gst_object_unref (data->selector_srcpad1);
  gst_object_unref (data->selector_sinkpad);

  gst_object_unref (data->loose_srcpad);
  gst_object_unref (data->loose_sinkpad1);
  gst_object_unref (data->loose_sinkpad2);
}

static void
test_safe_swapping (void)
{
  TestData data;
  setup (&data);

  g_usleep (G_USEC_PER_SEC * 3);

  teardown (&data);
}
