//**************************************************************************
//*                     This file is part of the                           *
//*                MMC - Mpxplay Multimedia Commander                      *
//*                   The source code of MMC is                            *
//*        (C) copyright 1998-2018 by PDSoft (Attila Padar)                *
//*                http://mpxplay.sourceforge.net                          *
//*                  email: mpxplay@freemail.hu                            *
//**************************************************************************
//*  This program is distributed in the hope that it will be useful,       *
//*  but WITHOUT ANY WARRANTY; without even the implied warranty of        *
//*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.                  *
//*  Please contact with the author (with me) if you want to use           *
//*  or modify this source.                                                *
//**************************************************************************
//function: FFmpeg and hw video filter handling (deinterlace, video-mixer)

//#define MPXPLAY_USE_DEBUGF 1
#define DISPQT_DEBUG_OUTPUT stdout
#define DISPQT_DEBUG_APPLY stdout
#define DISPQT_DEBUG_ERROR stderr

#include "moc_video_qt.h"
#include "moc_mainwindow.h"

extern "C" {
 extern enum mpxplay_video_player_types mpxplay_config_videoplayer_type;
}

DispQtVideoFrameFilter::DispQtVideoFrameFilter(MainWindow *main_window, QObject *parent, dispqt_videofilter_processdescriptor_s *process_desc, unsigned int nb_procdesc) : QObject(parent)
{
	this->main_window = main_window;
	this->parent_object = parent;
	this->videofilter_process_descriptor = process_desc;
	this->nb_video_filter_process_descriptors = nb_procdesc;
	this->first_selected_process_descriptor = NULL;

#ifdef MPXPLAY_LINK_ORIGINAL_FFMPEG
	this->videofilter_ffmpeg_filternames_collection[0] = 0;
	this->videofilter_ffmpeg_graph = NULL;
	this->video_filter_close();
#endif
}

DispQtVideoFrameFilter::~DispQtVideoFrameFilter()
{
	this->mutex_filter.tryLock(DISPQT_VIDEO_MUTEX_TIMEOUT);
	this->video_filter_close();
	this->mutex_filter.unlock();
}

//------------------------------------------------------------------------------------------------------------------------
// close video filter
int DispQtVideoFrameFilter::video_filter_close(void)
{
	int i;
#ifdef MPXPLAY_LINK_ORIGINAL_FFMPEG
	if(this->videofilter_ffmpeg_graph)
		avfilter_graph_free(&this->videofilter_ffmpeg_graph);
	this->videofilter_ffmpeg_buffersrc_ctx = NULL;
	this->videofilter_ffmpeg_buffersink_ctx = NULL;
	this->videofilter_ffmpeg_graph = NULL;
	this->videofilter_initialized_width = 0;
	this->videofilter_initialized_height = 0;
	this->videofilter_initialized_pixeltype = DISPQT_VIDEOFILTER_INVALID_VALUE;
#endif
	if(!this->nb_video_filter_process_descriptors)
		return -1;
	for(i = 0; i < this->nb_video_filter_process_descriptors; i++)
	{
		struct dispqt_videofilter_processdescriptor_s *process_descr = &this->videofilter_process_descriptor[i];
		struct dispqt_videofilter_elemdescriptor_s *filter_elem = process_descr->elemdesc;
		while(filter_elem && (filter_elem->ffmpeg_option_name || filter_elem->variable_ptr))
		{
			filter_elem->last_configured_variable_value = (filter_elem->variable_ptr)? filter_elem->conf_value_center : DISPQT_VIDEOFILTER_INVALID_VALUE;
			filter_elem++;
		}
	}
	return 0;
}

