/*
 *
 * Se tiene un videomixer con 1 entrada de fondo y 10 más en una capa mas arriba.
 * Cada uno de estos 10 inputs estan encapsulados en un bin.
 *
 * Una vez que la pipa esta en play se agregan 10 videotestsrc dinamicamente
 * y luego se van removiendo de a uno.
 *
 */

#include <gst/gst.h>
#include <glib.h>
#include <unistd.h>

static GstElement *pipeline;
static GstElement *mixer;
GstElement *src, *filter; //last added elements
static GstElement* input; //last added bin

GMutex mutex, mutex2;
GCond mixer_unlinked_cond, srcpad_blocked_cond;
gboolean is_mixer_unlinked = FALSE;
gboolean is_srcpad_blocked = FALSE;


GstElement* get_mixer_input(char *input_name)
{
	src = gst_element_factory_make ("videotestsrc", NULL);
	g_object_set(src, "is-live", 1, NULL);
	g_object_set(src, "pattern", 1, NULL);
	filter = gst_element_factory_make("capsfilter", NULL);
	GstCaps *caps =  gst_caps_from_string("video/x-raw, width=300, height=300, framerate=25/1");
	g_object_set(G_OBJECT(filter), "caps", caps, NULL);
	gst_caps_unref(caps);

	GstElement* output_bin = gst_bin_new(input_name);
	gst_bin_add_many(GST_BIN(output_bin), src, filter, NULL);
	gst_element_link_many(src, filter, NULL);

	GstPad* srcpad = gst_element_get_static_pad(filter, "src");
	GstPad *input_src_pad = gst_ghost_pad_new("src", srcpad);
	gst_element_add_pad(output_bin, input_src_pad);

	gst_object_unref(srcpad);
	return  output_bin;
}


static void add_src(char *input_name, char *sink_name)
{

	GstElement *new_input = get_mixer_input(input_name);
	GstPad *input_pad = gst_element_get_static_pad(new_input, "src");

	GstPad *mpad = gst_element_get_request_pad(mixer, sink_name);
	g_object_set(G_OBJECT(mpad), "zorder", 2, NULL);
	g_object_set(G_OBJECT(mpad), "xpos", 50, NULL);
	g_object_set(G_OBJECT(mpad), "ypos", 50, NULL);
	gst_bin_add(GST_BIN(pipeline), new_input);
	gst_pad_link(input_pad, mpad);

	gst_element_set_state(new_input, GST_STATE_PLAYING);


	gst_object_unref(GST_OBJECT(input_pad));
	gst_object_unref(GST_OBJECT(mpad));

	input = new_input;
}

static void pad_unlinked(GstPad* pad, GstPad *peer, gpointer user_data)
// callback executed when I unlink the bin's src pad and the mixer's sink pad.
// It sends EOS to the mixer's sink pad.
{
	char * pad_name = gst_pad_get_name(pad);
	char * peer_name = gst_pad_get_name(peer);
	g_print("pad %s unlinked \n", pad_name);
	g_print("pad %s unlinked \n", peer_name);

	g_print("sending EOS to the sink pad \n");
	gst_pad_send_event(pad, gst_event_new_eos());

	g_mutex_lock(&mutex);
	is_mixer_unlinked = TRUE;
	g_cond_signal(&mixer_unlinked_cond);
	g_mutex_unlock(&mutex);

	g_free(pad_name);
	g_free(peer_name);
}

static GstPadProbeReturn input_eos_cb (
	GstPad * pad,
	GstPadProbeInfo * info,
	gpointer user_data
)
// probe callback on the bin's src pad. It unlinks the bin and mixer
{
	GstEvent * event = NULL;
	if (info->type & GST_PAD_PROBE_TYPE_EVENT_DOWNSTREAM){
		event = gst_pad_probe_info_get_event(info);
		if (event->type == GST_EVENT_EOS){
			g_print("EOS arrived to the input bin ! \n");
			GstPad * mpad = gst_pad_get_peer(pad);
			g_signal_connect(mpad, "unlinked", (GCallback) pad_unlinked, NULL);
			gst_pad_unlink(pad, mpad);
			gst_object_unref(mpad);

			return GST_PAD_PROBE_DROP;
		}

	}

	return GST_PAD_PROBE_OK;
}


static GstPadProbeReturn src_probe_cb (
	GstPad * pad,
	GstPadProbeInfo * info,
	gpointer user_data
)
// blocking probe on the videotestsrc's src pad
{
	g_mutex_lock(&mutex2);
	is_srcpad_blocked = TRUE;
	g_cond_signal(&srcpad_blocked_cond);
	g_mutex_unlock(&mutex2);

	return GST_PAD_PROBE_OK;
}

