/*
 *  BackgroundMusic.cpp
 *  doom
 *
 *  Created by John Carmack on 5/15/09.
 *  Copyright 2009 Id Software. All rights reserved.
 *
 */
/*
 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; if not, write to the Free Software
 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 
 */


//==================================================================================================
//	Includes
//==================================================================================================

//	System Includes
#include <AudioToolbox/AudioToolbox.h>
#include <CoreFoundation/CFURL.h>
#include <OpenAL/al.h>
#include <OpenAL/alc.h>
#include <map>
#include <vector>
#include <pthread.h>
#include <mach/mach.h>
#include <string>

extern "C" {
#include "doomiphone.h"
}

enum {
	kSoundEngineErrUnitialized			= 1,
	kSoundEngineErrInvalidID			= 2,
	kSoundEngineErrFileNotFound			= 3,
	kSoundEngineErrInvalidFileFormat	= 4,
	kSoundEngineErrDeviceNotFound		= 5
};


#define	AssertNoError(inMessage, inHandler)						\
if(result != noErr)									\
{													\
printf("%s: %d\n", inMessage, (int)result);		\
goto inHandler;									\
}


#define kNumberBuffers 3


static Float32				gMasterVolumeGain = 0.5f;


//==================================================================================================
//	Helper functions
//==================================================================================================

OSStatus LoadFileDataInfo(const char *inFilePath, AudioFileID &outAFID, AudioStreamBasicDescription &outFormat, UInt64 &outDataSize)
{
	UInt32 thePropSize;
	
	CFURLRef theURL = CFURLCreateFromFileSystemRepresentation(kCFAllocatorDefault, (UInt8*)inFilePath, strlen(inFilePath), false);
	if (theURL == NULL)
		return kSoundEngineErrFileNotFound;	

	OSStatus result = AudioFileOpenURL(theURL, kAudioFileReadPermission, 0, &outAFID);
	CFRelease(theURL);
	AssertNoError("Error opening file", end);
	
	thePropSize = sizeof(outFormat);
	result = AudioFileGetProperty(outAFID, kAudioFilePropertyDataFormat, &thePropSize, &outFormat);
	AssertNoError("Error getting file format", end);
	
	thePropSize = sizeof(UInt64);
	result = AudioFileGetProperty(outAFID, kAudioFilePropertyAudioDataByteCount, &thePropSize, &outDataSize);
	AssertNoError("Error getting file data size", end);
	
end:
	return result;
}

void CalculateBytesForTime (AudioStreamBasicDescription & inDesc, UInt32 inMaxPacketSize, Float64 inSeconds, UInt32 *outBufferSize, UInt32 *outNumPackets)
{
	static const UInt32 maxBufferSize = 0x10000; // limit size to 64K
	static const UInt32 minBufferSize = 0x4000; // limit size to 16K
	
	if (inDesc.mFramesPerPacket) {
		Float64 numPacketsForTime = inDesc.mSampleRate / inDesc.mFramesPerPacket * inSeconds;
		*outBufferSize = (long unsigned int)numPacketsForTime * inMaxPacketSize;
	} else {
		// if frames per packet is zero, then the codec has no predictable packet == time
		// so we can't tailor this (we don't know how many Packets represent a time period
		// we'll just return a default buffer size
		*outBufferSize = maxBufferSize > inMaxPacketSize ? maxBufferSize : inMaxPacketSize;
	}
	
	// we're going to limit our size to our default
	if (*outBufferSize > maxBufferSize && *outBufferSize > inMaxPacketSize)
		*outBufferSize = maxBufferSize;
	else {
		// also make sure we're not too small - we don't want to go the disk for too small chunks
		if (*outBufferSize < minBufferSize)
			*outBufferSize = minBufferSize;
	}
	*outNumPackets = *outBufferSize / inMaxPacketSize;
}

