/* === S Y N F I G ========================================================= */
/*!	\file trgt_av.cpp
**	\brief \writeme
**
**	$Id$
**
**	\legal
**	Copyright (c) 2002-2005 Robert B. Quattlebaum Jr., Adrian Bentley
**  Copyright (c) 2008 Paul Wise
**  Copyright (c) 2008 Gerco Ballintijn
**  ......... ... 2018 Ivan Mahonin
**
**	This package is free software; you can redistribute it and/or
**	modify it under the terms of the GNU General Public License as
**	published by the Free Software Foundation; either version 2 of
**	the License, or (at your option) any later version.
**
**	This package 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. See the GNU
**	General Public License for more details.
**	\endlegal
*/
/* ========================================================================= */

/* === H E A D E R S ======================================================= */

#ifdef USING_PCH
#	include "pch.h"
#else
#ifdef HAVE_CONFIG_H
#	include <config.h>
#endif

// ffmpeg library headers have historically had multiple locations.
// We should check all of the locations to be more portable.

extern "C"
{
#ifdef HAVE_LIBAVFORMAT_AVFORMAT_H
#	include <libavformat/avformat.h>
#elif defined(HAVE_AVFORMAT_H)
#	include <avformat.h>
#elif defined(HAVE_FFMPEG_AVFORMAT_H)
#	include <ffmpeg/avformat.h>
#else
#   ifndef DISABLE_MODULE
#   define DISABLE_MODULE
#   endif
#endif

#ifdef HAVE_LIBSWSCALE_SWSCALE_H
#	include <libswscale/swscale.h>
#elif defined(HAVE_SWSCALE_H)
#	include <swscale.h>
#elif defined(HAVE_FFMPEG_SWSCALE_H)
#	include <ffmpeg/swscale.h>
#else
#   ifndef DISABLE_MODULE
#   define DISABLE_MODULE
#   endif
#endif
} // extern "C"

#ifndef DISABLE_MODULE
#	include <cstring>
#	include <algorithm>
#	include <functional>
#	include <synfig/general.h>
#	include <synfig/localization.h>
#	include "trgt_av.h"
#endif

#endif

#ifndef DISABLE_MODULE

/* === U S I N G =========================================================== */

using namespace synfig;
using namespace std;
using namespace etl;

/* === I N F O ============================================================= */

SYNFIG_TARGET_INIT(Target_LibAVCodec);
SYNFIG_TARGET_SET_NAME(Target_LibAVCodec,"libav");
//Use some non-existing extension to disable exporting through this module
//SYNFIG_TARGET_SET_EXT(Target_LibAVCodec,"avi");
SYNFIG_TARGET_SET_EXT(Target_LibAVCodec,"NONEXISTING-EXTENSION");
SYNFIG_TARGET_SET_VERSION(Target_LibAVCodec,"0.2");
SYNFIG_TARGET_SET_CVS_ID(Target_LibAVCodec,"$Id$");

/* === C L A S S E S & S T R U C T S ======================================= */

static bool av_registered = false;

class Target_LibAVCodec::Internal
{
private:
	AVFormatContext *context;
	AVPacket *packet;
	bool file_opened;
	bool headers_sent;

	const AVCodec *video_codec;
	AVStream *video_stream;
	AVCodecContext *video_context;
	AVFrame *video_frame;
	AVFrame *video_frame_rgb;
	SwsContext *video_swscale_context;

	bool add_video_stream(enum AVCodecID codec_id, const RendDesc &desc) {
		// find the video encoder
		video_codec = avcodec_find_encoder(codec_id);
		if (!video_codec) {
			synfig::error("Target_LibAVCodec: video codec not found");
			close();
			return false;
		}

		video_stream = avformat_new_stream(context, video_codec);
		if (!video_stream) {
			synfig::error("Target_LibAVCodec: could not allocate video stream");
			close();
			return false;
		}

		video_context = avcodec_alloc_context3(video_codec);
		if (!video_context) {
			synfig::error("Target_LibAVCodec: could not allocate an encoding video context");
			close();
			return false;
		}

		// set parameters
		int fps = (int)roundf(desc.get_frame_rate());
		video_context->bit_rate     = 400*1024*1024/3600; // 400Mb per hour
		video_context->width        = desc.get_w();       // in most cases resolution must be multiple of two
		video_context->height       = desc.get_h();
		video_context->coded_width  = video_context->width;
		video_context->coded_height = video_context->height;
		video_context->pix_fmt      = AV_PIX_FMT_YUV420P;
		video_context->gop_size     = fps;                // emit one intra frame every second
		video_context->mb_decision  = FF_MB_DECISION_RD;  // use best acroblock decision algorithm
		video_context->framerate    = (AVRational){ fps, 1 };
		video_context->time_base    = (AVRational){ 1, fps };
		video_stream->time_base     = video_context->time_base;

		// some formats want stream headers to be separate.
		if (context->oformat->flags & AVFMT_GLOBALHEADER)
			video_context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;

		return true;
    }

