#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <cstdint>
using namespace std;
using std::cerr;

#include <Windows.h>
#include <gst/gst.h>
#include <gst/app/gstappsink.h>
#include <gst/video/video.h>
#include <gst/video/gstvideometa.h>

#include <opencv2/opencv.hpp>
using namespace cv;

#define NUM_ALLOCATOR_BUFFERS 2

GST_DEBUG_CATEGORY_STATIC (my_category);
#define GstPlayFlags_Video 0x00000001

GstElement *g_pipeline = NULL;
GstBus *g_bus = NULL;
GstElement *g_sink = NULL;
GMainLoop *g_mainLoop = NULL;
thread g_threadBusWatch;
mutex g_mutexGraph;
condition_variable g_cvGraph;

cv::Mat g_frame;

bool g_waitingForAsync = false;
bool g_playing = false;

GstFlowReturn NewVideoBufferCallback (GstSample *sample, bool isPreroll) {
	cerr << "NewVideoBufferCallback: preroll=" << (isPreroll ? "true" : "false") << ", sample=" << (sample ? "valid" : "null") << endl;
	if (!sample)
		return GST_FLOW_OK;
	// get information about the frame's image format
	int width = 0;
	int height = 0;
	int stride = 0;
	gpointer data = NULL;

	GstBuffer *buffer = gst_sample_get_buffer (sample);
	GstMapInfo mapInfo;

	GstVideoMeta *meta = gst_buffer_get_video_meta (buffer);
	if (meta) {
		stride = meta->stride[0];
		width = meta->width;
		height = meta->height;
		gboolean success = gst_video_meta_map (meta, 0, &mapInfo, &data, &stride, GST_MAP_READ);
	}
	else {
		gst_sample_unref (sample);
		return GST_FLOW_ERROR;
	}

	if (width == 0 || height == 0 || stride == 0) {
		cerr << "Error in NewBufferCallback - width/height/stride cannot be 0" << endl;
		gst_video_meta_unmap (meta, 0, &mapInfo);
		gst_sample_unref (sample);
		return GST_FLOW_ERROR;
	}

	cv::Mat m (height, width, CV_8UC4, (uint8_t *)data, stride);

	gst_video_meta_unmap (meta, 0, &mapInfo);
	gst_sample_unref (sample);

	unique_lock <mutex> lockGraph (g_mutexGraph);

	if (!g_playing) {
		g_playing = true;
		g_cvGraph.notify_all ();
	}

	while (true) {
		if (g_waitingForAsync)
			return GST_FLOW_OK;
		if (g_frame.empty())
			break;
		g_cvGraph.wait (lockGraph);
	}
	g_frame = m.clone();


	g_cvGraph.notify_all ();
	return GST_FLOW_OK;
}

GstFlowReturn staticOnNewVideoPreroll (GstAppSink *sink, gpointer user_data) {
	return NewVideoBufferCallback (gst_app_sink_pull_preroll (sink), true);
}
	
GstFlowReturn staticOnNewVideoBuffer (GstAppSink *sink, gpointer user_data) {
	return NewVideoBufferCallback (gst_app_sink_pull_sample (sink), false);
}

string GstStateToString (GstState state) {
	switch (state) {
		case GST_STATE_PLAYING: return "PLAYING";
		case GST_STATE_PAUSED: return "PAUSED";
		case GST_STATE_READY: return "READY";
		case GST_STATE_NULL: return "NULL";
		case 0: return "(unset)";
		default: return "(UNKNOWN)";
	}
}


gboolean staticOnBusMessage (GstBus *bus, GstMessage *message, gpointer user_data) {

	unique_lock <mutex> lockGraph (g_mutexGraph);
	
	switch (GST_MESSAGE_TYPE (message)) {
		case GST_MESSAGE_ERROR: {
			GError *err; gchar *debug;
			gst_message_parse_error (message, &err, &debug);
			cerr << "Error: " << err->message << endl;
			exit (1);
		}
		case GST_MESSAGE_EOS:
			exit (1);
		case GST_MESSAGE_ASYNC_DONE:
			GST_CAT_INFO (my_category, ">>>>> async complete");
			g_waitingForAsync = false;
			g_cvGraph.notify_all ();
			break;
		case GST_MESSAGE_STATE_CHANGED: {
			GstState oldState, newState, pendingState;
			gst_message_parse_state_changed (message, &oldState, &newState, &pendingState);
			ostringstream oss;
			oss << "Got " << GST_MESSAGE_TYPE_NAME (message) << " message from " << GST_MESSAGE_SRC (message)->name
				<< "  " << GstStateToString (oldState) << " > " << GstStateToString (newState);
			if (pendingState != 0)
				oss << " (" << GstStateToString (pendingState) << " pending";

			GST_CAT_INFO (my_category, ">>>>> %s", oss.str().c_str());
			break;
		}
		default:
			// unhandled message
			GST_CAT_INFO (my_category, "<<<<< Got %s message from %s",
				GST_MESSAGE_TYPE_NAME (message),
				GST_MESSAGE_SRC (message)->name);
			break;
	}
	return true;
}