static void delete_src(GstElement *input, char *mixer_sink_name)
{
	g_print("delete_src started! \n");
	GstPad *mpad = gst_element_get_static_pad(mixer, mixer_sink_name);
	GstPad *spad = gst_element_get_static_pad(src, "src");
	GstPad *binpad = gst_element_get_static_pad(input, "src");

	gulong src_probe_id = gst_pad_add_probe(
				spad, GST_PAD_PROBE_TYPE_BLOCK_DOWNSTREAM, src_probe_cb, NULL, NULL
			);
	gulong eos_probe_id = gst_pad_add_probe(
				binpad, GST_PAD_PROBE_TYPE_BLOCK | GST_PAD_PROBE_TYPE_EVENT_DOWNSTREAM, input_eos_cb, NULL, NULL
			);

    //waits until the mixer's sink pad is unlinked
	g_mutex_lock(&mutex2);
	while (!is_srcpad_blocked){
		g_cond_wait(&srcpad_blocked_cond, &mutex2);
	}
	g_mutex_unlock(&mutex2);
	is_srcpad_blocked = FALSE;

	// unlink the videotesrsrc and the filter
	g_print("videotestsrc's src pad blocked! \n");
	GstPad *fpad = gst_element_get_static_pad(filter, "sink");
	g_print("unlinkg src from filter \n");
	gst_pad_unlink(spad, fpad);
	gst_object_unref(fpad);

	// sends EOS to the filter
	gst_pad_send_event(fpad, gst_event_new_eos());

	// waits until the EOS event arrived to the bin's src pad.
	g_mutex_lock(&mutex);
	while (!is_mixer_unlinked){
		g_cond_wait(&mixer_unlinked_cond, &mutex);
	}
	g_mutex_unlock(&mutex);
	is_mixer_unlinked = FALSE;

	// sets the bin to NULL STATE and remove it from the pipeline
	g_print("set input bin to STATE_NULL \n");
	gst_element_set_state(input, GST_STATE_NULL);
	g_print("removing input from pipe\n");
	gst_bin_remove(GST_BIN(pipeline), input);

	// releases the request pad
	g_print("releasing request pad.\n");
	gst_element_release_request_pad(mixer, mpad);

	// remove the probes
	gst_pad_remove_probe(spad, src_probe_id);
	gst_pad_remove_probe(binpad, eos_probe_id);

	gst_object_unref(spad);
	gst_object_unref(mpad);
	gst_object_unref(binpad);
	g_print("remove src: DONE\n");
	g_print("------------------------------------\n");
}

static gpointer thread_1 (gpointer user_data)
// Thread that adds an removes an input until the end of the world.
// NOTE: if you pass a new sink_name to every input and you don't call gst_release_request_pad on delete_src funtion, the programs seems to doesn't blow up.
{
	char *input_name;
	char *sink_name = "sink_1";
	int i;
	for (i=1; TRUE; i++){
		sleep(1);
		input_name = g_strdup_printf("input_%i", i);
		add_src(input_name, sink_name);
		sleep(1);
		delete_src(input, sink_name);
		g_free(input_name);
		i = i + 1;
	}

	return NULL;
}

static gboolean bus_cb (GstBus * bus, GstMessage * msg, gpointer user_data)
{
	GMainLoop *loop = (GMainLoop*)user_data;
	switch (GST_MESSAGE_TYPE(msg))
	{
		case GST_MESSAGE_ERROR:
		{
			GError *err = NULL;
			gchar *dbg;
			gst_message_parse_error (msg, &err, &dbg);
			gst_object_default_error (msg->src, err, dbg);
			g_error_free (err);
			g_free (dbg);
			g_main_loop_quit (loop);
			break;
		}
		default:
			break;
	}
	return TRUE;
}


int main (int argc, char **argv)
{
	GMainLoop *loop;
	GstElement *sink;

	gst_init(&argc, &argv);

	/* builds the pipeline */
	pipeline = gst_pipeline_new ("pipeline");
	GstElement *src1 = gst_element_factory_make ("videotestsrc", NULL);
	g_object_set(src1, "is-live", 1, NULL);
    GstElement *filter1 = gst_element_factory_make("capsfilter", NULL);
    GstCaps *caps =  gst_caps_from_string("video/x-raw, width=640, height=480, framerate=25/1");
    g_object_set(G_OBJECT(filter1), "caps", caps, NULL);
    gst_caps_unref(caps);
    GstElement *videoconvert1 = gst_element_factory_make("videoconvert", NULL);

    mixer = gst_element_factory_make("videomixer", NULL);

    GstElement *final_convert = gst_element_factory_make("videoconvert", NULL);
	sink = gst_element_factory_make ("xvimagesink", NULL);

	gst_bin_add_many (GST_BIN (pipeline), src1, filter1, videoconvert1, NULL);
    gst_bin_add_many(GST_BIN(pipeline), mixer, final_convert, sink, NULL);
	gst_element_link_many (src1, filter1, videoconvert1, NULL);
    gst_element_link_many(mixer, final_convert, sink, NULL);

    GstPad* mixer_pad1 = gst_element_get_request_pad(mixer, "sink_0");

    GstPad* bef_mixer_pad1 = gst_element_get_static_pad(videoconvert1, "src");
    gst_pad_link(bef_mixer_pad1, mixer_pad1);

    gst_object_unref(GST_OBJECT(mixer_pad1));
    gst_object_unref(GST_OBJECT(bef_mixer_pad1));

	gst_element_set_state (pipeline, GST_STATE_PLAYING);
	loop = g_main_loop_new (NULL, FALSE);
	gst_bus_add_watch (GST_ELEMENT_BUS (pipeline), bus_cb, loop);
	GThread * thread = g_thread_new("thread_1", (GThreadFunc) thread_1, loop);
	g_main_loop_run (loop);

	/* free resources */
	gst_element_set_state (pipeline, GST_STATE_NULL);
	gst_object_unref (pipeline);
	g_thread_unref(thread);

	return 0;
}
