#include <gst/gst.h>
#include <gst/gstpipeline.h>
#include <gst/app/gstappsink.h>
#include <cstdlib>
#include <iostream>
#include <memory>
#include <cstring>
#include <vector>

#include <gst/video/video-frame.h>


void cb_new_pad(GstElement* element, GstPad* new_pad, gpointer element_to_link_to)
{
    std::shared_ptr<GstCaps> new_pad_caps(gst_pad_get_current_caps(new_pad), &gst_caps_unref);
    GstStructure *new_pad_struct = NULL;
    std::shared_ptr<GstPad> videoconvert_sink_pad(gst_element_get_static_pad(GST_ELEMENT(element_to_link_to), "sink"), &gst_object_unref);

    const gchar* new_pad_type = NULL;
    GstPadLinkReturn ret;

    if(gst_pad_is_linked(videoconvert_sink_pad.get()))
    {
        return;
    }

    new_pad_struct = gst_caps_get_structure(new_pad_caps.get(), 0);

    /*
     * unsure about this here
     */
    new_pad_type = gst_structure_get_name(new_pad_struct);

    if(!g_str_has_prefix(new_pad_type, "video/x-raw"))
    {
        return;
    }

    ret = gst_pad_link(new_pad, videoconvert_sink_pad.get());

    if(GST_PAD_LINK_FAILED(ret))
    {
        g_print("failed linking pads %s and %s\n", gst_pad_get_name(new_pad), gst_pad_get_name(videoconvert_sink_pad.get()));
    }

}

std::shared_ptr<GstPipeline> getVideoPipeline(std::string _video_file)
{
	std::string pipeline_name     = "pipeline for "; pipeline_name.append(_video_file);
	std::shared_ptr<GstPipeline>  pipeline(reinterpret_cast<GstPipeline*>(gst_pipeline_new(pipeline_name.c_str())), &gst_object_unref);

	std::string filesrc_name      = "filesrc for "; filesrc_name.append(_video_file);
	GstElement* filesrc 		  = gst_element_factory_make("filesrc", filesrc_name.c_str());

	std::string decodebin_name    = "decodebin for "; decodebin_name.append(_video_file);
	GstElement* decodebin         = gst_element_factory_make("decodebin", decodebin_name.c_str());

	std::string videoconvert_name = "videoconvert for "; videoconvert_name.append(_video_file);
	GstElement* videoconvert      = gst_element_factory_make("videoconvert", videoconvert_name.c_str());

	std::string capsfilter_name   = "capsfilter for "; capsfilter_name.append(_video_file);
	GstElement* capsfilter        = gst_element_factory_make("capsfilter", capsfilter_name.c_str());

	std::string appsink_name      = "appsink for "; appsink_name.append(_video_file);
	GstElement* appsink			  = gst_element_factory_make("appsink", appsink_name.c_str());

    if(!pipeline.get())
    {
        g_printerr("could not create pipeline\n");
        return NULL;
    }

    if(!filesrc)
    {
        g_printerr("could not create source\n");
        return NULL;
    }

    if(!decodebin)
    {
        g_printerr("could not create decodebin\n");
        return NULL;
    }

    if(!videoconvert)
    {
        g_printerr("could not create videoconvert\n");
        return NULL;
    }

    if(!capsfilter)
    {
    	g_printerr("could not create capsfilter\n");
    	return NULL;
    }

    if(!appsink)
    {
        g_printerr("could not create appsink\n");
        return NULL;
    }

    g_object_set(filesrc, "location", _video_file.c_str(), NULL);

    gst_bin_add_many(GST_BIN(pipeline.get()), filesrc, decodebin, videoconvert, capsfilter, appsink, NULL);

    if(!gst_element_link(filesrc, decodebin))
    {
    	g_printerr("failed linking %s and %s\n", gst_element_get_name(filesrc), gst_element_get_name(decodebin));
        return NULL;
    }

    gst_element_link_pads(decodebin, "src", videoconvert, "sink");

    g_signal_connect (decodebin, "pad-added", G_CALLBACK (cb_new_pad), videoconvert);


    if(!gst_element_link(videoconvert, capsfilter))
    {
    	g_printerr("failed linking %s and %s\n", gst_element_get_name(videoconvert), gst_element_get_name(capsfilter));
    	return NULL;
    }

    if(!gst_element_link(capsfilter, appsink))
    {
    	g_printerr("failed linking %s and %s\n", gst_element_get_name(capsfilter), gst_element_get_name(appsink));
        return NULL;
    }

	std::shared_ptr<GstCaps> caps(gst_caps_from_string("video/x-raw, format=RGB"), &gst_caps_unref);
	g_object_set(capsfilter, "caps", caps.get(), NULL);

	return pipeline;
}