void ThreadBodyBusWatch () {
	guint busWatchID = gst_bus_add_watch (g_bus, staticOnBusMessage, NULL);
	g_mainLoop = g_main_loop_new (NULL, FALSE);
	g_main_loop_run (g_mainLoop);
	g_main_loop_unref (g_mainLoop);
	g_mainLoop = NULL;
	g_source_remove (busWatchID);
}

bool CreatePipeline () {

	string uri = "file:///c:/Users/Alex/Desktop/SeekAndCropTestVid.mp4";
	g_pipeline = gst_pipeline_new ("pipeline");
	GstElement *playbin = gst_element_factory_make ("playbin", "pb");
	g_object_set (playbin, "uri", uri.c_str(), NULL);
	g_object_set (playbin, "flags", GstPlayFlags_Video, NULL);

	gst_bin_add (GST_BIN(g_pipeline), playbin);

	// disable clocking so the graph runs as fast as possible
//	gst_pipeline_use_clock (GST_PIPELINE (g_pipeline), NULL);

	GstElement *colorspace = gst_element_factory_make ("videoconvert", "colorspace");
	if (!colorspace) {
		cerr << "Couldn't create video converter" << endl;
		gst_object_unref (g_pipeline);
		g_pipeline = NULL;
		return false;
	}

	GstElement *capsFilter = gst_element_factory_make ("capsfilter", "rgbcaps");
	if (!capsFilter) {
		cerr << "Couldn't create rgb caps filter" << endl;
		gst_object_unref (GST_OBJECT (colorspace));
		gst_object_unref (g_pipeline);
		g_pipeline = NULL;
		return false;
	}
	GstCaps *caps = gst_caps_new_empty();
	GstStructure *cs;
	// BRGA: video/x-raw,format=(string)RGBx, bpp=(int)32, depth=(int)32, endianness=(int)4321, red_mask=(int)65280, green_mask=(int)16711680, blue_mask=(int)-16777216, alpha_mask=(int)255
	// BGRx: video/x-raw,format=(string)RGBx, bpp=(int)32, depth=(int)24, endianness=(int)4321, red_mask=(int)65280, green_mask=(int)16711680, blue_mask=(int)-16777216
	cs = gst_structure_new ("video/x-raw", "format", G_TYPE_STRING,"BGRx", NULL);
	gst_caps_append_structure (caps, cs);

	g_object_set (G_OBJECT (capsFilter), "caps", caps, NULL);
	gst_caps_unref (caps);


	g_sink = gst_element_factory_make ("appsink", "myvidsink");
	if (!g_sink) {
		cerr << "Couldn't create video sink" << endl;
		gst_object_unref (GST_OBJECT (capsFilter));
		gst_object_unref (GST_OBJECT (colorspace));
		gst_object_unref (g_pipeline);
		g_pipeline = NULL;
		return false;
	}
//	gst_app_sink_set_max_buffers (GST_APP_SINK (g_sink), NUM_ALLOCATOR_BUFFERS);
//	gst_app_sink_set_drop (GST_APP_SINK (g_sink), false);
//	g_object_set (G_OBJECT(g_sink), "sync", (gboolean) false, NULL);

	// set up appsink callbacks
	GstAppSinkCallbacks callbacks2 = { NULL, staticOnNewVideoPreroll, staticOnNewVideoBuffer,
			NULL, { NULL } };
	gst_app_sink_set_callbacks (GST_APP_SINK (g_sink), &callbacks2, NULL, NULL);

	// Create the sink bin, add the elements and link them
	GstElement *bin = gst_bin_new ("video_sink_bin");
	gst_bin_add_many (GST_BIN (bin), colorspace, capsFilter, g_sink, NULL);
	gst_element_link_many (colorspace, capsFilter, g_sink, NULL);
	GstPad *pad = gst_element_get_static_pad (colorspace, "sink");	// find "sink" pad of the color converter
	GstPad *ghost_pad = gst_ghost_pad_new ("sink", pad);			// create "sink" pad of the bin, connected to "sink" pad of color converter
	gst_pad_set_active (ghost_pad, TRUE);
	gst_element_add_pad (bin, ghost_pad);
	gst_object_unref (pad);

	// Set playbin's video sink to be our sink bin
	g_object_set (GST_OBJECT (playbin), "video-sink", bin, NULL);
		
	g_bus = gst_element_get_bus (g_pipeline);
	g_threadBusWatch = thread (&ThreadBodyBusWatch);

	// Try to enter playing state directly.  We will either:
	//    * fail (we return VideoError)
	//    * succeed immediately
	//    * transition asynchronously (most likely)
	GstStateChangeReturn gscr = gst_element_set_state (g_pipeline, GST_STATE_PLAYING);
	unique_lock <mutex> lockGraph (g_mutexGraph);
	if (gscr == GST_STATE_CHANGE_FAILURE) {
		cerr << "Error transitioning to PLAYING state." << endl;
		exit (1);
		return false;
	}
	else if (gscr == GST_STATE_CHANGE_SUCCESS) {
		cerr << "Successfully transitioned to PLAYING state." << endl;
		g_playing = true;
		return true;
	}
	else if (gscr == GST_STATE_CHANGE_ASYNC) {
		cerr << "Transition to PLAYING state will happen asynchronously." << endl;
		while (!g_playing)
			g_cvGraph.wait (lockGraph);
		return true;
	}
	else {
		// if we get here we must have gotten GST_STATE_CHANGE_NO_PREROLL
		cerr << "Unknown response from gst_element_set_state: " << gscr << endl;
		exit (1);
		return false;
	}
}