static Boolean MatchFormatFlags(const AudioStreamBasicDescription& x, const AudioStreamBasicDescription& y)
{
	UInt32 xFlags = x.mFormatFlags;
	UInt32 yFlags = y.mFormatFlags;
	
	// match wildcards
	if (x.mFormatID == 0 || y.mFormatID == 0 || xFlags == 0 || yFlags == 0) 
		return true;
	
	if (x.mFormatID == kAudioFormatLinearPCM)
	{		 		
		// knock off the all clear flag
		xFlags = xFlags & ~kAudioFormatFlagsAreAllClear;
		yFlags = yFlags & ~kAudioFormatFlagsAreAllClear;
		
		// if both kAudioFormatFlagIsPacked bits are set, then we don't care about the kAudioFormatFlagIsAlignedHigh bit.
		if (xFlags & yFlags & kAudioFormatFlagIsPacked) {
			xFlags = xFlags & ~kAudioFormatFlagIsAlignedHigh;
			yFlags = yFlags & ~kAudioFormatFlagIsAlignedHigh;
		}
		
		// if both kAudioFormatFlagIsFloat bits are set, then we don't care about the kAudioFormatFlagIsSignedInteger bit.
		if (xFlags & yFlags & kAudioFormatFlagIsFloat) {
			xFlags = xFlags & ~kAudioFormatFlagIsSignedInteger;
			yFlags = yFlags & ~kAudioFormatFlagIsSignedInteger;
		}
		
		//	if the bit depth is 8 bits or less and the format is packed, we don't care about endianness
		if((x.mBitsPerChannel <= 8) && ((xFlags & kAudioFormatFlagIsPacked) == kAudioFormatFlagIsPacked))
		{
			xFlags = xFlags & ~kAudioFormatFlagIsBigEndian;
		}
		if((y.mBitsPerChannel <= 8) && ((yFlags & kAudioFormatFlagIsPacked) == kAudioFormatFlagIsPacked))
		{
			yFlags = yFlags & ~kAudioFormatFlagIsBigEndian;
		}
		
		//	if the number of channels is 0 or 1, we don't care about non-interleavedness
		if (x.mChannelsPerFrame <= 1 && y.mChannelsPerFrame <= 1) {
			xFlags &= ~kLinearPCMFormatFlagIsNonInterleaved;
			yFlags &= ~kLinearPCMFormatFlagIsNonInterleaved;
		}
	}
	return xFlags == yFlags;
}

Boolean FormatIsEqual(AudioStreamBasicDescription x, AudioStreamBasicDescription y)
{
	//	the semantics for equality are:
	//		1) Values must match exactly
	//		2) wildcard's are ignored in the comparison
	
#define MATCH(name) ((x.name) == 0 || (y.name) == 0 || (x.name) == (y.name))
	
	return 
	((x.mSampleRate==0.) || (y.mSampleRate==0.) || (x.mSampleRate==y.mSampleRate)) 
	&& MATCH(mFormatID)
	&& MatchFormatFlags(x, y)  
	&& MATCH(mBytesPerPacket) 
	&& MATCH(mFramesPerPacket) 
	&& MATCH(mBytesPerFrame) 
	&& MATCH(mChannelsPerFrame) 		
	&& MATCH(mBitsPerChannel) ;
}

#pragma mark ***** BackgroundTrackMgr *****
//==================================================================================================
//	BackgroundTrackMgr class
//==================================================================================================
typedef struct BG_FileInfo {
	std::string						mFilePath;
	AudioFileID						mAFID;
	AudioStreamBasicDescription		mFileFormat;
	UInt64							mFileDataSize;
	//UInt64							mFileNumPackets; // this is only used if loading file to memory
	Boolean							mLoadAtOnce;
	Boolean							mFileDataInQueue;
} BackgroundMusicFileInfo;

class BackgroundTrackMgr
	{	
	public:
		BackgroundTrackMgr();
		~BackgroundTrackMgr();
		
		void Teardown();
				
		static void QueueCallback( void * inUserData, AudioQueueRef inAQ, AudioQueueBufferRef inCompleteAQBuffer);
		
		OSStatus SetupQueue(BG_FileInfo *inFileInfo);
		OSStatus SetupBuffers(BG_FileInfo *inFileInfo);
		OSStatus LoadTrack(const char* inFilePath, Boolean inAddToQueue, Boolean inLoadAtOnce);
		
		OSStatus SetVolume(Float32 inVolume);
		Float32 GetVolume() const;
		
		OSStatus Start();
		OSStatus Stop(Boolean inStopAtEnd);
		
		AudioQueueRef						mQueue;
		AudioQueueBufferRef					mBuffers[kNumberBuffers];
		UInt32								mBufferByteSize;
		SInt64								mCurrentPacket;
		UInt32								mNumPacketsToRead;
		Float32								mVolume;
		AudioStreamPacketDescription *		mPacketDescs;
		static BG_FileInfo *				CurFileInfo;
		Boolean								mStopAtEnd;
	};

