// compiled with:
//  g++ -std=c++11 `pkg-config --cflags gstreamer-1.0 gdk-pixbuf-2.0` test_c.cpp -o test `pkg-config --libs gstreamer-1.0 gdk-pixbuf-2.0 gstreamer-app-1.0`
// usage:
//  ./test -i video_file.mpg # TEST IMAGE DATA PROVIDED IN main.cpp MUST BE SAME FORMAT !!!

#define BUG2

#include <stdio.h>
#include <stdlib.h>
#include <string>
#include <memory.h>
#include <vector>
#include <gst/gst.h>
#include <gst/controller/controller.h>	// GstPlugin
#include <gst/app/gstappsink.h>
#include <gdk-pixbuf/gdk-pixbuf.h>
#include <glib.h>

#define APP_NAME	"test"

typedef struct
{
	int									nWidth;
	int									nHeight;
	GdkColorspace				ColorSpace;
	int									nBitsPerChannel;
	int									nChannels;
	gboolean						bHasAlpha;
	int									nRowStride;
	gsize								nByteLength;
	guchar							*pPixels;
} tSImage;

typedef std::vector<tSImage *> tImagesList;

GMainLoop						*g_pMainLoop;
gchar								*g_pszInFile;
GstElement					*g_pPipeline;
GstElement					*g_pElementVideoSink;
tImagesList					g_ImagesList;

// ----------------------------------------------------------------------------
void EatLotsOfMemory(tSImage *pFrame)
// ----------------------------------------------------------------------------
{
	// access to pixel buffers leads to huge memory consumption!
	for(tImagesList::const_iterator iImage=g_ImagesList.begin(); iImage!=g_ImagesList.end(); iImage++)
	{
		unsigned long test = 0;
		for(int nPos=0; nPos < pFrame->nHeight * pFrame->nRowStride; nPos++)
		{
			if(pFrame->pPixels[nPos] == (*iImage)->pPixels[nPos])
			{
				test++;
			}
		}
	}
}

///////////////////////////////////////////////////////////////////////////////

// ----------------------------------------------------------------------------
void OnFrame(tSImage *pFrame)
// ----------------------------------------------------------------------------
{
	gint64 pos;
	if(gst_element_query_position(g_pPipeline, GST_FORMAT_TIME, &pos))
		g_print("Time: %" GST_TIME_FORMAT " \r", GST_TIME_ARGS(pos));

	EatLotsOfMemory(pFrame);
}

// ----------------------------------------------------------------------------
void ParsePixBufMessage(const GValue *val)
// ----------------------------------------------------------------------------
{
	GdkPixbuf *pPixBuf = GDK_PIXBUF(g_value_dup_object(val));

	tSImage Image;
	Image.ColorSpace				= gdk_pixbuf_get_colorspace(pPixBuf);
	Image.nChannels					= gdk_pixbuf_get_n_channels(pPixBuf);
	Image.bHasAlpha					= gdk_pixbuf_get_has_alpha(pPixBuf);
	Image.nBitsPerChannel		= gdk_pixbuf_get_bits_per_sample(pPixBuf);
	Image.nWidth						= gdk_pixbuf_get_width(pPixBuf);
	Image.nHeight						= gdk_pixbuf_get_height(pPixBuf);
	Image.nRowStride				= gdk_pixbuf_get_rowstride(pPixBuf);
	Image.nByteLength				= gdk_pixbuf_get_byte_length(pPixBuf);
	Image.pPixels						= gdk_pixbuf_get_pixels(pPixBuf);

	OnFrame(&Image);

	g_object_unref(pPixBuf);
}