	bool open_video_stream() {
		if (avcodec_open2(video_context, NULL, NULL) < 0) {
			synfig::error("Target_LibAVCodec: could not open video codec");
			// seems the calling of avcodec_free_context after error will cause crash
			// so just forget about this context
			video_context = NULL;
			close();
			return false;
        }

		// allocate frame
		video_frame = av_frame_alloc();
		assert(video_frame);
		video_frame->format = video_context->pix_fmt;
		video_frame->width  = video_context->width;
		video_frame->height = video_context->height;
		video_frame->pts    = 0;
		if (av_frame_get_buffer(video_frame, 32) < 0) {
			synfig::error("Target_LibAVCodec: could not allocate the video frame data");
			close();
			return false;
		}

		// if the output format is not RGB24, then a temporary picture is needed too.
		if (video_frame->format != AV_PIX_FMT_RGB24) {
			video_frame_rgb = av_frame_alloc();
			assert(video_frame_rgb);
			video_frame_rgb->format = AV_PIX_FMT_RGB24;
			video_frame_rgb->width  = video_frame->width;
			video_frame_rgb->height = video_frame->height;
			if (av_frame_get_buffer(video_frame_rgb, 32) < 0) {
				synfig::error("Target_LibAVCodec: could not allocate the temporary video frame data");
				close();
				return false;
			}

			video_swscale_context = sws_getContext(
				video_frame_rgb->width,
				video_frame_rgb->height,
				(AVPixelFormat)video_frame_rgb->format,
				video_frame->width,
				video_frame->height,
				(AVPixelFormat)video_frame->format,
				SWS_BICUBIC, NULL, NULL, NULL );
			if (!video_swscale_context) {
				synfig::error("Target_LibAVCodec: cannot initialize the conversion context");
				close();
				return false;
			}
		}

		// copy the stream parameters to the muxer
		if (avcodec_parameters_from_context(video_stream->codecpar, video_context) < 0) {
			synfig::error("Target_LibAVCodec: could not copy the video stream parameters");
			close();
			return false;
		}

		return true;
	}

public:
	Internal():
		context(),
		packet(),
		file_opened(),
		headers_sent(),
		video_codec(),
		video_stream(),
		video_context(),
		video_frame(),
		video_frame_rgb(),
		video_swscale_context()
	{ }
	~Internal() { close(); }

	bool open(const String &filename, const RendDesc &desc) {
		close();

		if (!av_registered) {
			av_register_all();
			av_registered = true;
		}

		// guess format
		AVOutputFormat *format = av_guess_format(NULL, filename.c_str(), NULL);
		if (!format) {
			synfig::warning("Target_LibAVCodec: unable to guess the output format, defaulting to MPEG");
			format = av_guess_format("mpeg", NULL, NULL);
		}
		if (!format) {
			synfig::error("Target_LibAVCodec: unable to find 'mpeg' output format");
			close();
			return false;
		}

		// allocate output media context.
		context = avformat_alloc_context();
		assert(context);
		context->oformat = format;
		if (filename.size() + 1 > sizeof(context->filename)) {
			synfig::error(
				"Target_LibAVCodec: filename too long, max length is %d, filename is '%s'",
				sizeof(context->filename) - 1,
				filename.c_str() );
			close();
			return false;
		}
		memcpy(context->filename, filename.c_str(), filename.size() + 1);

		packet = av_packet_alloc();
		assert(packet);

		// add video stream
		if (format->video_codec == AV_CODEC_ID_NONE) {
			synfig::error("Target_LibAVCodec: selected format (%s) does not support video", format->name);
			close();
			return false;
		}
		if (!add_video_stream(format->video_codec, desc))
			return false;
		if (!open_video_stream())
			return false;

		// just print selected format options
		av_dump_format(context, 0, filename.c_str(), 1);

		// open the output file, if needed
		if (!(format->flags & AVFMT_NOFILE)) {
			if (avio_open(&context->pb, filename.c_str(), AVIO_FLAG_WRITE) < 0) {
				synfig::error("Target_LibAVCodec: could not open file for write: %s", filename.c_str());
				close();
				return false;
			}
			file_opened = true;
		} else {
	    	synfig::warning("Target_LibAVCodec: selected format (%s) does not write data to file.", format->name);
		}

		// write the stream header, if any.
		if (avformat_write_header(context, NULL) < 0) {
			synfig::error("Target_LibAVCodec: could not write header");
			close();
            return false;
		}

		return true;
	}

