/////////////////////////////////////////////////////////////////////////////
//
//  CAPSEO - Capseo Video Codec Library
//  $Id$
//  (cpsrecode re-encodes capseo encoded files into y4m format for further reuse)
//
//  Authors:
//      Copyright (c) 2007 by Christian Parpart <trapni@gentoo.org>
//
//  This code is based on seom's seom-filter utility
//      (http://neopsis.com/projects/seom/)
//
//  This file as well as its whole library is licensed under
//  the terms of GPL. See the file COPYING.
//
/////////////////////////////////////////////////////////////////////////////
#include <capseo.h>

#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <getopt.h>
#include <errno.h>
#include <string.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#if THEORA
# include <ogg/ogg.h>
# include <theora/theora.h>
#endif

class IEncoder {//{{{
public:
	virtual ~IEncoder() {}

	virtual void initialize() = 0;
	virtual void writeFrame(capseo_frame_t *) = 0;
	virtual void finalize() = 0;
};//}}}

int fps = 25;					//!< average fps to re-encode with
int inputFd = -1;				//!< file descriptor, where to read the capseo video from
int outputFd = -1;				//!< file descriptor, where to write the re-encoded video to
capseo_stream_t *stream = 0;	//!< capseo input stream handle
capseo_info_t info;				//!< capseo out parameters
IEncoder *encoder = 0;			//!< the encoder to use

int die(const char *fmt, ...) {//{{{
	va_list va;

	fprintf(stderr, "ERROR: ");
	va_start(va, fmt);
	vfprintf(stderr, fmt, va);
	va_end(va);
	fprintf(stderr, "\nI die...\n");
	exit(1);
	return 1; // never reached.
}//}}}

void printHelp() {//{{{
	printf(
		"capseo recode, version %s\n"
		"\t-r:  frames per second\n"
		"\t-i:  input filename (or - for stdin)\n"
#if THEORA
		"\t-c:  specify output codec to use (y4m, theora)\n"
#else
		"\t-c:  specify output codec to use (y4m only)\n"
#endif
		"\t-o:  output filename (or - for stdout)\n"
		"\t-h:  print help text\n",
		VERSION
	);
}//}}}

template<typename T>
inline T diff(const T& t1, const T& t2) {
	return t1 < t2 ? t2 - t1 : t1 - t2;
}

class TY4MEncoder : public IEncoder {//{{{
public:
	virtual void initialize() {
		char header[128];
		int n = snprintf(header, sizeof(header), "YUV4MPEG2 W%d H%d F%d:1 Ip\n", info.width, info.height, fps);
		write(outputFd, header, n);
	}

	virtual void writeFrame(capseo_frame_t *frame) {
		static const char header[] = "FRAME\n";
		write(outputFd, header, sizeof(header) - 1);

		uint8_t *buffer = frame->buffer;

		// write each freame row up-side-down
		for (int y = info.height - 1; y >= 0; --y)
			write(outputFd, buffer + y * info.width, info.width);

		buffer += info.width * info.height;

		for (int i = 0; i < 2; ++i) {
			for (int y = (info.height / 2) - 1; y >= 0; --y)
				write(outputFd, buffer + y * (info.width / 2), (info.width / 2));

			buffer += info.width * info.height / 4;
		}
	}

	virtual void finalize() {
	}
};//}}}

#if THEORA
class TOggTheoraEncoder : public IEncoder {//{{{
private:
	ogg_stream_state ogg;
	theora_state theora;
	yuv_buffer yuv;
	unsigned char *buffer;

public:
	virtual void initialize() {
		buffer = new unsigned char[info.width * info.height * 4];
		yuv.y_width = ((info.width + 15) >> 4) << 4;
		yuv.y_height = ((info.height + 15) >> 4) << 4;
		yuv.y_stride = yuv.y_width;

		yuv.uv_width = yuv.y_width / 2;
		yuv.uv_stride = yuv.uv_width;
		yuv.uv_height = yuv.y_height / 2;

		theoraInit();
		theoraComments();
		theoraTables();
	}

	unsigned char *flipV(capseo_frame_t *frame) {
		// TODO
		return frame->buffer;
	}

	virtual void writeFrame(capseo_frame_t *frame) {
		yuv.y = flipV(frame);
		yuv.u = yuv.y + info.width * info.height;
		yuv.v = yuv.u + info.width * info.height / 4;

		int rv = theora_encode_YUVin(&theora, &yuv);
		checkError(rv, "theora_encode_YUVin");

		ogg_packet packet;
		rv = theora_encode_packetout(&theora, false, &packet);
		checkError(rv, "theora_encode_packetout");

		ogg_stream_packetin(&ogg, &packet);
		flushOnce();
	}

