#include "gstreamerrecorder.h"

using namespace TT::Modules::Utilities::Video;
using namespace TT::Exceptions;

//library objects
#include "gst\gst.h"
#include "gst\gsttoc.h"
#include "gst\app\gstappsrc.h"

void add_tag_test(GstElement* container)
{
	GstTagSetter *tagsetter = GST_TAG_SETTER(container);
	const GstTagList *application_tags = gst_tag_setter_get_tag_list(tagsetter);
	gst_tag_setter_add_tags(tagsetter, GST_TAG_MERGE_APPEND, GST_TAG_TITLE, "comment", NULL);
	gst_tag_setter_add_tags(tagsetter, GST_TAG_MERGE_APPEND, GST_TAG_ARTIST, "comment", NULL);
	gst_tag_setter_add_tags(tagsetter, GST_TAG_MERGE_APPEND, GST_TAG_ALBUM, "comment", NULL);
	gst_tag_setter_add_tags(tagsetter, GST_TAG_MERGE_APPEND, GST_TAG_COMMENT, "comment", NULL);
	gst_tag_setter_add_tags(tagsetter, GST_TAG_MERGE_APPEND, GST_TAG_BITRATE, "comment", NULL);
	gst_tag_setter_add_tags(tagsetter, GST_TAG_MERGE_APPEND, GST_TAG_ENCODER, "comment", NULL);
	gst_tag_setter_add_tags(tagsetter, GST_TAG_MERGE_APPEND, GST_TAG_DATE, "comment", NULL);
	gst_tag_setter_add_tags(tagsetter, GST_TAG_MERGE_APPEND, GST_TAG_ISRC, "comment", NULL);
	gst_tag_setter_add_tags(tagsetter, GST_TAG_MERGE_APPEND, GST_TAG_COPYRIGHT, "comment", NULL);
	gst_tag_setter_add_tags(tagsetter, GST_TAG_MERGE_APPEND, GST_TAG_BEATS_PER_MINUTE, "comment", NULL);
	gst_tag_setter_add_tags(tagsetter, GST_TAG_MERGE_APPEND, GST_TAG_LICENSE, "comment", NULL);
	gst_tag_setter_add_tags(tagsetter, GST_TAG_MERGE_APPEND, GST_TAG_COMPOSER, "comment", NULL);
	gst_tag_setter_add_tags(tagsetter, GST_TAG_MERGE_APPEND, GST_TAG_PERFORMER, "comment", NULL);
	gst_tag_setter_add_tags(tagsetter, GST_TAG_MERGE_APPEND, GST_TAG_GENRE, "comment", NULL);

	/*
	GST_TAG_TITLE
	GST_TAG_ARTIST
	GST_TAG_ALBUM
	GST_TAG_COMMENT
	GST_TAG_BITRATE
	GST_TAG_BITRATE
	GST_TAG_ENCODER
	GST_TAG_DATE
	GST_TAG_ISRC
	GST_TAG_COPYRIGHT
	GST_TAG_BEATS_PER_MINUTE
	GST_TAG_LICENSE
	GST_TAG_COMPOSER
	GST_TAG_PERFORMER
	GST_TAG_GENRE
	*/
}

void add_toc_test(GstElement* pipeline, GstElement* container)
{
/*
On Fri, 2016-10-28 at 13:26 +0000, Michael Yarochkin wrote:

Hi Michael,

not sure what you mean with "Segment" exactly.

Re. TOCs - even if they work, I would expect the code to expect them to be there right from the beginning instead of just at the end. That could possibly be fixed, but it might be a bit of work.

Regards
-Tim
*/

	GstToc*	toc = gst_toc_new(GST_TOC_SCOPE_GLOBAL);

	GstTocEntry* entry = gst_toc_entry_new(GST_TOC_ENTRY_TYPE_CHAPTER, "entry");

	gst_toc_append_entry(toc, entry);

	GstTocEntry* subentry = gst_toc_entry_new(GST_TOC_ENTRY_TYPE_CHAPTER, "subentry");

	gst_toc_entry_append_sub_entry(entry, subentry);

	GstEvent* event = gst_event_new_toc(toc, true);

	gst_toc_setter_set_toc((GstTocSetter*)container, toc);

	GstToc* tocResult = gst_toc_setter_get_toc((GstTocSetter*)container);

	gst_element_send_event(pipeline, event);
}


GStreamerRecorder::GStreamerRecorder()
{
	//initialization
	Initialziation();
}

GStreamerRecorder::~GStreamerRecorder()
{
	StopVideoFile();
}