#ifdef MPXPLAY_LINK_ORIGINAL_FFMPEG
// initialize filter contexts
int DispQtVideoFrameFilter::video_filter_init(int width, int height, int pixeltype_in, int pixeltype_out, AVFrame *inframe)
{
	int ret = AVERROR(ENOMEM);
	char args[512]="";

	this->video_filter_close();

	AVFilter *buffersrc = NULL, *buffersink = NULL;
	AVFilterInOut *inputs  = avfilter_inout_alloc();
	AVFilterInOut *outputs = avfilter_inout_alloc();
	enum AVPixelFormat pix_fmts[] = { (AVPixelFormat)pixeltype_out, AV_PIX_FMT_NONE };

	this->videofilter_ffmpeg_graph = avfilter_graph_alloc();
	if (!outputs || !inputs || !this->videofilter_ffmpeg_graph)
	{
		goto end;
	}

	if(this->first_selected_process_descriptor && (this->first_selected_process_descriptor->process_flags & DISPQT_VIDEOFILTER_PROCESSFLAG_AUDIOSRC))
	{
		char ch_layout[64]="";
		av_get_channel_layout_string(ch_layout, sizeof(ch_layout), 0, inframe->channel_layout);
		snprintf(args, sizeof(args), "channel_layout=%s:sample_fmt=%s:time_base=%d/%d:sample_rate=%d", ch_layout, av_get_sample_fmt_name((AVSampleFormat)inframe->format),
				1, 1, inframe->sample_rate);
//				1, inframe->sample_rate, inframe->sample_rate);
		mpxplay_debugf(DISPQT_DEBUG_OUTPUT, "video_filter_init AUDIO %s", args);
		buffersrc = avfilter_get_by_name("abuffer");
	}
	else
	{
		snprintf(args, sizeof(args), "video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d", width, height, pixeltype_in, 1, 1, 1, 1);
		buffersrc = avfilter_get_by_name("buffer");
	}
	ret = avfilter_graph_create_filter(&this->videofilter_ffmpeg_buffersrc_ctx, buffersrc, "in", args, NULL, this->videofilter_ffmpeg_graph);
	if(ret < 0)
	{
		mpxplay_debugf(DISPQT_DEBUG_ERROR, "Cannot avfilter_graph_create_filter (err:%d) for %s if:%d", ret, this->videofilter_ffmpeg_filternames_collection, inframe->format);
		goto end;
	}

	// buffer video sink: to terminate the filter chain
	buffersink = avfilter_get_by_name("buffersink");
	ret = avfilter_graph_create_filter(&this->videofilter_ffmpeg_buffersink_ctx, buffersink, "out", NULL, NULL, this->videofilter_ffmpeg_graph);
	if(ret < 0)
	{
		mpxplay_debugf(DISPQT_DEBUG_ERROR, "Cannot create buffer sink for %s", this->videofilter_ffmpeg_filternames_collection);
		goto end;
	}

	ret = av_opt_set_int_list(this->videofilter_ffmpeg_buffersink_ctx, "pix_fmts", pix_fmts, AV_PIX_FMT_NONE, AV_OPT_SEARCH_CHILDREN);
	if(ret < 0)
	{
		mpxplay_debugf(DISPQT_DEBUG_ERROR, "Cannot set output pixel format for %s", this->videofilter_ffmpeg_filternames_collection);
		goto end;
	}

	outputs->name       = av_strdup("in");
	outputs->filter_ctx = this->videofilter_ffmpeg_buffersrc_ctx;
	outputs->pad_idx    = 0;
	outputs->next       = NULL;

	inputs->name       = av_strdup("out");
	inputs->filter_ctx = this->videofilter_ffmpeg_buffersink_ctx;
	inputs->pad_idx    = 0;
	inputs->next       = NULL;

	ret = avfilter_graph_parse_ptr(this->videofilter_ffmpeg_graph, this->videofilter_ffmpeg_filternames_collection, &inputs, &outputs, NULL);
	if(ret < 0)
	{
		mpxplay_debugf(DISPQT_DEBUG_ERROR, "avfilter_graph_parse_ptr failed for %s err:%d", this->videofilter_ffmpeg_filternames_collection, ret);
		goto end;
	}

	ret = avfilter_graph_config(this->videofilter_ffmpeg_graph, NULL);

end:
	avfilter_inout_free(&inputs);
	avfilter_inout_free(&outputs);
	if(ret >= 0)
	{
		this->videofilter_initialized_width = width;
		this->videofilter_initialized_height = height;
		this->videofilter_initialized_pixeltype = pixeltype_in;
		//if(this->videofilter_process_descriptor[0].process_type != DISPQT_VIDEOFRAMEFILTER_PROCESSTYPE_DECODER)
		{
			mpxplay_debugf(DISPQT_DEBUG_OUTPUT, "video_filter_init f:%s w:%d h:%d fmt_in:%d fmt_out:%d", this->videofilter_ffmpeg_filternames_collection, width, height, pixeltype_in, pixeltype_out);
		}
	}
	else
	{
		video_filter_close();
	}

	return ret;
}