// ----------------------------------------------------------------------------
static gboolean BusCallback(GstBus *bus, GstMessage *msg, gpointer data)
// ----------------------------------------------------------------------------
{
	switch(GST_MESSAGE_TYPE(msg))
	{
		// ------------------------------------------------------------------------
		case GST_MESSAGE_EOS:
			g_print("End of stream\n");
			g_main_loop_quit(g_pMainLoop);
		break;

		// ------------------------------------------------------------------------
		case GST_MESSAGE_ERROR:
		case GST_MESSAGE_WARNING:
		{
			gchar  *debug = NULL;
			GError *error;

			if(GST_MESSAGE_TYPE(msg) == GST_MESSAGE_WARNING)
				gst_message_parse_warning(msg, &error, &debug);
			else
				gst_message_parse_error(msg, &error, &debug);
			if(debug != NULL)
			{
				g_free(debug);

				g_printerr("%s: %s\n", (GST_MESSAGE_TYPE(msg) == GST_MESSAGE_WARNING) ? "Warning" : "Error" ,error->message);
				g_error_free(error);

			}
			if((GST_MESSAGE_TYPE(msg) == GST_MESSAGE_ERROR))
				g_main_loop_quit(g_pMainLoop);
		}
		break;

		// ------------------------------------------------------------------------
		case GST_MESSAGE_ELEMENT:
		{
			if(GST_MESSAGE_SRC(msg) != GST_OBJECT_CAST(g_pElementVideoSink))
				break;
			const GstStructure *pMsgStructure = gst_message_get_structure(msg);
			if(!gst_structure_has_name(pMsgStructure, "preroll-pixbuf") && !gst_structure_has_name (pMsgStructure, "pixbuf"))
				break;

			const GValue *val = gst_structure_get_value(pMsgStructure, "pixbuf");
			if(val != NULL)
				ParsePixBufMessage(val);
		}
		break;

		// ------------------------------------------------------------------------
		default:
//			g_print("msg %u: \"%s\"\n", GST_MESSAGE_TYPE(msg), GST_MESSAGE_TYPE_NAME(msg));
		break;
	}

	return TRUE;
}

// ----------------------------------------------------------------------------
static void on_decoder_pad_added(GstElement *element, GstPad *pPad, gpointer data)
// ----------------------------------------------------------------------------
{
	GstElement *decoder = (GstElement *) data;

	GstCaps *pCaps = gst_pad_get_current_caps(pPad);

	GstStructure *pCapsStructure = gst_caps_get_structure(pCaps, 0);
	if(pCapsStructure != NULL)
	{
		const gchar *pszStructName = gst_structure_get_name(pCapsStructure);
		if(g_str_has_prefix(pszStructName, "video"))
		{
			GstPad *pSinkpad = gst_element_get_static_pad(decoder, "sink");
			if(pSinkpad != NULL)
			{
				GstPadLinkReturn Ret = gst_pad_link(pPad, pSinkpad);
				if(Ret != GST_PAD_LINK_OK)
					g_print("failed to link pad! (%d)", Ret);
				gst_object_unref(pSinkpad);
			}
		}
	}
}

// ----------------------------------------------------------------------------
void AppSinkCallbackEOS(GstAppSink *appsink, gpointer user_data)
// ----------------------------------------------------------------------------
{
}

// ----------------------------------------------------------------------------
GstFlowReturn AppSinkCallbackNewPreroll(GstAppSink *appsink, gpointer user_data)
// ----------------------------------------------------------------------------
{
	return GST_FLOW_OK;
}