BG_FileInfo *BackgroundTrackMgr::CurFileInfo;


BackgroundTrackMgr::BackgroundTrackMgr() 
:	mQueue(0),
mBufferByteSize(0),
mCurrentPacket(0),
mNumPacketsToRead(0),
mVolume(1.0f),
mPacketDescs(NULL),
mStopAtEnd(false)
{ }

BackgroundTrackMgr::~BackgroundTrackMgr() {
	Teardown();
}

void BackgroundTrackMgr::Teardown() {
	if (mQueue) {
		AudioQueueDispose(mQueue, true);
		mQueue = NULL;
	}
	if ( CurFileInfo ) {
		AudioFileClose( CurFileInfo->mAFID);
		delete CurFileInfo;
		CurFileInfo = NULL;
	}
	if (mPacketDescs) {
		delete[] mPacketDescs;
		mPacketDescs = NULL;
	}
}


void BackgroundTrackMgr::QueueCallback( void * inUserData, AudioQueueRef inAQ, AudioQueueBufferRef inCompleteAQBuffer ) {
	// dispose of the buffer if no longer in use
	OSStatus result = noErr;
	BackgroundTrackMgr *THIS = (BackgroundTrackMgr*)inUserData;
	UInt32 nPackets = 0;
	// loop the current buffer if the following:
	// 1. file was loaded into the buffer previously
	// 2. only one file in the queue
	// 3. we have not been told to stop at playlist completion
	if ((CurFileInfo->mFileDataInQueue) && (!THIS->mStopAtEnd)) {
		nPackets = THIS->mNumPacketsToRead;
	} else {
		UInt32 numBytes;
		while (nPackets == 0) {
			// if loadAtOnce, get all packets in the file, otherwise ~.5 seconds of data
			nPackets = THIS->mNumPacketsToRead;					
			result = AudioFileReadPackets(CurFileInfo->mAFID, false, &numBytes, THIS->mPacketDescs, THIS->mCurrentPacket, &nPackets, 
										  inCompleteAQBuffer->mAudioData);
			AssertNoError("Error reading file data", end);
			
			inCompleteAQBuffer->mAudioDataByteSize = numBytes;	
			
			if (nPackets == 0) { // no packets were read, this file has ended.
				if (CurFileInfo->mLoadAtOnce) {
					CurFileInfo->mFileDataInQueue = true;
				}
				
				THIS->mCurrentPacket = 0;
				
				// we have gone through the playlist. if mStopAtEnd, stop the queue here
				if ( THIS->mStopAtEnd ) {
					result = AudioQueueStop(inAQ, false);
					AssertNoError("Error stopping queue", end);
					return;
				}
			}
		}
	}
	
	result = AudioQueueEnqueueBuffer(inAQ, inCompleteAQBuffer, (THIS->mPacketDescs ? nPackets : 0), THIS->mPacketDescs);
    if(result != noErr) {
        result = AudioQueueFreeBuffer(inAQ, inCompleteAQBuffer);
		AssertNoError("Error freeing buffers that didn't enqueue", end);
    }
	AssertNoError("Error enqueuing new buffer", end);
	if (CurFileInfo->mLoadAtOnce) {
		CurFileInfo->mFileDataInQueue = true;
	}
	
	THIS->mCurrentPacket += nPackets;
	
end:
	return;
}