// configure filter elems (like eq -> brightness)
int DispQtVideoFrameFilter::video_filter_config(int width, int height, int pixeltype_in, int pixeltype_out, AVFrame *inframe)
{
	int i, retVal = 0;
	char stmp[128];

	if((this->videofilter_initialized_width != width) || (this->videofilter_initialized_height != height) || (this->videofilter_initialized_pixeltype != pixeltype_in))
	{
		retVal = this->video_filter_init(width, height, pixeltype_in, pixeltype_out, inframe);
		if(retVal < 0)
			return retVal;
	}

	for(i = 0; i < this->nb_video_filter_process_descriptors; i++)
	{
		struct dispqt_videofilter_processdescriptor_s *process_descr = &this->videofilter_process_descriptor[i];
		dispqt_videofilter_elemdescriptor_s *filter_elem = process_descr->elemdesc;
		while(filter_elem && (filter_elem->ffmpeg_option_name || filter_elem->variable_ptr))
		{
			int varValue = (filter_elem->variable_ptr)? *(filter_elem->variable_ptr) : filter_elem->conf_value_center; // !!! if there's no variable_ptr, then we use/config the value_center
			if(filter_elem->last_configured_variable_value != varValue) // variable value has changed, send to filter
			{
				if(filter_elem->ffmpeg_option_name)
				{
					if(filter_elem->flags & DISPQT_VIDEOFILTER_ELEMTYPE_BOOLEAN)
					{
						stmp[0] = (varValue == filter_elem->conf_value_scale)? '1' : '0';
						stmp[1] = 0;
					}
					else if(filter_elem->flags & DISPQT_VIDEOFILTER_ELEMTYPE_INTEGER)
					{
						int setval = (varValue + filter_elem->conf_value_offset) / filter_elem->conf_value_scale;
						snprintf(stmp, sizeof(stmp), "%d", setval);
					}
					else if(filter_elem->flags & DISPQT_VIDEOFILTER_ELEMTYPE_FLOAT)
					{
						float setval = ((float)varValue + (float)filter_elem->conf_value_offset) / (float)filter_elem->conf_value_scale;
						snprintf(stmp, sizeof(stmp), "%1.3f", setval);
					}
					retVal = avfilter_graph_send_command(this->videofilter_ffmpeg_graph, process_descr->ffmpeg_filter_name, filter_elem->ffmpeg_option_name, stmp, NULL, 0, 0);
					//if(process_descr->process_type != DISPQT_VIDEOFRAMEFILTER_PROCESSTYPE_DECODER)
					{
						mpxplay_debugf(DISPQT_DEBUG_OUTPUT, "video_filter_config %s r:%d w:%d h:%d fmt:%d %s:%s", process_descr->ffmpeg_filter_name, retVal, width, height, pixeltype_in, filter_elem->ffmpeg_option_name, stmp);
					}
				}
				filter_elem->last_configured_variable_value = varValue;
			}
			filter_elem++;
		}
	}

	return retVal;
}

