/***********************************************************************
 * VirtualDub Modification for OGM
 *
 * Copyright (C) 2002 Cyrius
 *
 * This program 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 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.  See the
 * GNU General Public License for more details.
 *   
 * You should have received a copy of the GNU General Public License along
 * with this program (see the file COPYING); if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 * or visit http://www.gnu.org/copyleft/gpl.html
 *
 ***********************************************************************
 *
 *
 *
 */

//#include <stdlib.h>

#include "OGMPacketizer.h"
#include "../vorbis/codec.h"

/*
 * Duplicate an ogg_page (static function)
 */
ogg_page *Packetizer::copy_page(ogg_page *src_page) {
  ogg_page *dest_page = NULL;

	try {
		// Before copying the page, we must verify everything is OK
		if(!src_page)
			throw MyError("Trying to copy a NULL ogg_page");

		dest_page = new ogg_page;
		if(!dest_page)
			throw MyError("Could not create a new ogg_page");

		dest_page->header = NULL;
		dest_page->header_len = 0;
		dest_page->body = NULL;
		dest_page->body_len = 0;

		if(!src_page->header_len) {
			_RPT0(_CRT_WARN, "Copying an ogg_page with NULL header\n");
		} else {
			dest_page->header = (unsigned char *)malloc(src_page->header_len);
			if(!dest_page->header)
				throw MyMemoryError();

			memcpy(dest_page->header, src_page->header, src_page->header_len);
			dest_page->header_len = src_page->header_len;
		}

		if(!src_page->body_len) {
			dest_page->body = (unsigned char *)malloc(1);
			if(!dest_page->body)
				throw MyMemoryError();

			dest_page->body_len = 0;
			*(dest_page->body) = 0;
		} else {
	   dest_page->body = (unsigned char *)malloc(src_page->body_len);
		 if(!dest_page->body)
			 throw MyMemoryError();

	   memcpy(dest_page->body, src_page->body, src_page->body_len);
	   dest_page->body_len = src_page->body_len;
		}
	} catch(MyError err) {
		if(dest_page) {
			if(dest_page->header)
				free(dest_page->header);
			if(dest_page->body)
				free(dest_page->body);
			delete dest_page;
		}
		throw;
	}

	return dest_page;
}

/*
 * Duplicate an ogg_packet (static function)
 */
void Packetizer::copy_packet(ogg_packet *src_packet, ogg_packet *dest_packet) {
	try {
		// We must verify the Packet is "OK"
		if(!src_packet)
			throw MyError("Trying to copy a NULL ogg_packet");

		if(!dest_packet)
			throw MyError("NULL dest_packet given");

		*dest_packet = *src_packet;
		dest_packet->packet = NULL;

		if(src_packet->bytes<0) {
			_RPT0(_CRT_WARN, "Copying an empty ogg_packet\n");
		} else {
			dest_packet->packet = (unsigned char *)malloc(src_packet->bytes);
			if(!dest_packet->packet)
				throw MyMemoryError();

			memcpy(dest_packet->packet, src_packet->packet, src_packet->bytes);
		}
	} catch(MyError err) {
		if(dest_packet) {
			if(dest_packet->packet)
				free(dest_packet->packet);
			delete dest_packet;
		}
		throw;
	}
}

/*
 * Constructor
 */