OSStatus BackgroundTrackMgr::SetupQueue(BG_FileInfo *inFileInfo) {
	UInt32 size = 0;
	OSStatus result = AudioQueueNewOutput(&inFileInfo->mFileFormat, QueueCallback, this, 
										  CFRunLoopGetMain() /* CFRunLoopGetCurrent() */, kCFRunLoopCommonModes, 0, &mQueue);
	AssertNoError("Error creating queue", end);
#if 0	
	// (2) If the file has a cookie, we should get it and set it on the AQ
	size = sizeof(UInt32);
	result = AudioFileGetPropertyInfo (inFileInfo->mAFID, kAudioFilePropertyMagicCookieData, &size, NULL);
	
	if (!result && size) {
		char* cookie = new char [size];		
		result = AudioFileGetProperty (inFileInfo->mAFID, kAudioFilePropertyMagicCookieData, &size, cookie);
		AssertNoError("Error getting magic cookie", end);
		result = AudioQueueSetProperty(mQueue, kAudioQueueProperty_MagicCookie, cookie, size);
		delete [] cookie;
		AssertNoError("Error setting magic cookie", end);
	}
#endif
	// channel layout
	OSStatus err = AudioFileGetPropertyInfo(inFileInfo->mAFID, kAudioFilePropertyChannelLayout, &size, NULL);
	if (err == noErr && size > 0) {
		AudioChannelLayout *acl = (AudioChannelLayout *)malloc(size);
		result = AudioFileGetProperty(inFileInfo->mAFID, kAudioFilePropertyChannelLayout, &size, acl);
		AssertNoError("Error getting channel layout from file", end);
		result = AudioQueueSetProperty(mQueue, kAudioQueueProperty_ChannelLayout, acl, size);
		free(acl);
		AssertNoError("Error setting channel layout on queue", end);
	}
	
	// volume
	result = SetVolume(mVolume);
	
end:
	return result;
}

OSStatus BackgroundTrackMgr::SetupBuffers(BG_FileInfo *inFileInfo) {
	int numBuffersToQueue = kNumberBuffers;
	UInt32 maxPacketSize;
	UInt32 size = sizeof(maxPacketSize);
	// we need to calculate how many packets we read at a time, and how big a buffer we need
	// we base this on the size of the packets in the file and an approximate duration for each buffer
	
	// first check to see what the max size of a packet is - if it is bigger
	// than our allocation default size, that needs to become larger
	OSStatus result = AudioFileGetProperty(inFileInfo->mAFID, kAudioFilePropertyPacketSizeUpperBound, &size, &maxPacketSize);
	AssertNoError("Error getting packet upper bound size", end);
	bool isFormatVBR = (inFileInfo->mFileFormat.mBytesPerPacket == 0 || inFileInfo->mFileFormat.mFramesPerPacket == 0);
	
	CalculateBytesForTime(inFileInfo->mFileFormat, maxPacketSize, 0.5/*seconds*/, &mBufferByteSize, &mNumPacketsToRead);
	
	// if the file is smaller than the capacity of all the buffer queues, always load it at once
	if ((mBufferByteSize * numBuffersToQueue) > inFileInfo->mFileDataSize) {
		inFileInfo->mLoadAtOnce = true;
	}
	
	if (inFileInfo->mLoadAtOnce) {
		UInt64 theFileNumPackets;
		size = sizeof(UInt64);
		result = AudioFileGetProperty(inFileInfo->mAFID, kAudioFilePropertyAudioDataPacketCount, &size, &theFileNumPackets);
		AssertNoError("Error getting packet count for file", end);
		
		mNumPacketsToRead = (UInt32)theFileNumPackets;
		mBufferByteSize = (UInt32)inFileInfo->mFileDataSize;
		numBuffersToQueue = 1;
	} else {
		mNumPacketsToRead = mBufferByteSize / maxPacketSize;
	}
	
	if (isFormatVBR) {
		mPacketDescs = new AudioStreamPacketDescription [mNumPacketsToRead];
	} else {
		mPacketDescs = NULL; // we don't provide packet descriptions for constant bit rate formats (like linear PCM)	
	}
	
	// allocate the queue's buffers
	for (int i = 0; i < numBuffersToQueue; ++i) {
		result = AudioQueueAllocateBuffer(mQueue, mBufferByteSize, &mBuffers[i]);
		AssertNoError("Error allocating buffer for queue", end);
		QueueCallback (this, mQueue, mBuffers[i]);
		if (inFileInfo->mLoadAtOnce) {
			inFileInfo->mFileDataInQueue = true;
		}
	}
	
end:
	return result;
}