// check that filter have to be used (any of the elems is not in the center state, eg: brightness != 0), returns AV_PIX_FMT_ value
int DispQtVideoFrameFilter::video_filter_check(AVFrame *frame_in, int frameout_format)
{
	struct dispqt_videofilter_processdescriptor_s *process_descr, *first_descr;
	bool enabled_filters = FALSE;
	int framein_format;
	int pass, i, pos;
	char filtername_collection[256];

	if(!frame_in)
	{
		return AV_PIX_FMT_NONE;
	}
	framein_format = frame_in->format;

	filtername_collection[0] = 0;
	first_descr = NULL;

	pass = 0;
	do
	{
		i = 0;
		pos = 0;
		process_descr = &this->videofilter_process_descriptor[0];
		do
		{
			char filter_name_with_optional_config[128];
			bool enable_filter = false;

			pds_strcpy(filter_name_with_optional_config, (char *)process_descr->ffmpeg_filter_name);

			if((pass > 0) && funcbit_test(process_descr->process_flags, DISPQT_VIDEOFILTER_PROCESSFLAG_DEFAULT))
			{
				enable_filter = true;
			}
			else if(process_descr->process_condition_func)
			{
				if(process_descr->process_condition_func(this->main_window->gui_config, frame_in, &filter_name_with_optional_config[0], sizeof(filter_name_with_optional_config)) == TRUE)
				{
					enable_filter = true;
				}
			}
			else if(process_descr->elemdesc)
			{
				struct dispqt_videofilter_elemdescriptor_s *filter_elem = process_descr->elemdesc;
				while(filter_elem->ffmpeg_option_name || filter_elem->variable_ptr)  // search that any of the elems is enabled (not in the center state)
				{
					if(filter_elem->variable_ptr && (*(filter_elem->variable_ptr) != filter_elem->conf_value_center))
					{
						break;
					}
					filter_elem++;
				}
				if(filter_elem->ffmpeg_option_name || filter_elem->variable_ptr) // a value is not in the center (disabled) state
				{
					enable_filter = true;
				}
			}
			if(enable_filter)
			{
				if(pos > 0) // a filter name is already added to the list, separate it
				{
					pos += pds_strcpy(&filtername_collection[pos], (char *)",");
				}
				pos += pds_strcpy(&filtername_collection[pos], filter_name_with_optional_config);
				if(!funcbit_test(process_descr->process_flags, DISPQT_VIDEOFILTER_PROCESSFLAG_DEPENDANT))
				{
					enabled_filters = TRUE;
				}
				if(!first_descr)
				{
					first_descr = process_descr;
				}
			}
			if(++i >= this->nb_video_filter_process_descriptors)
			{
				break;
			}
			process_descr++;
		}while(TRUE);
	}while(!enabled_filters && (frameout_format != AV_PIX_FMT_NONE) && (frameout_format != framein_format) && (++pass < 2)); // we search for a default filter (pad) in second pass for colorspace conversion

	if(enabled_filters)
	{
		if(frameout_format == AV_PIX_FMT_NONE)
		{
			frameout_format = DISPQT_VIDEO_FILTERPROCESS_AVFORMAT; // !!! default filter output format (instead of input avframe format), if the filter is enabled
		}
	}
	else
	{
		frameout_format = AV_PIX_FMT_NONE;
	}

	if((!enabled_filters && this->videofilter_ffmpeg_graph) || (pds_strcmp(filtername_collection, this->videofilter_ffmpeg_filternames_collection) != 0))
	{
		//if(this->videofilter_process_descriptor[0].process_type != DISPQT_VIDEOFRAMEFILTER_PROCESSTYPE_DECODER)
		{
			mpxplay_debugf(DISPQT_DEBUG_OUTPUT, "video_filter_check -> video_filter_close of:%d \"%s\" -> \"%s\"", frameout_format, this->videofilter_ffmpeg_filternames_collection, filtername_collection);
		}
		this->video_filter_close(); // filter is closed, if it's not used or it's changed
		pds_strcpy(this->videofilter_ffmpeg_filternames_collection, filtername_collection);
	}

	this->first_selected_process_descriptor = first_descr;

	return frameout_format;
}