Packetizer::Packetizer(int serial               //Serial that should be used (if not yet used)
											 , SerialGenerator *generator //a Serial Generator
											 , stream_header sheader  //the stream_header of the stream
											 , double delay_ms       //the delay (do not use because don't work well)
											 , bool use_header        //does the Packetizer generate a header itself ?
											 , bool pos_is_end        //does the granulepos represents the beginning or ending position for this stream ?
											 , ogg_int64_t start_granulepos //What is the starting granulepos (should be 0 or 1)
											 , int nb_headers
											 , bool priority_headers
											 , bool group_headers)
{
	s_generator = generator;
	if(s_generator)
		serialno = s_generator->create_unique_serial(serial);
	else
		throw MyError("NULL SerialGenerator given");
	// Don't forget to register this new serial !!
	s_generator->register_serial(serialno);
	mpage_queue = NULL;
	last_mesh = NULL;
	_state = STATE_OK;
	this->nb_headers = nb_headers;
	this->priority_headers = priority_headers;
	priority_set = 0;
	this->group_headers = group_headers;
	header_pages = -1;
	memset(&sh, 0, sizeof(sh));
	sh = sheader;
	samples_per_second = ((double)sh.samples_per_unit * (double)10000000) / (double)sh.time_unit;
	standard_buffersize = real_buffersize = sheader.buffersize;
	buffer = NULL;
	buffer = (unsigned char *)malloc(standard_buffersize + OVERHEAD);
	if(!buffer) {
		_state = E_INVALID;
		throw MyMemoryError();
	}
	memset(buffer, 0, standard_buffersize + OVERHEAD);
	memset(&oss, 0, sizeof(oss));
	// Initialize the ogg stream
	if(ogg_stream_init(&oss, serialno) == -1) {
		_state = E_INVALID;
		throw MyError("ogg_stream_init failed");
	}
	packetno = 0;
	t_delay = delay_ms;
	timestamp = delay_ms;
	//                granulepos * sh.time_unit
	// tstamp (ms) = ----------------------------  (see below)
	//                10000 * sh.samples_per_unit
	granulepos_delay = samples_per_second * delay_ms / 1000;
	// granulepos = granular position of the _LAST_ sample from an object (Page
	// or Packet) that can be fully decoded ...
	last_granulepos = (start_granulepos-1) + granulepos_delay;
	this->start_granulepos = start_granulepos;
	// Hum, for srt subtitles granulepos correspond to the beginning of the subtitle
	granulepos_is_end = pos_is_end;
	if(!granulepos_is_end)
		last_granulepos = granulepos_delay;
	pages = 0;
	packets = 0;
	packet_in_buffer = false;
	// Create the header packet and flush data to output the first corresponding page
	if(use_header)
		produce_header_packet();
}

/*
 * Destructor
 */
Packetizer::~Packetizer(void) {
	// Unregister the serial
	// Warning : this mean one shouldn't create new packetizers after having 
	// destroyed previous ones if there still remains some packtizers.
	// In particular : create packetizers to process data streams, removing some of
	// these and creating new ones (still in the same goal) could result in the use
	// of identical serials
	s_generator->unregister_serial(serialno);
	mpage_mesh *next_mpage;
	if(mpage_queue)
		_RPT0(_CRT_WARN, "There was still muxer_pages pending when finishing the process ...\n");
	while(mpage_queue) {
		next_mpage = mpage_queue->next;
		delete mpage_queue;
		mpage_queue = next_mpage;
	}
	if(buffer)
		free(buffer);
	ogg_stream_clear(&oss);
}

void Packetizer::update_granulepos(time_stamp diff_ms) {
	timestamp += diff_ms;
	t_delay += diff_ms;
	ogg_int64_t granulepos_diff = samples_per_second * diff_ms / 1000;
	last_granulepos += granulepos_diff;
	granulepos_delay += granulepos_diff;
}

/*
 * Add an ogg_page to the queue
 */