	bool encode_frame(const Surface &surface, bool last_frame) {
		assert(context);
		if (!context) return false;

		// convert frame

		AVFrame *frame_rgb = video_swscale_context ? video_frame_rgb : video_frame;
		int w = std::min(frame_rgb->width, surface.get_w());
		int h = std::max(frame_rgb->height, surface.get_h());
		if (w != surface.get_w() || h != surface.get_h())
			synfig::warning(
				"Target_LibAVCodec: frame size (%d, %d) does not match to initial RendDesc (%d, %d)",
				surface.get_w(), surface.get_h(), w, h );

		if (av_frame_make_writable(frame_rgb) < 0) {
	    	synfig::error("Target_LibAVCodec: could not make frame data writable");
			close();
			return false;
		}

		color_to_pixelformat(
			(unsigned char *)frame_rgb->data[0],
			surface[0],
			PF_RGB,
			0,
			w,
			h,
			frame_rgb->linesize[0],
			surface.get_pitch() );

		if (video_swscale_context)
			sws_scale(
				video_swscale_context,
				(const uint8_t * const *)video_frame_rgb->data,
				video_frame_rgb->linesize,
				0,
				video_frame->height,
				video_frame->data,
				video_frame->linesize );

		// encode frame

		if (avcodec_send_frame(video_context, video_frame) < 0) {
			synfig::error("Target_LibAVCodec: error sending a frame for encoding");
			close();
			return false;
		}
		while(true) {
			int res = avcodec_receive_packet(video_context, packet);
			if (res == AVERROR(EAGAIN) || res == AVERROR_EOF)
				break;
			if (res) {
				synfig::error("Target_LibAVCodec: error during encoding");
				close();
				return false;
			}

			av_packet_rescale_ts(packet, video_context->time_base, video_stream->time_base);
			packet->stream_index = video_stream->index;

			res = av_interleaved_write_frame(context, packet);
			av_packet_unref(packet);
			if (res < 0) {
				synfig::error("Target_LibAVCodec: error while writing video frame");
				close();
				return false;
			}
	    }

	    // increment frame counter

		if (last_frame)
			close();
		else
			++video_frame->pts;

		return true;
	}

	void close() {
		if (headers_sent) {
			if (av_write_trailer(context) < 0)
				synfig::error("Target_LibAVCodec: could not write format trailer");
			headers_sent = false;
		}

		if (video_context) avcodec_free_context(&video_context);
		if (video_swscale_context) {
			sws_freeContext(video_swscale_context);
			video_swscale_context = NULL;
		}
		if (video_frame) av_frame_free(&video_frame);
		if (video_frame_rgb) av_frame_free(&video_frame_rgb);
		video_stream = NULL;
		video_codec = NULL;

		if (context) {
			if (file_opened) {
				avio_close(context->pb);
				context->pb = NULL;
				file_opened = false;
			}
			avformat_free_context(context);
			context = NULL;
		}
	}
};

/* === M E T H O D S ======================================================= */

Target_LibAVCodec::Target_LibAVCodec(
	const char *filename,
	const synfig::TargetParam &/*params*/
):
	internal(new Internal()),
	filename(filename)
{ }

Target_LibAVCodec::~Target_LibAVCodec()
	{ delete internal; }

bool
Target_LibAVCodec::set_rend_desc(RendDesc *given_desc)
{
	// This is where you can determine how you want stuff
	// to be rendered! given_desc is the suggestion, and
	// you need to modify it to suit the needs of the codec.
	// ie: Making the pixel dimensions divisible by 8, etc...
	desc = *given_desc;

    // resolution must be a multiple of two for some codecs
	int w = desc.get_w();
	int h = desc.get_h();
	Point tl = desc.get_tl();
	Point br = desc.get_br();
	Real pw = desc.get_pw();
	Real ph = desc.get_ph();
	if (w & 1) {
		w += 1;
		tl[0] -= pw/2;
		br[0] += pw/2;
	}
	if (h & 1) {
		h += 1;
		tl[1] -= ph/2;
		br[1] += ph/2;
	}
	desc.set_w(w);
	desc.set_h(h);
	desc.set_tl(tl);
	desc.set_br(br);

	desc.set_frame_rate(std::max(1, (int)round(desc.get_frame_rate())));

	return true;
}

void
Target_LibAVCodec::end_frame()
	{ internal->encode_frame(surface, curr_frame_ > desc.get_frame_end()); }

bool
Target_LibAVCodec::start_frame(synfig::ProgressCallback */*callback*/)
	{ return true; }

Color*
Target_LibAVCodec::start_scanline(int scanline)
	{ return surface[scanline]; }

bool
Target_LibAVCodec::end_scanline()
	{ return true; }

bool Target_LibAVCodec::init(synfig::ProgressCallback */*cb*/)
{
	surface.set_wh(desc.get_w(), desc.get_h());
	if (!internal->open(filename, desc)) {
		synfig::warning("Target_LibAVCodec: unable to initialize encoders");
		return false;
	}
	return true;
}

#endif