// apply video filter for AVFrame (decoder-deinterlace or picture-mixer)
int DispQtVideoFrameFilter::video_filter_apply(AVFrame *frame_in, AVFrame **frame_out, int frameout_format)
{
	int retVal = DISPQT_VIDEOFILTER_APPLYRETBIT_NOFILTERAPPLY;
	bool outframe_allocated = false;

	if(!frame_out)
		return retVal;

	this->mutex_filter.tryLock(DISPQT_VIDEO_MUTEX_TIMEOUT);

	if(frame_in)
	{
		frameout_format = video_filter_check(frame_in, frameout_format);
		if(frameout_format == AV_PIX_FMT_NONE)
		{
			goto out_filter;
		}
		if(this->video_filter_config(frame_in->width, frame_in->height, frame_in->format, frameout_format, frame_in) < 0)
		{
			goto out_filter;
		}
	} // else if there's no frame_in, then we try to flush the buffered frames

	if(!this->videofilter_ffmpeg_buffersrc_ctx || !this->videofilter_ffmpeg_buffersink_ctx)
		goto out_filter;

	if(!*frame_out)
	{
		*frame_out = av_frame_alloc();
		if(!*frame_out)
			goto out_filter;
		outframe_allocated = true;
	}

	do{
		int avErr = av_buffersink_get_frame(this->videofilter_ffmpeg_buffersink_ctx, *frame_out);
		if(avErr == 0)
		{
			AVRational time_base = av_buffersink_get_time_base(this->videofilter_ffmpeg_buffersink_ctx);
			if(time_base.num && time_base.den)
			{
				(*frame_out)->pts = (*frame_out)->pts * time_base.num / time_base.den;
			}
			mpxplay_debugf(DISPQT_DEBUG_APPLY, "av_buffersink_get_frame err:%d ret:%d w:%d h:%d fmt:%d sn:%d d:%8.8X pts_in:%lld pts_out:%lld tbase:%d/%d",
					avErr, retVal,
					(*frame_out)->width, (*frame_out)->height, (*frame_out)->format, frame_in->nb_samples, *((unsigned int *)&frame_in->data[0]),
					frame_in->pts, (*frame_out)->pts, time_base.num, time_base.den);
			funcbit_enable(retVal, DISPQT_VIDEOFILTER_APPLYRETBIT_OUTPUTFRAME_PRODUCED);
			break;
		}
		if((avErr != AVERROR(EAGAIN)) && (avErr != AVERROR_EOF))
		{
			retVal = DISPQT_VIDEOFILTER_APPLYRETBIT_NOFILTERAPPLY; // input frame is not processed (reused upper)
			goto err_out_filter;
		}
		if(funcbit_test(retVal, DISPQT_VIDEOFILTER_APPLYRETBIT_INPUTFRAME_PROCESSED) || !frame_in)
		{
			goto err_out_filter;
		}
		if(av_buffersrc_add_frame_flags(this->videofilter_ffmpeg_buffersrc_ctx, frame_in, AV_BUFFERSRC_FLAG_KEEP_REF) < 0)
		{
			goto err_out_filter;
		}
		funcbit_enable(retVal, DISPQT_VIDEOFILTER_APPLYRETBIT_INPUTFRAME_PROCESSED);
	}while(1);

	goto out_filter;

err_out_filter:
	if(outframe_allocated && *frame_out)
		av_frame_free(frame_out);

out_filter:
	this->mutex_filter.unlock();
	return retVal;
}

#endif // MPXPLAY_LINK_ORIGINAL_FFMPEG

//------------------------------------------------------------------------------------------------------------------------
void DispQtVideoFrameFilter::resetVideoFilterValues(int process_type_select)
{
	struct dispqt_videofilter_processdescriptor_s *process_descr = &this->videofilter_process_descriptor[0];
	bool update_surface = false;
	unsigned int i = 0;

	for(i = 0; i < this->nb_video_filter_process_descriptors; i++, process_descr++)
	{
		if(process_descr->process_type == process_type_select)
		{
			struct dispqt_videofilter_elemdescriptor_s *filter_elem = process_descr->elemdesc;
			while(filter_elem && (filter_elem->ffmpeg_option_name || filter_elem->variable_ptr))
			{
				if(this->setVideoFilterInternalValue(filter_elem->unique_id, filter_elem->conf_value_center))
				{
					update_surface = true;
				}
				filter_elem++;
			}
		}
	}

#ifdef MPXPLAY_LINK_ORIGINAL_FFMPEG
	if(update_surface)
	{
//		switch(process_type_select)
		{
//			case DISPQT_VIDEOFRAMEFILTER_PROCESSTYPE_VMIXERSLIDER:
				emit this->main_window->qt_video_player->signal_video_surface_filtered_eq_refresh();
//				break;
//			case DISPQT_VIDEOFRAMEFILTER_PROCESSTYPE_TRANSFORM:
//				emit this->main_window->qt_video_player->signal_video_surface_filtered_trans_refresh();
//				break;
		}
	}
#endif
}