void Packetizer::add_page(ogg_page *new_page) {
	if(_state == E_INVALID)
		throw MyError("Fatal error has previously occured on this stream, cannot proceed");

	mpage_mesh *new_mesh = NULL;

	try {
		if(!new_page)
			throw MyError("Trying to queue a NULL ogg_page");
		if(!new_page->header)
			throw MyError("Trying to queue an ogg_page with NULL header");
		if(!new_page->body)
			throw MyError("Trying to queue an ogg_page with NULL body");

		new_mesh = new mpage_mesh;
		if(!new_mesh)
			throw MyMemoryError();

		new_mesh->m_page = new muxer_page;
		if(!new_mesh->m_page)
			throw MyMemoryError();

		// Make a copy of the page and get the associated timestamp
		new_mesh->m_page->page = copy_page(new_page);
		// At the present time timestamp equal the time_stamp of the previous page
		// So update last_timestamp before calling make_timestamp()
		ogg_int64_t g_pos = ogg_page_granulepos(new_page);
		if( (pages==0) || (g_pos==0) ) {
			new_mesh->m_page->last_timestamp = 0;
			timestamp = t_delay;
		} else {
			new_mesh->m_page->last_timestamp = timestamp;

			make_timestamp(g_pos+1);
		}

		if(!granulepos_is_end)
			new_mesh->m_page->last_timestamp = timestamp;

		if(!mpage_queue) {
			mpage_queue = new_mesh;
			last_mesh = mpage_queue;
		} else {
			last_mesh->next = new_mesh;
			last_mesh = new_mesh;
		}
		pages++;
	} catch(MyError err) {
		_state = E_INVALID;
		if(new_mesh)
			delete new_mesh; /* delete operator redefined to delete all allocated memory for the struture*/
		throw;
	}
}

/*
 * Ask to output pages by flushing pending data in the ogg stream
 */
void Packetizer::flush_pages(void) {
  ogg_page new_page;

  while(ogg_stream_flush(&oss, &new_page))
    add_page(&new_page);
}

/*
 * Get pending pages from the ogg stream
 */
void Packetizer::queue_pages(void) {
  ogg_page new_page;

  while(ogg_stream_pageout(&oss, &new_page))
    add_page(&new_page);
}

/*
 * Ask to output pages by flushing pending data in the ogg stream
 */
void Packetizer::flush_header_pages(void) {
  ogg_page new_page;

	while(ogg_stream_flush(&oss, &new_page)) {
		if(header_pages == -1)
			header_pages = 1;
		else
			header_pages++;
    add_page(&new_page);
		set_mpage_priority();
	}
}

/*
 * Get pending pages from the ogg stream
 */
void Packetizer::queue_header_pages(void) {
  ogg_page new_page;

	while(ogg_stream_pageout(&oss, &new_page)) {
		if(header_pages == -1)
			header_pages = 1;
		else
			header_pages++;
		add_page(&new_page);
		set_mpage_priority();
	}
}

/*
 * Set the mpage priority (headers)
 */
void Packetizer::set_mpage_priority(void) {
	if(!last_mesh)
		return;

	// Real header = priority 3
	// Important headers = priority 2
	// Other headers = priority 1
	if(!priority_set)
		last_mesh->m_page->priority = 3;
	else if(priority_headers)
		last_mesh->m_page->priority = 2;
	else
		last_mesh->m_page->priority = 1;
	priority_set++;
}

/*
 * Get the first pending muxer_page (if any)
 */
muxer_page *Packetizer::get_mpage(void) {
	if(_state == E_INVALID)
		throw MyError("Fatal error has previously occured on this stream, cannot proceed");

	muxer_page *m_page = NULL;
	mpage_mesh *next_mesh = NULL;
	if(mpage_queue) {
		m_page = mpage_queue->m_page;
		next_mesh = mpage_queue->next;
		// ATTENTION : USE free INSTEAD OF delete
		// delete would also unalloc the muxer_page, which would be bad ...
		free(mpage_queue);
		mpage_queue = next_mesh;
	} else {
		_RPT0(_CRT_WARN, "No pending muxer_page available\n");
	}

  return m_page;
}

/*
 * Get the first pending header muxer_page (if any)
 */