// ----------------------------------------------------------------------------
GstFlowReturn AppSinkCallbackNewSample(GstAppSink *appsink, gpointer user_data)
// ----------------------------------------------------------------------------
{
	GstSample *pSample = gst_app_sink_pull_sample(appsink);

	if(pSample != NULL)
	{
		GstCaps *pCaps = gst_sample_get_caps(pSample);
		if(pCaps != NULL)
		{
			GstStructure	*pStructure = gst_caps_get_structure(pCaps, 0);
			if(pStructure != NULL)
			{
				gint nWidth, nHeight, nDepth;
				if(!gst_structure_get_int(pStructure, "width", &nWidth) || !gst_structure_get_int (pStructure, "height", &nHeight) || gst_structure_get_int(pStructure, "depth", &nDepth))
					return GST_FLOW_ERROR;

				GstBuffer *pBuffer = gst_sample_get_buffer(pSample);
				if(pBuffer != NULL)
				{
					GstMapInfo map;
					if(gst_buffer_map(pBuffer, &map, GST_MAP_READ))
					{
						tSImage Image;
						Image.ColorSpace				= GDK_COLORSPACE_RGB;
						Image.nChannels					= 3;
						Image.bHasAlpha					= false;
						Image.nBitsPerChannel		= 8;
						Image.nWidth						= nWidth;
						Image.nHeight						= nHeight;
						Image.nRowStride				= nWidth * Image.nChannels * (Image.nBitsPerChannel	/ 8);
						Image.nByteLength				= 0;
						Image.pPixels						= map.data;

						OnFrame(&Image);

						gst_buffer_unmap(pBuffer, &map);
					}
					else
						g_print("couldn't get map!\n");

					gst_buffer_unref(pBuffer);
				}
				else
					g_print("couldn't get buffer!\n");
			}
			else
				g_print("couldn't get structure!\n");
		}
		else
			g_print("couldn't get caps!\n");
		gst_sample_unref(pSample);
	}
	else
		g_print("couldn't get sample!\n");

	return GST_FLOW_OK;
}

// ----------------------------------------------------------------------------
void Decode(gchar *pszFile)
// ----------------------------------------------------------------------------
{
	g_pMainLoop = g_main_loop_new(NULL, FALSE);

	g_pPipeline													= gst_pipeline_new("pipeline");
	GstBin			*pBin										= GST_BIN(g_pPipeline);

	GstElement	*pElementSource					= gst_element_factory_make("filesrc", "file-source");
	GstElement	*pElementDecoder				= gst_element_factory_make("decodebin", "decoder");
	GstElement	*pElementConvert				= gst_element_factory_make("videoconvert", "convert");	// "autovideoconvert" ?
#ifdef BUG2
	g_pElementVideoSink									= gst_element_factory_make("appsink", "sink");	// test
#else
	g_pElementVideoSink									= gst_element_factory_make("gdkpixbufsink", "pixbufsink");	// "imagefreeze" ? "autovideosink" ?
#endif

	if(pElementSource == NULL || pElementDecoder == NULL || pElementConvert == NULL || g_pElementVideoSink == NULL)
	{
		g_print("Failed to create elements\n");
		return;
	}

	// configure elements
	g_object_set(G_OBJECT(pElementSource),			"location",	pszFile,	NULL);
	g_object_set(G_OBJECT(g_pElementVideoSink),	"qos",			FALSE,		"max-lateness", (gint64) - 1, "sync", false, "async", false, NULL);

#ifdef BUG2	
	// configure elements
	GstCaps *pAppSinkCaps = gst_caps_new_simple("video/x-raw",
																							"format",			G_TYPE_STRING,	"RGB",
																							"width",			G_TYPE_INT,			720,
																							"height",			G_TYPE_INT,			576,
																							NULL );
	gst_app_sink_set_caps(GST_APP_SINK(g_pElementVideoSink), pAppSinkCaps);
	gst_caps_unref(pAppSinkCaps);

	GstAppSinkCallbacks cbs; // Does this need to be kept alive?
	// Set Video Sink callback methods
	cbs.eos					= &AppSinkCallbackEOS;
	cbs.new_preroll	= &AppSinkCallbackNewPreroll;
	cbs.new_sample	= &AppSinkCallbackNewSample;
	gst_app_sink_set_callbacks(GST_APP_SINK(g_pElementVideoSink), &cbs, NULL, NULL);
#endif

	// set up pipeline
	gst_bin_add(pBin, pElementSource);

	// add elements to pipeline
	gst_bin_add_many(pBin, pElementDecoder, pElementConvert, g_pElementVideoSink, NULL);

	// link the elements together
	gboolean bOK;
	bOK = gst_element_link(pElementSource, pElementDecoder);
	bOK &= gst_element_link(pElementConvert, g_pElementVideoSink);
	if(!bOK)
	{
		g_print("Failed to link elements\n");
		return;
	}

	// link decoder's video-stream pad dynamically
	g_signal_connect(pElementDecoder, "pad-added", G_CALLBACK(on_decoder_pad_added), pElementConvert);

	// add message handler
	GstBus *pBus = gst_element_get_bus(g_pPipeline);
	guint bus_watch_id = gst_bus_add_watch(pBus, BusCallback, NULL /* app data */);

	// set the pipeline to "playing" state
	gst_element_set_state(g_pPipeline, GST_STATE_PLAYING);

	// iterate
	g_main_loop_run(g_pMainLoop);

	// out of the main loop, clean up nicely
	gst_element_set_state(g_pPipeline, GST_STATE_NULL);

	gst_object_unref(pBus);
	gst_object_unref(GST_OBJECT(g_pPipeline));
	g_source_remove(bus_watch_id);
	g_main_loop_unref(g_pMainLoop);
}