dispqt_videofilter_elemdescriptor_s *DispQtVideoFrameFilter::searchElemByUniqueId(int u_id, int *process_type)
{
	struct dispqt_videofilter_processdescriptor_s *process_descr = &this->videofilter_process_descriptor[0];
	unsigned int i = 0;

	if(process_type)
	{
		*process_type = -1;
	}

	for(i = 0; i < this->nb_video_filter_process_descriptors; i++, process_descr++)
	{
		struct dispqt_videofilter_elemdescriptor_s *filter_elem = process_descr->elemdesc;
		while(filter_elem && (filter_elem->ffmpeg_option_name || filter_elem->variable_ptr))
		{
			if(filter_elem->unique_id == u_id)
			{
				if(process_type)
				{
					*process_type = process_descr->process_type;
				}
				return filter_elem;
			}
			filter_elem++;
		}
	}
	return NULL;
}

int DispQtVideoFrameFilter::getVideoFilterValue(int u_id)
{
	int retVal = DISPQT_VMIXER_VALUE_CENTER;
	dispqt_videofilter_elemdescriptor_s *filter_elem = this->searchElemByUniqueId(u_id, NULL);
	if(filter_elem)
		retVal = *(filter_elem->variable_ptr);
	return retVal;
}

bool DispQtVideoFrameFilter::setVideoFilterInternalValue(int u_id, int value)
{
	dispqt_videofilter_elemdescriptor_s *filter_elem;
	int process_type = -1;
	bool update_surface = false;

	this->mutex_filter.tryLock(DISPQT_VIDEO_MUTEX_TIMEOUT);

	filter_elem = this->searchElemByUniqueId(u_id, &process_type);
	if(filter_elem && (filter_elem->variable_ptr) && (*(filter_elem->variable_ptr) != value))
	{
		*(filter_elem->variable_ptr) = value;
		update_surface = true;
	}

	if(!this->main_window->qt_video_player)
	{
		goto out_setvalue;
	}

	if(process_type == DISPQT_VIDEOFRAMEFILTER_PROCESSTYPE_VMIXERSLIDER)
	{
#ifdef MPXPLAY_LINK_QTMEDIA
		if(mpxplay_config_videoplayer_type == MPXPLAY_VIDEOPLAYERTYPE_QTMEDIA) // and all hw based video output, which have these api
		{
			QVideoWidget *videowidget = (QVideoWidget *)main_window->qt_video_player->video_get_widgetptr();
			if(videowidget)
			{
				bool save_update = update_surface;
				update_surface = false;
				switch(u_id)
				{
					case DISPQT_VIDEO_VMIXERTYPE_BRIGHTNESS: videowidget->setBrightness(value); break;
					case DISPQT_VIDEO_VMIXERTYPE_CONTRAST:   videowidget->setContrast(value); break;
					case DISPQT_VIDEO_VMIXERTYPE_SATURATION: videowidget->setSaturation(value); break;
					case DISPQT_VIDEO_VMIXERTYPE_HUE:        videowidget->setHue(-value); break;  // !!! QtMedia and AVFilter are different
					default: update_surface = save_update; break;
				}
			}
		}
#endif
	}

out_setvalue:
	this->mutex_filter.unlock();

	return update_surface;
}

void DispQtVideoFrameFilter::setVideoFilterValue(int u_id, int value)
{
	bool update_surface = setVideoFilterInternalValue(u_id, value);
#ifdef MPXPLAY_LINK_ORIGINAL_FFMPEG
	if(update_surface)
	{
		// TODO: process_type_select
		emit this->main_window->qt_video_player->signal_video_surface_filtered_eq_refresh();
	}
#endif
}