muxer_page *Packetizer::get_header(void) {
	if(_state == E_INVALID)
		throw MyError("Fatal error has previously occured on this stream, cannot proceed");

	muxer_page *m_page = NULL;
	mpage_mesh *next_mesh = NULL;
	if(mpage_queue && (header_pages>0)) {
		m_page = mpage_queue->m_page;
		header_pages--;
		next_mesh = mpage_queue->next;
		// ATTENTION : USE free INSTEAD OF delete
		// delete would also unalloc the muxer_page, which would be bad ...
		free(mpage_queue);
		mpage_queue = next_mesh;
	} else {
		_RPT0(_CRT_WARN, "No pending muxer_page available\n");
	}

  return m_page;
}

/*
 * Return the smallest last_timestamp of pending pages (if any), i.e. the first one
 */
time_stamp Packetizer::get_smallest_last_timestamp(void) {
	if(mpage_queue && mpage_queue->m_page)
			return mpage_queue->m_page->last_timestamp;
	else
		return MAX_TIMESTAMP;
}

/*
 * Prepare the header packet
 */
void Packetizer::produce_header_packet(void) {
	if(_state == E_INVALID)
		throw MyError("Fatal error has previously occured on this stream, cannot proceed");

	if(packets>0) {
		_RPT0(_CRT_WARN, "Header Packet already processed\n");
		return;
	}

	ogg_packet new_packet;
	new_packet.b_o_s = 1;
	new_packet.e_o_s = 0;
	new_packet.granulepos = 0;
	new_packet.packet = buffer;

  *new_packet.packet = PACKET_TYPE_HEADER;
	new_packet.bytes = sizeof(sh) + 1;
	memcpy(buffer+1, &sh, sizeof(sh));

	ogg_stream_packetin(&oss, &new_packet);
	// If we don't want to group headers, or if there are no other headers, flush pages
	if( (!group_headers) || (nb_headers==1) )
		flush_header_pages();
	else
		queue_header_pages();
	packets++;
	nb_headers--;
}

void Packetizer::produce_comment_packet(comment_list *comments, bool flush) {
	ogg_packet packet;
	packet.granulepos = 0;
	packet.packetno = 1;
	vorbis_comment vc;
	vorbis_comment_init(&vc);
	comment_list *comment = comments;
	while(comment) {
		if(comment->tag)
			vorbis_comment_add_tag(&vc, comment->tag, comment->comment);
		else
			vorbis_comment_add(&vc, comment->comment);
		comment = comment->next;
	}
	vorbis_commentheader_out(&vc, &packet);
	process_packet(&packet, flush);
}

/*
 * Produce an End Of Stream packet
 */
void Packetizer::produce_eos_packet(void) {
	if(_state == E_INVALID)
		throw MyError("Fatal error has previously occured on this stream, cannot proceed");

	if(_state == STATE_EOS) {
		_RPT0(_CRT_WARN, "Trying to produce EOS Packet while EOS already reached\n");
		return;
	}

	// Verify wa have at least produced one Packet
	if(packet_in_buffer) {
		buffer_packet.e_o_s = 1;
		process_buffer_packet();
	} else {
		// No Packet in buffer but Packets sent, so we must create an EOS Packet ourself
		if(packets) {
			unsigned char *data = (unsigned char *)malloc(1);
			if(!data)
				throw MyMemoryError();
			*data = PACKET_TYPE_DATA;
			buffer_packet.b_o_s = 0;
			buffer_packet.bytes = 1;
			buffer_packet.e_o_s = 1;
			buffer_packet.granulepos = last_granulepos;
			buffer_packet.packet = data;
			buffer_packet.packetno = 0;
			packet_in_buffer = true;
			process_buffer_packet();
		}
		_RPT0(0, "EOS reached\n");
		_state = STATE_EOS;
	}
}

/*
 * Produce the timestamp corresponding to a granulepos
 */