OSStatus BackgroundTrackMgr::LoadTrack(const char* inFilePath, Boolean inAddToQueue, Boolean inLoadAtOnce) {
//	OSStatus result = LoadFileDataInfo(CurFileInfo->mFilePath.c_str(), CurFileInfo->mAFID, CurFileInfo->mFileFormat, CurFileInfo->mFileDataSize);
//	AssertNoError("Error getting file data info", fail);
	OSStatus result;
	UInt32 thePropSize;
	
	CFURLRef theURL = CFURLCreateFromFileSystemRepresentation(kCFAllocatorDefault, (UInt8*)inFilePath, strlen(inFilePath), false);
	if (theURL == NULL)
		result = kSoundEngineErrFileNotFound;
	else
		result = 0;
	AssertNoError("Error opening URL", fail);
	
	CurFileInfo = new BG_FileInfo;
	CurFileInfo->mFilePath = inFilePath;
	
	result = AudioFileOpenURL(theURL, kAudioFileReadPermission, 0, &CurFileInfo->mAFID);
	CFRelease(theURL);
	AssertNoError("Error opening file", fail);
	
	thePropSize = sizeof(CurFileInfo->mFileFormat);
	result = AudioFileGetProperty(CurFileInfo->mAFID, kAudioFilePropertyDataFormat, &thePropSize, &CurFileInfo->mFileFormat);
	AssertNoError("Error getting file format", fail);
	
	thePropSize = sizeof(UInt64);
	result = AudioFileGetProperty(CurFileInfo->mAFID, kAudioFilePropertyAudioDataByteCount, &thePropSize, &CurFileInfo->mFileDataSize);
	AssertNoError("Error getting file data size", fail);
	
	CurFileInfo->mLoadAtOnce = inLoadAtOnce;
	CurFileInfo->mFileDataInQueue = false;
	
	result = SetupQueue(CurFileInfo);
	AssertNoError("Error setting up queue", fail);
	
	result = SetupBuffers(CurFileInfo);
	AssertNoError("Error setting up queue buffers", fail);					
	
	return result;
	
fail:
	if (CurFileInfo) {
		delete CurFileInfo;
		CurFileInfo = NULL;
	}
	return result;
}

OSStatus BackgroundTrackMgr::SetVolume(Float32 inVolume) {
	mVolume = inVolume;
	return AudioQueueSetParameter(mQueue, kAudioQueueParam_Volume, mVolume * gMasterVolumeGain);
}

Float32 BackgroundTrackMgr::GetVolume() const {
	return mVolume;
}

OSStatus BackgroundTrackMgr::Start() {
	OSStatus result = AudioQueuePrime(mQueue, 1, NULL);	
	if (result)	{
		printf("BackgroundTrackMgr: Error priming queue: %d\n", (int)result);
		return result;
	}
	return AudioQueueStart(mQueue, NULL);
}

OSStatus BackgroundTrackMgr::Stop(Boolean inStopAtEnd) {
	if (inStopAtEnd) {
		mStopAtEnd = true;
		return noErr;
	} else {
		return AudioQueueStop(mQueue, true);
	}
}


static BackgroundTrackMgr	sBackgroundTrackMgr;

static char currentMusicName[1024];

void iphonePauseMusic() {
	if ( music->value == 0 ) {
		// music is disabled
		return;
	}
	AudioQueuePause(sBackgroundTrackMgr.mQueue);
}
void iphoneResumeMusic() {
	if ( music->value == 0 ) {
		// music is disabled
		return;
	}
	AudioQueueStart(sBackgroundTrackMgr.mQueue,NULL);
}
void iphoneStopMusic() {
	sBackgroundTrackMgr.Teardown();
}

void iphoneStartMusic() {
	if ( music->value == 0 ) {
		// music is disabled
		return;
	}
	char	fullName[1024];
	sprintf( fullName, "%s/base/music/d_%s.mp3", SysIphoneGetAppDir(), currentMusicName );
	
	printf( "Starting music '%s'\n", fullName );

	iphoneStopMusic();
	sBackgroundTrackMgr.LoadTrack( fullName, false, true);
	sBackgroundTrackMgr.Start();
	
	if ( !strcmp( currentMusicName, "intro" ) ) {
		// stop the intro music at end, don't loop
		sBackgroundTrackMgr.mStopAtEnd = true;
	} else {
		sBackgroundTrackMgr.mStopAtEnd = false;
	}
}

void iphonePlayMusic( const char *name ) {
	strcpy( currentMusicName, name );
	
	iphoneStartMusic();
}




