// Copyright (C) 2002-2007 Nikolaus Gebhardt
// Part of the code for this plugin for irrKlang is based on:
//  MP3 input for Audiere by Matt Campbell <mattcampbell@pobox.com>, based on
//  libavcodec from ffmpeg (http://ffmpeg.sourceforge.net/).
// See license.txt for license details of this plugin.

#include "CIrrKlangAudioStreamMP3.h"
#include <memory.h>
#include <stdlib.h> // free, malloc and realloc
#include <string.h>
#include <algorithm>

namespace irrklang
{

CIrrKlangAudioStreamMP3::CIrrKlangAudioStreamMP3(IFileReader* file)
: File(file), TheMPAuDecContext(0), InputPosition(0), InputLength(0),
	DecodeBuffer(0), FirstFrameRead(false), EndOfFileReached(0),
	FileBegin(0), Position(0)
{
	if (File)
	{
		File->grab();

		TheMPAuDecContext = new MPAuDecContext();

		if (!TheMPAuDecContext || mpaudec_init(TheMPAuDecContext) < 0)
		{
			File->drop();
			File = 0;
			delete TheMPAuDecContext;
			TheMPAuDecContext = 0;
			return;
		}

		// init, get format

		DecodeBuffer = new ik_u8[MPAUDEC_MAX_AUDIO_FRAME_SIZE];

		if (File->getSize()>0)
		{
			// seekable file, now parse file to get size
			// (needed to make it possible for the engine to loop a stream correctly)

			skipID3IfNecessary();

			TheMPAuDecContext->parse_only = 1;
			Format.FrameCount = 0;

			while(!EndOfFileReached)
			{
				if (!decodeFrame())
					break;

				Format.FrameCount += TheMPAuDecContext->frame_size;

				if (!EndOfFileReached /*&& File->isSeekable()*/ )
				{
					// to be able to seek in the stream, store offsets and sizes

					SFramePositionData data;
					data.size = TheMPAuDecContext->frame_size;
					data.offset = File->getPos() - (InputLength - InputPosition) - TheMPAuDecContext->coded_frame_size;

					FramePositionData.push_back(data);
				}
			}

			TheMPAuDecContext->parse_only = 0;
			setPosition(0);
		}
		else
			decodeFrame(); // decode first frame to read audio format

		if (!TheMPAuDecContext->channels ||
			!TheMPAuDecContext->sample_rate )
		{
			File->drop();
			File = 0;
			delete TheMPAuDecContext;
			TheMPAuDecContext = 0;
			return;
		}
	}
}

CIrrKlangAudioStreamMP3::~CIrrKlangAudioStreamMP3()
{
	if (File)
		File->drop();

	if (TheMPAuDecContext)
	{
		mpaudec_clear(TheMPAuDecContext);
		delete TheMPAuDecContext;
	}

	delete [] DecodeBuffer;
}



//! returns format of the audio stream
SAudioStreamFormat CIrrKlangAudioStreamMP3::getFormat()
{
	return Format;
}


//! tells the audio stream to read n audio frames into the specified buffer
ik_s32 CIrrKlangAudioStreamMP3::readFrames(void* target, ik_s32 frameCountToRead)
{
	const int frameSize = Format.getFrameSize();

	int framesRead = 0;
	ik_u8* out = (ik_u8*)target;

	while (framesRead < frameCountToRead)
	{
		// no more samples?  ask the MP3 for more
		if (DecodedQueue.getSize() < frameSize)
		{
			if (!decodeFrame() || EndOfFileReached)
				return framesRead;

			// if the buffer is still empty, we are done
			if (DecodedQueue.getSize() < frameSize)
				return framesRead;
		}

		const int framesLeft = frameCountToRead - framesRead;
		const int dequeSize = DecodedQueue.getSize() / frameSize;
		const int framesToRead = framesLeft < dequeSize ? framesLeft : dequeSize;

		DecodedQueue.read(out, framesToRead * frameSize);

		out += framesToRead * frameSize;
		framesRead += framesToRead;
		Position += framesToRead;
	}

	return framesRead;
}



bool CIrrKlangAudioStreamMP3::decodeFrame()
{
    int outputSize = 0;

	while (!outputSize)
	{
		if (InputPosition == InputLength)
		{
			InputPosition = 0;
			InputLength = File->read(InputBuffer, IKP_MP3_INPUT_BUFFER_SIZE);

			if (InputLength == 0)
			{
				EndOfFileReached = true;
				return true;
			}
		}

		int rv = mpaudec_decode_frame( TheMPAuDecContext, (ik_s16*)DecodeBuffer,
									   &outputSize,
									   (ik_u8*)InputBuffer + InputPosition,
									   InputLength - InputPosition);

		if (rv < 0)
			return false;

		InputPosition += rv;
	} // end while

	if (!FirstFrameRead)
	{
		Format.ChannelCount = TheMPAuDecContext->channels;
		Format.SampleRate = TheMPAuDecContext->sample_rate;
		Format.SampleFormat = ESF_S16;
		Format.FrameCount = -1; // unknown lenght

		FirstFrameRead = true;
	}
	else
	if (TheMPAuDecContext->channels != Format.ChannelCount ||
		TheMPAuDecContext->sample_rate != Format.SampleRate)
	{
		// Can't handle format changes mid-stream.
		return false;
    }

	if (!TheMPAuDecContext->parse_only)
	{
		if (outputSize < 0)
		{
			// Couldn't decode this frame.  Too bad, already lost it.
			// This should only happen when seeking.

			outputSize = TheMPAuDecContext->frame_size;
			memset(DecodeBuffer, 0, outputSize * Format.getFrameSize());
		}

		DecodedQueue.write(DecodeBuffer, outputSize);
	}

    return true;
}



//! sets the position of the audio stream.
/** For example to let the stream be read from the beginning of the file again,
setPosition(0) would be called. This is usually done be the sound engine to
loop a stream after if has reached the end. Return true if sucessful and 0 if not. */
bool CIrrKlangAudioStreamMP3::setPosition(ik_s32 pos)
{
	if (!File || !TheMPAuDecContext)
		return false;

	if (pos == 0)
	{
		// usually done for looping, just reset to start

		File->seek(FileBegin); // skip possible ID3 header

		EndOfFileReached = false;

		DecodedQueue.clear();

		MPAuDecContext oldContext = *TheMPAuDecContext;

		mpaudec_clear(TheMPAuDecContext);
		mpaudec_init(TheMPAuDecContext);

		TheMPAuDecContext->bit_rate = oldContext.bit_rate;
		TheMPAuDecContext->channels = oldContext.channels;
		TheMPAuDecContext->frame_size = oldContext.frame_size;
		TheMPAuDecContext->sample_rate = oldContext.sample_rate;

		InputPosition = 0;
		InputLength = 0;
		Position = 0;
		CurrentFramePosition = 0;

		return true;
	}
	else
	{
		// user wants to seek in the stream, so do this here

		int scan_position = 0;
		int target_frame = 0;
		int frame_count = (int)FramePositionData.size();

		while (target_frame < frame_count)
		{
			int frame_size = FramePositionData[target_frame].size;

			if (pos <= scan_position + frame_size)
				break;
			else
			{
				scan_position += frame_size;
				target_frame++;
			}
		}


		const int MAX_FRAME_DEPENDENCY = 10;
		target_frame = std::max(0, target_frame - MAX_FRAME_DEPENDENCY);
		setPosition(0);

		File->seek(FramePositionData[target_frame].offset, false);

		int i;
		for (i = 0; i < target_frame; i++)
		{
			if (i>=(int)FramePositionData.size())
			{
				// internal error
				setPosition(0);
				return false;
			}

			Position += FramePositionData[i].size;
		}

		if (!decodeFrame() || EndOfFileReached)
		{
			setPosition(0);
			return false;
		}

		int frames_to_consume = pos - Position; // PCM frames now
		if (frames_to_consume > 0)
		{
			ik_u8 *buf = new ik_u8[frames_to_consume * Format.getFrameSize()];
			readFrames(buf, frames_to_consume);
			delete[] buf;
		}

      	return true;
	}

	return false;
}


CIrrKlangAudioStreamMP3::QueueBuffer::QueueBuffer()
{
	Capacity = 256;
	Size = 0;

	Buffer = (ik_u8*)malloc(Capacity);
}


CIrrKlangAudioStreamMP3::QueueBuffer::~QueueBuffer()
{
	free(Buffer);
}

int CIrrKlangAudioStreamMP3::QueueBuffer::getSize()
{
	return Size;
}

void CIrrKlangAudioStreamMP3::QueueBuffer::write(const void* buffer, int size)
{
	bool needRealloc = false;

	while (size + Size > Capacity)
	{
		Capacity *= 2;
		needRealloc = true;
	}

    if (needRealloc)
	{
        Buffer = (ik_u8*)realloc(Buffer, Capacity);
    }

	memcpy(Buffer + Size, buffer, size);
	Size += size;
}


int CIrrKlangAudioStreamMP3::QueueBuffer::read(void* buffer, int size)
{
	int toRead = size < Size ? size : Size;

	memcpy(buffer, Buffer, toRead);
	memmove(Buffer, Buffer + toRead, Size - toRead);

	Size -= toRead;
	return toRead;
}


void CIrrKlangAudioStreamMP3::QueueBuffer::clear()
{
	Size = 0;
}


void CIrrKlangAudioStreamMP3::skipID3IfNecessary()
{
	char header[10];
	int read = File->read(&header, 10);

	if (read == 10 &&
		header[0] == 'I' && header[1] == 'D' && header[2] == '3')
	{
		int versionMajor = header[3];
		int versionMinor = header[4];
		int flags = header[5];

		// IDv2 size looks like the following: ID3v2 size  4 * %0xxxxxxx.
		// Sick, but that's how it works.

		int size = 0;
		size  = (header[6] & 0x7f) << (3*7);
		size |= (header[7] & 0x7f) << (2*7);
		size |= (header[8] & 0x7f) << (1*7);
		size |= (header[9] & 0x7f) ;

		size += 10; // header size

		FileBegin = size;
		File->seek(FileBegin);
	}
	else
		File->seek(0);
}


} // end namespace irrklang