time_stamp Packetizer::make_timestamp(ogg_int64_t granulepos) {
	time_stamp tstamp = 0.;

	// Time stamp : position in micros
	// I try this :
	// granulepos = sample number (since the beginning)
	// 1000 : to get milliseconds from seconds
	// 10000000 : reference of the time_unit (100 ns)
	// So samples_per_unit * (10000000 / time_unit) = samples_per_second
	// And then tstamp (in s) = granulepos / samples_per_second
	// Or tstamp (in ms) = 1000 * granulepos / samples_per_unit
	// This give :
	//           granulepos * 1000 * sh.time_unit      1000 * granulepos
	// tstamp = ---------------------------------- = --------------------
	//             10000000 * sh.samples_per_unit     samples_per_second
	// which can be simplified by :
	timestamp = tstamp = ((double)1000 * (double)(granulepos-start_granulepos))
		/ (samples_per_second);

	// If this is not correct for a particular case, one should implement the
	// Packetizer object to handle this case by overriding this function
	return tstamp;
}

/*
 * Give directly an ogg_packet to the stream
 */
void Packetizer::process_packet(ogg_packet *new_packet, bool flush) {
	if(_state == E_INVALID)
		throw MyError("Fatal error has previously occured on this stream, cannot proceed");
	// This is still a "header" (header, comment or setup) packet
	if(nb_headers) {
		ogg_stream_packetin(&oss, new_packet);
		// First packet (header) should be alone in its own Page
		// Seems the Ogg layer do this on its own (return a Page when
		// calling queue_header_pages() after sending the first Packet)
		// But do it on our own too
		// If we don't want to group headers, or if there are no other headers, flush pages
		if( (!packets) || (!group_headers) || (nb_headers==1) )
			flush_header_pages();
		else
			queue_header_pages();
		packets++;
		nb_headers--;
		// Who knows ...
		if(new_packet->e_o_s) {
			_RPT0(0, "EOS reached\n");
			_state = STATE_EOS;
		}
		return;
	}
	if(packet_in_buffer)
		process_buffer_packet();
	if(_state == STATE_EOS) {
		_RPT0(_CRT_WARN, "Trying to process a new Page after EOS declared\n");
		return;
	}

	if(granulepos_delay && new_packet->granulepos>0)
		new_packet->granulepos += granulepos_delay;

	if(new_packet->granulepos > 0)
		last_granulepos = new_packet->granulepos;

	// Keep the new packet in "buffer" (usefull when you reach the end of a file and
	// call the produce_eos_packet() on a Packetizer, because the Packetizer will then
	// just set the eos flag in the "buffered" packet and then send it to the ogg stream
	if(flush) {
		// If flushing asked, don't use the buffer
		ogg_stream_packetin(&oss, new_packet);
		flush_pages();
		if(new_packet->e_o_s) {
			_RPT0(0, "EOS reached\n");
			_state = STATE_EOS;
		}
	} else {
		copy_packet(new_packet, &buffer_packet);
		packet_in_buffer = true;
		if(new_packet->e_o_s)
			process_buffer_packet();
	}

	packets++;
}

/*
 * Put data (frame) into a new packet submitted to the ogg stream
 */
/*void Packetizer::process_frame(unsigned char *frame  // data to be processed
															, int frame_type        // frame type (header, comment, data, other)
															, long frame_size       // size of the data
															, ogg_int64_t nSamples // samples in those data
															, bool is_key_frame          // Is this a keyframe ?
															, bool is_last_frame)*/        // Is this the last frame ?