void GStreamerRecorder::Initialziation()
{
	//init gstreamer library
	gst_init(nullptr, nullptr);

	_loop = g_main_loop_new(NULL, FALSE);
}

#pragma region record video
void GStreamerRecorder::StartVideoFile(QString name, int width, int height, int fps)
{
	//store parameters
	_width = width;
	_height = height;
	_fps = fps;
	_videoFrameSize = _width * _height * 3;
	_frameDuration = 1000 / _fps * 1000000;
	_sectionID = 1;

	//make pipeline
	_pipeline = MakeRecordingPipeline(name.toStdString().data());

	//register call back function for soft stop
	bus = gst_pipeline_get_bus(GST_PIPELINE(_pipeline));
	gst_bus_add_watch(bus, BusCallback, _loop);

	//prepare video buffer
	_videoBuffer = MakeBuffer(_videoFrameSize, -_frameDuration, _frameDuration);

	//prepare time buffer
	_titleBuffer = MakeBuffer(0, -_frameDuration, _frameDuration);

	//prepare time buffer
	_timeBuffer = MakeBuffer(_timeSize, -_frameDuration, _frameDuration);

	//prepare timing buffer
	_timingBuffer = MakeBuffer(_timingSize, -_frameDuration, _frameDuration);

	//prepare timing buffer
	_contentBuffer = MakeBuffer(0, -_frameDuration, _frameDuration);

	//add_tag_test(_container);

	//start recording
	GstStateChangeReturn status;
	status = gst_element_set_state(_pipeline, GST_STATE_PLAYING);
	//status = gst_element_set_state(_pipeline, GST_STATE_PAUSED);
	if (status == GST_STATE_CHANGE_FAILURE)
		throw ErrorHandler(this, ErrorCodes::CannotStartPipeline);
}

void GStreamerRecorder::AttachVideoFrame(char* frame, unsigned long long time)
{
	//validate environment
	if (_pipeline == nullptr) return;

	//shift position to frame for recording
	GST_BUFFER_PTS(_videoBuffer) += GST_BUFFER_DURATION(_videoBuffer);
	GST_BUFFER_PTS(_titleBuffer) += GST_BUFFER_DURATION(_titleBuffer);
	GST_BUFFER_PTS(_timeBuffer) += GST_BUFFER_DURATION(_timeBuffer);
	GST_BUFFER_PTS(_timingBuffer) += GST_BUFFER_DURATION(_timingBuffer);
	GST_BUFFER_PTS(_contentBuffer) += GST_BUFFER_DURATION(_contentBuffer);

	//push video buffer to file
	PushBuffer(_videoSource, _videoBuffer, frame, _videoFrameSize);

	//push empty title buffer to file
	PushBuffer(_titleSource, _titleBuffer, 0, 0);

	//push time buffer to file: 
	//warning: this convertion take time
	_timeCovertor.setMSecsSinceEpoch(time);
	PushBuffer(_timeSource, _timeBuffer, _timeCovertor.toString("dd/MM/yyyy HH:mm:ss.zzz").toStdString().c_str(), _timeSize);

	//convert time to hex
	unsigned long long shift;
	unsigned long long base = 0xFF;
	unsigned long long mask;
	unsigned char timeByte;
	int position = 15;

	for (int i = 0; i != 8; i++)
	{
		shift = 8 * i;
		mask = base << shift;
		timeByte = (time & mask) >> shift;

		_timeChars[position] = _hexChars[timeByte & 0x0F];
		position--;

		_timeChars[position] = _hexChars[(timeByte & 0xF0) >> 4];
		position--;
	}

	//push timing buffer to file
	PushBuffer(_timingSource, _timingBuffer, _timeChars, _timingSize);

	//get section string
	if (_sectionStringSize == 0)
		GenerateSectiontext(time);

	//push content buffer to file
	PushBuffer(_contentSource, _contentBuffer, _sectionString, _sectionStringSize);
}

void GStreamerRecorder::StartNewSection()
{
	_sectionID++;

	_sectionStringSize = 0;

	delete _sectionString;
}

void GStreamerRecorder::StopVideoFile()
{
	//validate environment
	if (_pipeline == nullptr) return;

	//send soft stop signal
	gst_element_send_event(_pipeline, gst_event_new_eos());

	//activate main loop in blocking way
	StartWaitgLoop();

	//stop recording
	gst_element_set_state(_pipeline, GST_STATE_NULL);

	_pipeline = nullptr;
}
#pragma endregion