	virtual void finalize() {
		flushAll();

		theora_clear(&theora);
		ogg_stream_clear(&ogg);

		delete[] buffer;
	}

private:
	void theoraInit() {
		theora_info ti;
		theora_info_init(&ti);

		ti.width = info.width;
		ti.height = info.height;
		ti.frame_width = info.width;
		ti.frame_height = info.height;
		ti.offset_x = 0;
		ti.offset_y = 0;

		ti.fps_numerator = fps;
		ti.fps_denominator = 1;

		ti.aspect_numerator = 1;
		ti.aspect_denominator = 1;

		ti.dropframes_p = 1;
		ti.quick_p = 0;
		ti.keyframe_auto_p = 1;
		ti.keyframe_frequency = 64;
		ti.keyframe_frequency_force = 64;
		ti.keyframe_data_target_bitrate = (ogg_uint32_t)(ti.target_bitrate * 1.5);
		ti.keyframe_auto_threshold = 80;
		ti.keyframe_mindistance = 8;
		ti.quality = 63; // 0..63
		ti.noise_sensitivity = 1;

		theora_encode_init(&theora, &ti);
		theora_info_clear(&ti);

		ogg_packet packet;
		int rv = theora_encode_header(&theora, &packet);
		checkError(rv, "theora_encode_header");

		ogg_stream_packetin(&ogg, &packet);

		flushOnce();
	}

	void theoraComments() {
		theora_comment tc;
		theora_comment_init(&tc);

		static struct {
			char *key;
			char *value;
		} comments[] = {
			{ "ENCODER", "cpsrecode" },
			{ "SOURCE", "captury" },
			{ 0, 0 }
		};

		for (int i = 0; comments[i].key; ++i)
			theora_comment_add_tag(&tc, comments[i].key, comments[i].value);

		ogg_packet packet;
		int rv = theora_encode_comment(&tc, &packet);
		checkError(rv, "theora_encode_comment");

		ogg_stream_packetin(&ogg, &packet);
	}

	void theoraTables() {
		ogg_packet packet;
		int rv = theora_encode_tables(&theora, &packet);
		checkError(rv, "theora_encode_tables");

		ogg_stream_packetin(&ogg, &packet);
		flushAll();
	}

	void checkError(int rv, const char *fname) {
		if (rv < 0) {
			fprintf(stderr, "theora error(%s): %d\n", fname ? fname : "unknown", rv);
			exit(1);
		}
	}

	void flushOnce() {
		ogg_page page;

		if (ogg_stream_pageout(&ogg, &page)) {
			write(outputFd, page.header, page.header_len);
			write(outputFd, page.body, page.body_len);
		}
	}

	void flushAll() {
		ogg_page page;

		while (ogg_stream_pageout(&ogg, &page)) {
			write(outputFd, page.header, page.header_len);
			write(outputFd, page.body, page.body_len);
		}
	}
};//}}}
#endif

void parseCmdLineArgs(int argc, char *argv[]) {//{{{
	int nargs = 1;
	for (int c; (c = getopt(argc, argv, "r:i:c:o:h")) != -1; ++nargs) {
		switch (c) {
			case 'r':
				fps = atoi(optarg);
				break;
			case 'i':
				if (strcmp(optarg, "-") == 0)
					inputFd = STDIN_FILENO;
#if defined(O_LARGEFILE)
				else if ((inputFd = open(optarg, O_RDONLY | O_LARGEFILE)) == -1)
#else
				else if ((inputFd = open(optarg, O_RDONLY)) == -1)
#endif
					die("Error opening input file(%s): %s", optarg, strerror(errno));

				break;
			case 'o':
				if (strcmp(optarg, "-") == 0)
					outputFd = STDOUT_FILENO;
				else if ((outputFd = open(optarg, O_WRONLY | O_CREAT | O_TRUNC, 0644)) == -1)
					die("Error opening output file(%s): %s", optarg, strerror(errno));

				break;
			case 'c':
#if THEORA
				if (strcmp(optarg, "theora") == 0) {
					encoder = new TOggTheoraEncoder();
					break;
				}
#endif
				if (strcmp(optarg, "y4m") == 0) {
					encoder = new TY4MEncoder();
					break;
				}
				die("Unsupported output codec: %s\n", optarg);
			case 'h':
				printHelp();
				exit(0);
			default:
				break;
		}
	}

	if (!encoder)
		encoder = new TY4MEncoder(); // default to y4m

	if (inputFd == -1)
		die("No input file specified");

	if (outputFd == -1)
		die("No output file specified");

	info.format = CAPSEO_FORMAT_YUV420;
	if (int error = CapseoStreamCreateFd(CAPSEO_MODE_DECODE, &info, inputFd, &stream))
		die("Could not create input stream (error %d)", error);
}//}}}

int main(int argc, char *argv[]) {
	bzero(&info, sizeof(capseo_info_t));

	parseCmdLineArgs(argc, argv);

	encoder->initialize();

	capseo_frame_t *frames[2];

	CapseoStreamDecodeFrame(stream, &frames[0], true);
	CapseoStreamDecodeFrame(stream, &frames[1], true);

	uint64_t timeStep = 1000000 / fps;
	uint64_t timeNext = frames[0]->id;

	for (;;) {
		while (diff(frames[0]->id, timeNext) > diff(frames[1]->id, timeNext)) {
			frames[0] = frames[1];

			if (int error = CapseoStreamDecodeFrame(stream, &frames[1], true)) {
				if (error == CAPSEO_STREAM_END)
					goto out;

				return die("CapseoStreamDecodeFrame: decode error (code %d)", error);
			}

			if (!frames[1])
				goto out;
		}

		encoder->writeFrame(frames[0]);

		timeNext += timeStep;
	}

out:
	// write last frame
	encoder->writeFrame(frames[0]);
	encoder->finalize();
	delete encoder;

	CapseoStreamDestroy(stream);

	return 0;
}
