/**
 * Copyright (c) 2006-2016 LOVE Development Team
 *
 * This software is provided 'as-is', without any express or implied
 * warranty.  In no event will the authors be held liable for any damages
 * arising from the use of this software.
 *
 * Permission is granted to anyone to use this software for any purpose,
 * including commercial applications, and to alter it and redistribute it
 * freely, subject to the following restrictions:
 *
 * 1. The origin of this software must not be misrepresented; you must not
 *    claim that you wrote the original software. If you use this software
 *    in a product, an acknowledgment in the product documentation would be
 *    appreciated but is not required.
 * 2. Altered source versions must be plainly marked as such, and must not be
 *    misrepresented as being the original software.
 * 3. This notice may not be removed or altered from any source distribution.
 **/

// STL
#include <iostream>

// LOVE
#include "VideoStream.h"

using love::filesystem::File;

namespace love
{
namespace video
{
namespace theora
{

VideoStream::VideoStream(love::filesystem::File *file)
	: file(file)
	, headerParsed(false)
	, streamInited(false)
	, videoSerial(0)
	, decoder(nullptr)
	, frameReady(false)
	, lastFrame(0)
	, nextFrame(0)
	, eos(false)
	, lagCounter(0)
{
	ogg_sync_init(&sync);
	th_info_init(&videoInfo);

	frontBuffer = new Frame();
	backBuffer = new Frame();

	try
	{
		parseHeader();
	}
	catch (love::Exception &ex)
	{
		delete backBuffer;
		delete frontBuffer;
		th_info_clear(&videoInfo);
		ogg_sync_clear(&sync);
		throw ex;
	}

	frameSync.set(new DeltaSync(), Acquire::NORETAIN);
}

VideoStream::~VideoStream()
{
	if (decoder)
		th_decode_free(decoder);

	th_info_clear(&videoInfo);
	if (headerParsed)
		ogg_stream_clear(&stream);

	ogg_sync_clear(&sync);

	delete frontBuffer;
	delete backBuffer;
}

int VideoStream::getWidth() const
{
	if (headerParsed)
		return videoInfo.pic_width;
	else
		return 0;
}

int VideoStream::getHeight() const
{
	if (headerParsed)
		return videoInfo.pic_height;
	else
		return 0;
}

const std::string &VideoStream::getFilename() const
{
	return file->getFilename();
}

void VideoStream::setSync(FrameSync *frameSync)
{
	love::thread::Lock l(bufferMutex);
	this->frameSync = frameSync;
}

const void *VideoStream::getFrontBuffer() const
{
	return frontBuffer;
}

size_t VideoStream::getSize() const
{
	return sizeof(Frame);
}

bool VideoStream::isPlaying() const
{
	return frameSync->isPlaying() && !eos;
}

void VideoStream::readPage()
{
	char *syncBuffer = nullptr;
	while (ogg_sync_pageout(&sync, &page) != 1)
	{
		if (syncBuffer && !headerParsed && ogg_stream_check(&stream))
			throw love::Exception("Invalid stream");

		syncBuffer = ogg_sync_buffer(&sync, 8192);
		size_t read = file->read(syncBuffer, 8192);
		ogg_sync_wrote(&sync, read);
	}
}

bool VideoStream::readPacket(bool mustSucceed)
{
	if (!streamInited)
	{
		readPage();
		videoSerial = ogg_page_serialno(&page);
		ogg_stream_init(&stream, videoSerial);
		streamInited = true;
		ogg_stream_pagein(&stream, &page);
	}

	while (ogg_stream_packetout(&stream, &packet) != 1)
	{
		do
		{
			// We need to read another page, but there is none, we're at the end
			if (ogg_page_eos(&page) && !mustSucceed)
				return eos = true;

			readPage();
		} while (ogg_page_serialno(&page) != videoSerial);

		ogg_stream_pagein(&stream, &page);
	}

	return false;
}

template<typename T>
inline void scaleFormat(th_pixel_fmt fmt, T &x, T &y)
{
	switch(fmt)
	{
	case TH_PF_420:
		y /= 2;
	case TH_PF_422:
		x /= 2;
		break;
	default:
		break;
	}
}

void VideoStream::parseHeader()
{
	if (headerParsed)
		return;

	th_comment comment;
	th_setup_info *setupInfo = nullptr;
	th_comment_init(&comment);
	int ret;

	do
	{
		readPacket();
		ret = th_decode_headerin(&videoInfo, &comment, &setupInfo, &packet);
		if (ret == TH_ENOTFORMAT)
		{
			ogg_stream_clear(&stream);
			streamInited = false;
		}
	} while(ret < 0 && !ogg_page_eos(&page));

	if (ret < 0)
	{
		th_comment_clear(&comment);
		throw love::Exception("Could not find header");
	}

	while (ret > 0)
	{
		readPacket();
		ret = th_decode_headerin(&videoInfo, &comment, &setupInfo, &packet);
	}

	th_comment_clear(&comment);

	decoder = th_decode_alloc(&videoInfo, setupInfo);
	th_setup_free(setupInfo);

	Frame *buffers[2] = {backBuffer, frontBuffer};

	yPlaneXOffset = cPlaneXOffset = videoInfo.pic_x;
	yPlaneYOffset = cPlaneYOffset = videoInfo.pic_y;

	scaleFormat(videoInfo.pixel_fmt, cPlaneXOffset, cPlaneYOffset);

	for (int i = 0; i < 2; i++)
	{
		buffers[i]->cw = buffers[i]->yw = videoInfo.pic_width;
		buffers[i]->ch = buffers[i]->yh = videoInfo.pic_height;

		scaleFormat(videoInfo.pixel_fmt, buffers[i]->cw, buffers[i]->ch);

		buffers[i]->yplane = new unsigned char[buffers[i]->yw * buffers[i]->yh];
		buffers[i]->cbplane = new unsigned char[buffers[i]->cw * buffers[i]->ch];
		buffers[i]->crplane = new unsigned char[buffers[i]->cw * buffers[i]->ch];

		memset(buffers[i]->yplane, 16, buffers[i]->yw * buffers[i]->yh);
		memset(buffers[i]->cbplane, 128, buffers[i]->cw * buffers[i]->ch);
		memset(buffers[i]->crplane, 128, buffers[i]->cw * buffers[i]->ch);
	}

	headerParsed = true;
	th_decode_packetin(decoder, &packet, nullptr);
}

// Arbitrary seeking isn't supported yet, but rewinding is
void VideoStream::rewind()
{
	// Seek our data stream back to the start
	file->seek(0);

	// Break our sync, and discard the rest of the page
	ogg_sync_reset(&sync);
	ogg_sync_pageseek(&sync, &page);

	// Read our first page/packet from the stream again
	readPacket(true);

	// Now tell theora we're at frame 1 (not 0!)
	int64 granPos = 1;
	th_decode_ctl(decoder, TH_DECCTL_SET_GRANPOS, &granPos, sizeof(granPos));

	// Force a redraw, since this will always be less than the sync's position
	lastFrame = nextFrame = -1;
	eos = false;
}

void VideoStream::seekDecoder(double target)
{
	if (target < 0.01)
	{
		rewind();
		return;
	}

	double low = 0;
	double high = file->getSize();

	while (high-low > 0.0001)
	{
		// Determine our next binary search position
		double pos = (high+low)/2;
		file->seek(pos);

		// Break sync
		ogg_sync_reset(&sync);
		ogg_sync_pageseek(&sync, &page);

		// Read a packet
		readPacket(false);
		if (eos)
			return;

		// Determine if this is the right place
		double curTime = th_granule_time(decoder, packet.granulepos);
		double nextTime = th_granule_time(decoder, packet.granulepos+1);

		if (curTime == -1)
			continue; // Invalid granule position (magic?)
		else if (curTime <= target && nextTime > target)
			break; // the current frame should be displaying right now
		else if (curTime > target)
			high = pos;
		else
			low = pos;
	}

	// Now update theora and our decoder on this new position of ours
	lastFrame = nextFrame = -1;
	eos = false;
	th_decode_ctl(decoder, TH_DECCTL_SET_GRANPOS, &packet.granulepos, sizeof(packet.granulepos));
}

void VideoStream::threadedFillBackBuffer(double dt)
{
	// Synchronize
	frameSync->update(dt);
	double position = frameSync->getPosition();

	// Seeking backwards
	if (position < lastFrame)
		seekDecoder(position);

	// If we're at the end of the stream, or if we're displaying the right frame
	// stop here
	if (eos || position < nextFrame)
		return;

	th_ycbcr_buffer bufferinfo;
	th_decode_ycbcr_out(decoder, bufferinfo);

	ogg_int64_t granulePosition;
	do
	{
		if (readPacket())
			return;
	} while (th_decode_packetin(decoder, &packet, &granulePosition) != 0);
	lastFrame = nextFrame;
	nextFrame = th_granule_time(decoder, granulePosition);

	{
		// Don't swap whilst we're writing to the backbuffer
		love::thread::Lock l(bufferMutex);
		frameReady = false;
	}

	for (int y = 0; y < backBuffer->yh; ++y)
	{
		memcpy(backBuffer->yplane+backBuffer->yw*y,
				bufferinfo[0].data+
					bufferinfo[0].stride*(y+yPlaneYOffset)+yPlaneXOffset,
				backBuffer->yw);
	}

	for (int y = 0; y < backBuffer->ch; ++y)
	{
		memcpy(backBuffer->cbplane+backBuffer->cw*y,
				bufferinfo[1].data+
					bufferinfo[1].stride*(y+cPlaneYOffset)+cPlaneXOffset,
				backBuffer->cw);
	}

	for (int y = 0; y < backBuffer->ch; ++y)
	{
		memcpy(backBuffer->crplane+backBuffer->cw*y,
				bufferinfo[2].data+
					bufferinfo[2].stride*(y+cPlaneYOffset)+cPlaneXOffset,
				backBuffer->cw);
	}

	// Seeking forwards:
	// If we're still not on the right frame, either we're lagging or we're seeking
	// After 5 frames, go for a seek. This is not ideal.. but what is
	if (position > nextFrame)
	{
		if (++lagCounter > 5)
			seek(position);
	}
	else
		lagCounter = 0;

	love::thread::Lock l(bufferMutex);
	frameReady = true;
}

void VideoStream::fillBackBuffer()
{
	// Done in worker thread
}

bool VideoStream::swapBuffers()
{
	if (eos)
		return false;

	love::thread::Lock l(bufferMutex);
	if (!frameReady)
		return false;
	frameReady = false;

	Frame *temp = frontBuffer;
	frontBuffer = backBuffer;
	backBuffer = temp;

	return true;
}

} // theora
} // video
} // love