#pragma region pipeline construction functions
GstElement* GStreamerRecorder::MakeRecordingPipeline(const char* path)
{
	GstElement* result = MakePipeline("VideoRecorder");

#pragma region pipeline elements
	GstElement* videoFormaterRaw;
	GstElement* videoConverter;
	GstElement* videoFormaterImage;
	GstElement* videoEncoder;

	GstElement* titleFilter;

	GstElement* timeFilter;

	GstElement* timingFilter;

	GstElement* contentFilter;

	GstElement* recorder;
#pragma endregion

#pragma region video elements
	//init video source
	_videoSource = MakeSourceElement("appsrc", "VideoFrame");//appsrc videotestsrc
	
	videoFormaterRaw = MakeElement("capsfilter", "VideoFormatRaw", MakeVideoCapRaw());

	videoConverter = MakeElement("videoconvert", "VideoConvertor");

	videoFormaterImage = MakeElement("capsfilter", "VideoFormatImage", MakeVideoCapImage());

	//init video converter
	videoEncoder = MakeElement("jpegenc", "VideoEncoder");

	//set quality
//	g_object_set(videoEncoder, "quality", 20, NULL);
#pragma endregion

#pragma region titles
	//init subtitle source
	_titleSource = MakeSourceElement("appsrc", "RaceTitles");

	//init video filter
	titleFilter = MakeElement("capsfilter", "TitlesFilter", MakeSubtitleCap());
#pragma endregion

#pragma region sublitles for time
	//init timing source
	_timeSource = MakeSourceElement("appsrc", "Time");

	//init video filter
	timeFilter = MakeElement("capsfilter", "TimeFilter", MakeSubtitleCap());
#pragma endregion

#pragma region sublitles for timing
	//init timing source
	_timingSource = MakeSourceElement("appsrc", "Timing");

	//init video filter
	timingFilter = MakeElement("capsfilter", "TimingFilter", MakeSubtitleCap());
#pragma endregion

#pragma region sublitles for content
	//init timing source
	_contentSource = MakeSourceElement("appsrc", "Content");

	//init video filter
	contentFilter = MakeElement("capsfilter", "ContentFilter", MakeSubtitleCap());
#pragma endregion

#pragma region common elements
	//init video container
	_container = MakeElement("matroskamux", "Container"); //matroskamux avimux
	//min-index-interval
														  
	//init file recording
	recorder = MakeElement("filesink", "Recorder");

	//set file name
	g_object_set(recorder, "location", path, NULL);
#pragma endregion

#pragma region pipeline construction
	//build the pipeline
	gst_bin_add_many(GST_BIN(result),
		_videoSource, videoFormaterRaw, videoConverter, videoFormaterImage, videoEncoder,
		_titleSource, titleFilter,
		_timeSource, timeFilter,
		_timingSource, timingFilter,
		_contentSource, contentFilter,
		_container, recorder,
		NULL
	);

	//link video elemets
	if ((bool)gst_element_link_many(_videoSource, videoFormaterRaw, videoConverter, videoFormaterImage, videoEncoder, _container, NULL) != true)
		throw ErrorHandler(this, ErrorCodes::CannotLinkPipelineElements, "Video"); 

	//link titles elemets
	if ((bool)gst_element_link_many(_titleSource, titleFilter, _container, NULL) != true)
		throw ErrorHandler(this, ErrorCodes::CannotLinkPipelineElements, "Subtitles"); 

	//link time elemets
	if ((bool)gst_element_link_many(_timeSource, timeFilter, _container, NULL) != true)
		throw ErrorHandler(this, ErrorCodes::CannotLinkPipelineElements, "Time"); 

	//link timing elemets
	if ((bool)gst_element_link_many(_timingSource, timingFilter, _container, NULL) != true)
		throw ErrorHandler(this, ErrorCodes::CannotLinkPipelineElements, "Timing");

	//link content elemets
	if ((bool)gst_element_link_many(_contentSource, contentFilter, _container, NULL) != true)
		throw ErrorHandler(this, ErrorCodes::CannotLinkPipelineElements, "Content");

	//link common elemets
	if ((bool)gst_element_link(_container, recorder) != true)
		throw ErrorHandler(this, ErrorCodes::CannotLinkPipelineElements, "Storage");
#pragma endregion

	return result;
}
GstElement* GStreamerRecorder::MakePipeline(const char* name)
{
	GstElement* result = gst_pipeline_new(name);
	
	if (!result)
		throw ErrorHandler(this, ErrorCodes::CannotMakePipeline, "Pipeline");

	return result;
}
GstElement* GStreamerRecorder::MakeSourceElement(const char* name, const char* caption)
{
	GstElement* result = MakeElement(name, caption);

	//g_object_set(G_OBJECT(result), "stream-type", GST_APP_STREAM_TYPE_STREAM, "format", GST_FORMAT_TIME, NULL);
	g_object_set(G_OBJECT(result), "format", GST_FORMAT_TIME, NULL);

	return result;
}
GstElement* GStreamerRecorder::MakeElement(const char* name, const char* caption)
{
	return MakeElement(name, caption, nullptr);
}
GstElement* GStreamerRecorder::MakeElement(const char* name, const char* caption, GstCaps* cap)
{
	GstElement* result = gst_element_factory_make(name, caption);

	if (!result)
		throw ErrorHandler(this, ErrorCodes::CannotMakePipelineElement, QString(caption));

	if (cap != nullptr)
		g_object_set(G_OBJECT(result), "caps", cap, NULL);

	return result;
}
GstBuffer* GStreamerRecorder::MakeBuffer(int size, unsigned long long offset, unsigned long long duration)
{
	GstBuffer* result = gst_buffer_new_allocate(NULL, size, NULL);
	GST_BUFFER_PTS(result) = offset;
	GST_BUFFER_DURATION(result) = duration;

	return result;
}
GstCaps* GStreamerRecorder::MakeVideoCapRaw()
{
	return gst_caps_new_simple("video/x-raw",
		"format", G_TYPE_STRING, "RGB",
		"width", G_TYPE_INT, _width,
		"height", G_TYPE_INT, _height,
		"framerate", GST_TYPE_FRACTION, _fps, 1,
		NULL);
}
GstCaps* GStreamerRecorder::MakeVideoCapImage()
{
	return gst_caps_new_simple("video/x-raw",
		"format", G_TYPE_STRING, "I420",
		"width", G_TYPE_INT, _width,
		"height", G_TYPE_INT, _height,
		"framerate", GST_TYPE_FRACTION, _fps, 1,
		NULL);
}
GstCaps* GStreamerRecorder::MakeSubtitleCap()
{
	return gst_caps_new_simple("text/x-raw",//application/x-ssa text/x-raw
//		"format", G_TYPE_STRING, "utf8",
		NULL);
}
#pragma endregion