///////////////////////////////////////////////////////////////////////////////
// main
///////////////////////////////////////////////////////////////////////////////

// ----------------------------------------------------------------------------
bool ParseOptions(int argc, char *argv[])
// ----------------------------------------------------------------------------
{
	GError					*err			= NULL;
	GOptionContext	*ctx;
	GOptionEntry		entries[] =
	{
		{ "input",		'i', G_OPTION_FLAG_FILENAME,	G_OPTION_ARG_FILENAME,				&g_pszInFile,		"input video file",										NULL },
		{ NULL }
	};

	ctx = g_option_context_new("- " APP_NAME);
	g_option_context_add_main_entries(ctx, entries, NULL);
	g_option_context_add_group(ctx, gst_init_get_option_group());
	if(!g_option_context_parse(ctx, &argc, &argv, &err))
	{
		g_print("Failed to initialize: %s\n", err->message);
		g_error_free(err);
		g_option_context_free(ctx);
		return false;
	}

	if(g_pszInFile == NULL)
	{
		g_print("%s", g_option_context_get_help(ctx, TRUE, NULL));
		g_option_context_free(ctx);
		return false;
	}
	g_option_context_free(ctx);

	return true;
}

// ----------------------------------------------------------------------------
int main(int argc, char *argv[])
// ----------------------------------------------------------------------------
{
	gst_init(&argc, &argv);	// must be called here

	if(!ParseOptions(argc, argv))
		return EXIT_FAILURE;

	// !!! test image data - must match video input format !!!
	for(int i=0; i<3; i++)
	{
		tSImage *pImage = new tSImage();
		pImage->nWidth = 720;
		pImage->nHeight = 576;
		pImage->ColorSpace = GDK_COLORSPACE_RGB;
		pImage->nBitsPerChannel = 8;
		pImage->nChannels = 3;
		pImage->bHasAlpha = false;
		pImage->nRowStride = pImage->nWidth * pImage->nChannels;
		pImage->nByteLength = 0;
		pImage->pPixels	= new guchar[pImage->nHeight * pImage->nRowStride];
		memset(pImage->pPixels, (unsigned char) rand(), pImage->nHeight * pImage->nRowStride);

		g_ImagesList.push_back(pImage);
	}

	Decode(g_pszInFile);

	// clear images list
	for(tImagesList::iterator iImage=g_ImagesList.begin(); iImage!=g_ImagesList.end(); iImage++)
	{
		if((*iImage)->pPixels != NULL)
			delete [] (*iImage)->pPixels;
		delete (*iImage);
	}
	g_ImagesList.clear();

	return EXIT_SUCCESS;
}