int main(int argc, char *argv[])
{
    gst_init(&argc, &argv);

    std::shared_ptr<GstPipeline> pipeline = getVideoPipeline("/home/henning/Downloads/70-177.mp4");

    std::shared_ptr<GstAppSink> sink;

    GstIteratorResult itResult;
    GValue elem = G_VALUE_INIT;
    bool done = false;

    //Retrieve sink from pipeline
    auto iterator = std::shared_ptr<GstIterator>(gst_bin_iterate_sinks((GST_BIN(pipeline.get()))), &gst_iterator_free);

    while(!done)
    {
        itResult = gst_iterator_next(iterator.get(), &elem);
        switch(itResult) {
            case GST_ITERATOR_OK: {
                auto gst_elem = std::shared_ptr<GstElement>(static_cast<GstElement*>(g_value_peek_pointer(&elem)), &gst_object_unref);

                //might check if this is actually the desired sink
                //right now we've got only one
                //so leave it
                if (GST_IS_APP_SINK(gst_elem.get())) {
                    sink = std::shared_ptr<GstAppSink>(GST_APP_SINK(gst_elem.get()), &gst_object_unref);
                    done = true;
                }
                break;
            }
            case GST_ITERATOR_ERROR: {
                done = true;
                break;
            }
            case GST_ITERATOR_DONE: {
                done = true;
                break;
            }
            default: {
                break;
            }
        }
    }

    int ret;
    ret = gst_element_set_state(GST_ELEMENT(pipeline.get()), GST_STATE_PLAYING);
    if(ret ==  GST_STATE_CHANGE_FAILURE)
    {
        //TODO: handle error
    }

    while(true)
    {
    	if(gst_app_sink_is_eos(sink.get()))
    	{
    	    if(!gst_element_seek_simple(GST_ELEMENT(pipeline.get()), GST_FORMAT_TIME, (GstSeekFlags)(GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT), 0))
    	    {
    	        //TODO: log this
    	    }
    	}

        GstVideoFrame              video_frame;
        GstVideoInfo               video_info;
        std::shared_ptr<GstCaps>   caps;

        //return if no samples are available
        if(gst_app_sink_is_eos(sink.get()))
        {
        	continue;
        }

        auto sample = std::shared_ptr<GstSample>(gst_app_sink_pull_sample(sink.get()), &gst_sample_unref);
        if(sample.get() == NULL || !GST_IS_SAMPLE(sample.get()))
        {
            //TODO: log this
        	g_printerr("appsink %s failed receiving a sample\n", gst_element_get_name(GST_ELEMENT(sink.get())));
        }

        auto buffer = std::shared_ptr<GstBuffer>(gst_sample_get_buffer(sample.get()), &gst_buffer_unref);
        if(buffer.get() == NULL || !GST_IS_BUFFER(buffer.get()))
        {
            //TODO: log this
            g_printerr("no buffer\n");
        }

        caps = std::shared_ptr<GstCaps>(gst_sample_get_caps(sample.get()), &gst_caps_unref);

        GstStructure *structure = gst_caps_get_structure(caps.get(), 0);

        if(!gst_video_info_from_caps(&video_info, caps.get()))
        {
            //TODO: log this
        }
        if(!gst_video_frame_map(&video_frame, &video_info, buffer.get(), GST_MAP_READ))
        {
            //TODO: log this
        }

        printf("video info format: %s\n", video_info.finfo->name);
        GstVideoMeta *meta = gst_buffer_get_video_meta(buffer.get());
        printf("video frame meta info: %s\n", gst_video_format_to_string(meta->format));

        gst_video_frame_unmap(&video_frame);

    }

    return 0;
}