#pragma region pipeline control functions
void GStreamerRecorder::StartWaitgLoop()
{
	//make time out for waiting on the loop

	//register call back function for bus monitoring
	bus = gst_pipeline_get_bus(GST_PIPELINE(_pipeline));
	gst_bus_add_watch(bus, BusCallback, _loop);

	g_main_loop_run(_loop);
}

int GStreamerRecorder::BusCallback(GstBus* bus, GstMessage* message, void* loop)
{
	int t;

	switch (GST_MESSAGE_TYPE(message))
	{
	case GST_MESSAGE_ERROR:
		break;

	case GST_MESSAGE_EOS:

		g_main_loop_quit((GMainLoop *)loop);

		return false;

		break;

	default:
		t = 0;
		break;
	}

	//return true to be notified next time
	return true;
}

void GStreamerRecorder::PushBuffer(GstElement* destination, GstBuffer* buffer, const char* data, int size)
{
	//work aroung of too fast update of frames in one buffer
	GstBuffer* newBuffer = MakeBuffer(size, GST_BUFFER_PTS(buffer), GST_BUFFER_DURATION(buffer));

	//fill buffer with data
	//gst_buffer_fill(buffer, 0, data, size);
	gst_buffer_fill(newBuffer, 0, data, size);

	//push video buffer to file
	GstFlowReturn ret;
//	g_signal_emit_by_name(destination, "push-buffer", buffer, &ret);
	//ret = gst_app_src_push_buffer((GstAppSrc*)destination, buffer);
	ret = gst_app_src_push_buffer((GstAppSrc*)destination, newBuffer);

	//check for error
	if (ret != GST_FLOW_OK)
		throw ErrorHandler(this, ErrorCodes::CannotPushBuffer);

	//buffer released ONLY when pipeline is stoped. can make problems in memory management
	//gst_buffer_unref(newBuffer);
}

void GStreamerRecorder::GenerateSectiontext(unsigned long long time)
{
	QString result = "";

	//set section id
	result += QString::number(_sectionID) + ";";

	//set section start frame id
	result += QString::number(GST_BUFFER_PTS(_contentBuffer) / _frameDuration) + ";";

	//set section start time
	result += QString::number(time) + ";";

	//copy value
	_sectionStringSize = result.length();
	_sectionString = new char[_sectionStringSize];

	QByteArray resultArray(result.toUtf8());
	for (int i = 0; i != _sectionStringSize; i++)
		_sectionString[i] = resultArray[i];
}
#pragma endregion