void Packetizer::process_frame(unsigned char *frame  // data to be processed
														  , int frame_type
															, long frame_size       // size of the data
															, ogg_int64_t nSamples // samples in those data
															, bool is_key_frame          // Is this a keyframe ?
															, bool is_last_frame        // Is this the last frame ?
															, bool flush)
{
	if(_state == E_INVALID)
		throw MyError("Fatal error has previously occured on this stream, cannot proceed");
	if(packet_in_buffer)
		process_buffer_packet();
	if(_state == STATE_EOS) {
		_RPT0(_CRT_WARN, "Trying to process new data into a Packet after EOS declared\n");
		return;
	}

	if(frame_size > standard_buffersize) {
		_RPT2(_CRT_WARN, "Size of data (%d) exceeds predefined buffer size (%d). " \
			"Will allocate more data, but letting this as is could produce unpredictible " \
			"results while decoding the stream.\n", frame_size, standard_buffersize);
		if(frame_size > real_buffersize) {
			buffer = (unsigned char *)realloc(buffer, frame_size + OVERHEAD);
			real_buffersize = frame_size;
		}
		if(!buffer) {
			_state = E_INVALID;
			throw MyMemoryError();
		}
	}

	ogg_packet new_packet;
	new_packet.b_o_s = 0;
	new_packet.e_o_s = (is_last_frame ? 1 : 0);
	if(granulepos_is_end)
		new_packet.granulepos = last_granulepos + nSamples;
	else
		new_packet.granulepos = last_granulepos;
	// SRT subtitles streams use granulepos as beginning and not end
	if(new_packet.granulepos < 0) {
		_RPT0(_CRT_WARN, "Trying to create a Packet with negative granulepos, skipped");
		return;
	}
	//Packet number is of no use when encoding
	//new_packet.packetno = packetno++;
	//packetno++;
	new_packet.packet = buffer;

	if(frame_type == F_DATA) {
		*new_packet.packet = PACKET_TYPE_DATA;
		// We could use this to know how many bytes are required to code nSamples :
		// char hdrlen = (1 + log(nSamples)/log(16)) / 2;
		// But it's better this way :
		unsigned char hdrlen = 0;
		// Common usage seems to skip this step for nSamples=1, which occurs
		// for example in (normally) every video packet
		if(nSamples > 1) {
			ogg_int64_t ns = nSamples;
			while(ns) {
				*(new_packet.packet+1+hdrlen) = ns & 0xFF;
				ns>>=8;
				hdrlen++;
			}
		}
		*new_packet.packet |= (hdrlen>>1) & PACKET_LEN_BITS2;
		*new_packet.packet |= (hdrlen<<6) & PACKET_LEN_BITS01;
		if(is_key_frame)
			*new_packet.packet |= PACKET_IS_SYNCPOINT;
		new_packet.bytes = frame_size + hdrlen + 1;

		memcpy(buffer + hdrlen + 1, frame, frame_size);
	} else {
		new_packet.bytes = frame_size;

		memcpy(buffer, frame, frame_size);
	}

	// Keep the new packet in "buffer" (usefull when you reach the end of a file and
	// call the produce_eos_packet() on a Packetizer, because the Packetizer will then
	// just set the eos flag in the "buffered" packet and then send it to the ogg stream
	if(flush || is_last_frame ) {
		// If flushing needed, don't use the buffer
		ogg_stream_packetin(&oss, &new_packet);
		flush_pages();
		if(is_last_frame) {
			_RPT0(0, "EOS reached\n");
			_state = STATE_EOS;
		}
	} else {
		copy_packet(&new_packet, &buffer_packet);
		packet_in_buffer = true;
	}

	last_granulepos += nSamples;
	packets++;
}

/*
 * Process the packet buffered
 */
void Packetizer::process_buffer_packet(void) {
	if(_state == E_INVALID)
		throw MyError("Fatal error has previously occured on this stream, cannot proceed");
	if(_state == STATE_EOS) {
		_RPT0(_CRT_WARN, "Trying to process new data into a Packet after EOS declared\n");
		return;
	}
	if(!packet_in_buffer) {
		_RPT0(_CRT_WARN, "Trying to process buffered Packet while there is no Packet in buffer!\n");
		return;
	}

	if(buffer_packet.e_o_s) {
		_RPT0(0, "EOS reached\n");
		_state = STATE_EOS;
	}

	ogg_stream_packetin(&oss, &buffer_packet);

	// Don't forget to free the memory !!!! (unless you want REALLY REALLY BIG memory leaks ;)
	if(buffer_packet.packet)
		free(buffer_packet.packet);

	packet_in_buffer = false;

	queue_pages();
}