cv::Mat GetNextFrame () {
	unique_lock <mutex> lockGraph (g_mutexGraph);
	while (g_frame.empty()) {
		// we wait here until a new frame is available
		g_cvGraph.wait (lockGraph);
	}
	cv::Mat retval = g_frame;
	g_frame = cv::Mat();
	g_cvGraph.notify_all ();
	return retval;
}

bool SeekMs (std::int64_t positionMs) {
	unique_lock <mutex> lockGraph (g_mutexGraph);
	gint64 position = positionMs * 1000000LL;

	g_waitingForAsync = true;
	g_cvGraph.notify_all ();

	// clear pending frames
	g_frame = cv::Mat();

	// Create the seek event
	GstEvent *seek_event = gst_event_new_seek (1, GST_FORMAT_TIME,
		(GstSeekFlags) (GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_ACCURATE),
		GST_SEEK_TYPE_SET, position, GST_SEEK_TYPE_NONE, 0);
		
	lockGraph.unlock ();

	// Send the event
	cerr << "Sending seek event" << endl;
	gboolean success = gst_element_send_event (g_sink, seek_event);
	lockGraph.lock ();
	if (success) {
		while (g_waitingForAsync) {
			cerr << "Waiting for rate change" << endl;
			g_cvGraph.wait (lockGraph);
		}
		return true;
	}
	else {
		cerr << "Unable to process seek/speed command" << endl;
		return false;
	}
}



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

	int m_containerIdx = 0;
	gst_init (NULL, NULL);
	
	GST_DEBUG_CATEGORY_INIT (my_category, "BugExample", 0, "Bug example");

	if (!CreatePipeline ())
		return 1;

	for (int i=0; i<1; i++) {
		cerr << "Getting frame " << i << endl;
		cv::Mat m = GetNextFrame ();
		imshow ("test", m);
		waitKey (1);
	}
	
	for (int i=0; i<20; i++) {
//		Sleep (1000);
		GST_CAT_INFO (my_category, "********************************** Seek %d", i);
		if (!SeekMs (330*(i+1)))
			cerr << "Error seeking!" << endl;
		cv::Mat m = GetNextFrame ();
		imshow ("test", m);
		waitKey (1);
	}
	return 0;
}