/*
    SDL - Simple DirectMedia Layer
    Copyright (C) 1997-2012 Sam Lantinga

    This library is free software; you can redistribute it and/or
    modify it under the terms of the GNU Library General Public
    License as published by the Free Software Foundation; either
    version 2 of the License, or (at your option) any later version.

    This library 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
    Library General Public License for more details.

    You should have received a copy of the GNU Library General Public
    License along with this library; if not, write to the Free
    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335 USA  USA

    Sam Lantinga
    slouken@libsdl.org
*/
#include "../SDL_config.h"

#if defined(MACOSX) && defined(__clang__)

#include "CDPlayer.h"
#include "AudioFilePlayer.h"
#include "SDLOSXCAGuard.h"

#if defined(__clang__)
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
#pragma clang diagnostic ignored "-Wsometimes-uninitialized"
#pragma clang diagnostic ignored "-Wunused-parameter"
#else
#pragma GCC diagnostic ignored "-Wmaybe-uninitialized"
#endif
#pragma GCC diagnostic ignored "-Wformat"

/* we're exporting these functions into C land for SDL_syscdrom.c */
/*extern "C" {*/

/*///////////////////////////////////////////////////////////////////////////
    Constants
  //////////////////////////////////////////////////////////////////////////*/

#define kAudioCDFilesystemID   (UInt16)(('J' << 8) | 'H') /* 'JH'; this avoids compiler warning */

/* XML PList keys */
#define kRawTOCDataString           "Format 0x02 TOC Data"
#define kSessionsString             "Sessions"
#define kSessionTypeString          "Session Type"
#define kTrackArrayString           "Track Array"
#define kFirstTrackInSessionString      "First Track"
#define kLastTrackInSessionString       "Last Track"
#define kLeadoutBlockString         "Leadout Block"
#define kDataKeyString              "Data"
#define kPointKeyString             "Point"
#define kSessionNumberKeyString         "Session Number"
#define kStartBlockKeyString            "Start Block"   
    
/*///////////////////////////////////////////////////////////////////////////
    Globals
  //////////////////////////////////////////////////////////////////////////*/

#pragma mark -- Globals --

static int playBackWasInit = 0;
static AudioUnit theUnit;
static AudioFilePlayer* thePlayer            = NULL;
static CDPlayerCompletionProc completionProc = NULL;
static SDL_mutex* apiMutex                   = NULL;
static SDL_sem* callbackSem                  = NULL;
static SDL_CD* theCDROM                      = NULL;

/*///////////////////////////////////////////////////////////////////////////
    Prototypes
  //////////////////////////////////////////////////////////////////////////*/

#pragma mark -- Prototypes --

static OSStatus CheckInit (void);

static void     FilePlayNotificationHandler (void* inRefCon, OSStatus inStatus);

static int      RunCallBackThread (void* inRefCon);


void Lock(void)
{
	if (!apiMutex) {
		apiMutex = SDL_CreateMutex();
	}
	SDL_mutexP(apiMutex);
}

void Unlock(void)
{
	SDL_mutexV(apiMutex);
}

int DetectAudioCDVolumes(FSVolumeRefNum* volumes, int numVolumes)
{
	int volumeIndex;
	int cdVolumeCount = 0;
	OSStatus result   = noErr;

	for (volumeIndex = 1; result == noErr || result != nsvErr; volumeIndex++) {
		FSVolumeRefNum actualVolume;
		FSVolumeInfo volumeInfo;

		memset(&volumeInfo, 0, sizeof(volumeInfo));

		result = FSGetVolumeInfo(kFSInvalidVolumeRefNum,
		                         volumeIndex,
		                         &actualVolume,
		                         kFSVolInfoFSInfo,
		                         &volumeInfo,
		                         NULL,
		                         NULL);

		if (result == noErr) {
			if (volumeInfo.filesystemID == kAudioCDFilesystemID) /* It's an audio CD */
			{
				if (volumes != NULL && cdVolumeCount < numVolumes) {
					volumes[cdVolumeCount] = actualVolume;
				}

				cdVolumeCount++;
			}
		} else {
			/* I'm commenting this out because it seems to be
			 * harmless */
			/*SDL_SetError ("DetectAudioCDVolumes: FSGetVolumeInfo
			 * returned %d", result);*/
		}
	}

	return cdVolumeCount;
}

int ReadTOCData(FSVolumeRefNum theVolume, SDL_CD* theCD)
{
	HFSUniStr255 dataForkName;
	OSStatus theErr;
	FSIORefNum forkRefNum;
	SInt64 forkSize;
	Ptr forkData = 0;
	ByteCount actualRead;
	CFDataRef dataRef                 = 0;
	CFPropertyListRef propertyListRef = 0;
	FSRefParam fsRefPB;
	FSRef tocPlistFSRef;
	FSRef rootRef;
	const char* error = "Unspecified Error";
	const UniChar uniName[] = {'.', 'T', 'O', 'C', '.', 'p', 'l', 'i', 's', 't'};

	theErr = FSGetVolumeInfo(theVolume, 0, 0, kFSVolInfoNone, 0, 0, &rootRef);
	if (theErr != noErr) {
		error = "FSGetVolumeInfo";
		goto bail;
	}

	SDL_memset(&fsRefPB, '\0', sizeof(fsRefPB));

	/* get stuff from .TOC.plist */
	fsRefPB.ref              = &rootRef;
	fsRefPB.newRef           = &tocPlistFSRef;
	fsRefPB.nameLength       = sizeof(uniName) / sizeof(uniName[0]);
	fsRefPB.name             = uniName;
	fsRefPB.textEncodingHint = kTextEncodingUnknown;

	theErr = PBMakeFSRefUnicodeSync(&fsRefPB);
	if (theErr != noErr) {
		error = "PBMakeFSRefUnicodeSync";
		goto bail;
	}

	/* Load and parse the TOC XML data */

	theErr = FSGetDataForkName(&dataForkName);
	if (theErr != noErr) {
		error = "FSGetDataForkName";
		goto bail;
	}

	theErr = FSOpenFork(&tocPlistFSRef,
	                    dataForkName.length,
	                    dataForkName.unicode,
	                    fsRdPerm,
	                    &forkRefNum);
	if (theErr != noErr) {
		error = "FSOpenFork";
		goto bail;
	}

	theErr = FSGetForkSize(forkRefNum, &forkSize);
	if (theErr != noErr) {
		error = "FSGetForkSize";
		goto bail;
	}

	/* Allocate some memory for the XML data */
	forkData = NewPtr(forkSize);
	if (forkData == NULL) {
		error = "NewPtr";
		goto bail;
	}

	theErr = FSReadFork(forkRefNum,
	                    fsFromStart,
	                    0 /* offset location */,
	                    forkSize,
	                    forkData,
	                    &actualRead);
	if (theErr != noErr) {
		error = "FSReadFork";
		goto bail;
	}

	dataRef = CFDataCreate(kCFAllocatorDefault, (UInt8*)forkData, forkSize);
	if (dataRef == 0) {
		error = "CFDataCreate";
		goto bail;
	}

	propertyListRef = CFPropertyListCreateFromXMLData(kCFAllocatorDefault,
	                                                  dataRef,
	                                                  kCFPropertyListImmutable,
	                                                  NULL);
	if (propertyListRef == NULL) {
		error = "CFPropertyListCreateFromXMLData";
		goto bail;
	}

	/* Now we got the Property List in memory. Parse it. */

	/* First, make sure the root item is a CFDictionary. If not, release and
	 * bail. */
	if (CFGetTypeID(propertyListRef) == CFDictionaryGetTypeID()) {
		CFDictionaryRef dictRef = (CFDictionaryRef)propertyListRef;

		CFArrayRef theSessionArrayRef;
		CFIndex numSessions;
		CFIndex index;

		/* This is how we get the Raw TOC Data */
		(void /* unused rawTOC rvalue */)
		        CFDictionaryGetValue(dictRef, CFSTR(kRawTOCDataString));

		/* Get the session array info. */
		theSessionArrayRef = (CFArrayRef)
		        CFDictionaryGetValue(dictRef, CFSTR(kSessionsString));

		/* Find out how many sessions there are. */
		numSessions = CFArrayGetCount(theSessionArrayRef);

		/* Initialize the total number of tracks to 0 */
		theCD->numtracks = 0;

		/* Iterate over all sessions, collecting the track data */
		for (index = 0; index < numSessions; index++) {
			CFDictionaryRef theSessionDict;
			CFNumberRef leadoutBlock;
			CFArrayRef trackArray;
			CFIndex numTracks;
			CFIndex trackIndex;
			UInt32 value = 0;

			theSessionDict = (CFDictionaryRef)
			        CFArrayGetValueAtIndex(theSessionArrayRef, index);
			leadoutBlock = (CFNumberRef)CFDictionaryGetValue(
			        theSessionDict, CFSTR(kLeadoutBlockString));

			trackArray = (CFArrayRef)CFDictionaryGetValue(
			        theSessionDict, CFSTR(kTrackArrayString));

			numTracks = CFArrayGetCount(trackArray);

			for (trackIndex = 0; trackIndex < numTracks; trackIndex++) {
				CFDictionaryRef theTrackDict;
				CFNumberRef trackNumber;
				CFNumberRef startBlock;
				CFBooleanRef isDataTrack;
				UInt32 value;

				theTrackDict = (CFDictionaryRef)
				        CFArrayGetValueAtIndex(trackArray, trackIndex);

				trackNumber = (CFNumberRef)CFDictionaryGetValue(
				        theTrackDict, CFSTR(kPointKeyString));
				(void /* unused sessionNum*/) CFDictionaryGetValue(
				        theTrackDict, CFSTR(kSessionNumberKeyString));
				startBlock = (CFNumberRef)CFDictionaryGetValue(
				        theTrackDict, CFSTR(kStartBlockKeyString));
				isDataTrack = (CFBooleanRef)CFDictionaryGetValue(
				        theTrackDict, CFSTR(kDataKeyString));

				/* Fill in the SDL_CD struct */
				int idx = theCD->numtracks++;

				CFNumberGetValue(trackNumber,
				                 kCFNumberSInt32Type,
				                 &value);
				theCD->track[idx].id = value;

				CFNumberGetValue(startBlock, kCFNumberSInt32Type, &value);
				theCD->track[idx].offset = value;

				theCD->track[idx].type = (isDataTrack == kCFBooleanTrue)
				                               ? SDL_DATA_TRACK
				                               : SDL_AUDIO_TRACK;

				/* Since the track lengths are not stored in
				 * .TOC.plist we compute them. */
				if (trackIndex > 0) {
					theCD->track[idx - 1].length =
					        theCD->track[idx].offset -
					        theCD->track[idx - 1].offset;
				}
			}

			/* Compute the length of the last track */
			CFNumberGetValue(leadoutBlock, kCFNumberSInt32Type, &value);

			theCD->track[theCD->numtracks - 1].length =
			        value - theCD->track[theCD->numtracks - 1].offset;

			/* Set offset to leadout track */
			theCD->track[theCD->numtracks].offset = value;
		}
	}

	theErr = 0;
	goto cleanup;
bail:
	SDL_SetError("ReadTOCData: %s returned %d", error, theErr);
	theErr = -1;
cleanup:

	if (propertyListRef != NULL) {
		CFRelease(propertyListRef);
	}
	if (dataRef != NULL) {
		CFRelease(dataRef);
	}
	if (forkData != NULL) {
		DisposePtr(forkData);
	}

	FSCloseFork(forkRefNum);

	return theErr;
}

int ListTrackFiles(FSVolumeRefNum theVolume, FSRef* trackFiles, int numTracks)
{
	OSStatus result = -1;
	FSIterator iterator;
	ItemCount actualObjects;
	FSRef rootDirectory;
	FSRef ref;
	HFSUniStr255 nameStr;

	result = FSGetVolumeInfo(
	        theVolume, 0, NULL, kFSVolInfoFSInfo, NULL, NULL, &rootDirectory);

	if (result != noErr) {
		SDL_SetError("ListTrackFiles: FSGetVolumeInfo returned %d", result);
		return result;
	}

	result = FSOpenIterator(&rootDirectory, kFSIterateFlat, &iterator);
	if (result == noErr) {
		do {
			result = FSGetCatalogInfoBulk(iterator,
			                              1,
			                              &actualObjects,
			                              NULL,
			                              kFSCatInfoNone,
			                              NULL,
			                              &ref,
			                              NULL,
			                              &nameStr);
			if (result == noErr) {
				CFStringRef name;
				name = CFStringCreateWithCharacters(NULL,
				                                    nameStr.unicode,
				                                    nameStr.length);

				/* Look for .aiff extension */
				if (CFStringHasSuffix(name, CFSTR(".aiff")) ||
				    CFStringHasSuffix(name, CFSTR(".cdda"))) {
					/* Extract the track id from the
					 * filename */
					int trackID = 0, i = 0;
					while (i < nameStr.length &&
					       !isdigit(nameStr.unicode[i])) {
						++i;
					}
					while (i < nameStr.length &&
					       isdigit(nameStr.unicode[i])) {
						trackID = 10 * trackID +
						          (nameStr.unicode[i] - '0');
						++i;
					}

#	if DEBUG_CDROM
					printf("Found AIFF for track %d: '%s'\n",
					       trackID,
					       CFStringGetCStringPtr(
					               name,
					               CFStringGetSystemEncoding()));
#	endif

					/* Track ID's start at 1, but we want to
					 * start at 0 */
					trackID--;

					assert(0 <= trackID &&
					       trackID <= SDL_MAX_TRACKS);

					if (trackID < numTracks) {
						memcpy(&trackFiles[trackID],
						       &ref,
						       sizeof(FSRef));
					}
				}
				CFRelease(name);
			}
		} while (noErr == result);
		FSCloseIterator(iterator);
	}

	return 0;
}

int LoadFile(const FSRef* ref, int startFrame, int stopFrame)
{
	int error = -1;

	if (CheckInit() < 0) {
		goto bail;
	}

	/* release any currently playing file */
	if (ReleaseFile() < 0) {
		goto bail;
	}

#	if DEBUG_CDROM
	printf("LoadFile: %d %d\n", startFrame, stopFrame);
#	endif

	/*try {*/

	/* create a new player, and attach to the audio unit */

	thePlayer = new_AudioFilePlayer(ref);
	if (thePlayer == NULL) {
		SDL_SetError("LoadFile: Could not create player");
		return -3; /*throw (-3);*/
	}

	if (!thePlayer->SetDestination(thePlayer, &theUnit)) {
		goto bail;
	}

	if (startFrame >= 0) {
		thePlayer->SetStartFrame(thePlayer, startFrame);
	}

	if (stopFrame >= 0 && stopFrame > startFrame) {
		thePlayer->SetStopFrame(thePlayer, stopFrame);
	}

	/* we set the notifier later */
	/*thePlayer->SetNotifier(thePlayer, FilePlayNotificationHandler, NULL);*/

	if (!thePlayer->Connect(thePlayer)) {
		goto bail;
	}

#	if DEBUG_CDROM
	thePlayer->Print(thePlayer);
	fflush(stdout);
#	endif
	/*}
	  catch (...)
	  {
	      goto bail;
	  }*/

	error = 0;

bail:
	return error;
}

int ReleaseFile(void)
{
	int error = -1;

	/* (Don't see any way that the original C++ code could throw here.)
	 * --ryan. */
	/*try {*/
	if (thePlayer != NULL) {
		thePlayer->Disconnect(thePlayer);

		delete_AudioFilePlayer(thePlayer);

		thePlayer = NULL;
	}
	/*}
	  catch (...)
	  {
	      goto bail;
	  }*/

	error = 0;

	/*  bail: */
	return error;
}

int PlayFile(void)
{
	OSStatus result = -1;

	if (CheckInit() < 0) {
		goto bail;
	}

	/*try {*/

	// start processing of the audio unit
	result = AudioOutputUnitStart(theUnit);
	if (result) {
		goto bail; // THROW_RESULT("PlayFile: AudioOutputUnitStart")
	}

	/*}
	catch (...)
	{
	    goto bail;
	}*/

	result = 0;

bail:
	return result;
}

int PauseFile(void)
{
	OSStatus result = -1;

	if (CheckInit() < 0) {
		goto bail;
	}

	/*try {*/

	/* stop processing the audio unit */
	result = AudioOutputUnitStop(theUnit);
	if (result) {
		goto bail; /*THROW_RESULT("PauseFile: AudioOutputUnitStop")*/
	}
	/*}
	  catch (...)
	  {
	      goto bail;
	  }*/

	result = 0;
bail:
	return result;
}

void SetCompletionProc(CDPlayerCompletionProc proc, SDL_CD* cdrom)
{
	assert(thePlayer != NULL);

	theCDROM       = cdrom;
	completionProc = proc;
	thePlayer->SetNotifier(thePlayer, FilePlayNotificationHandler, cdrom);
}

int GetCurrentFrame(void)
{
	int frame;

	if (thePlayer == NULL) {
		frame = 0;
	} else {
		frame = thePlayer->GetCurrentFrame(thePlayer);
	}

	return frame;
}

#	pragma mark-- Private Functions --

static OSStatus CheckInit(void)
{
	if (playBackWasInit) {
		return 0;
	}

	OSStatus result = noErr;

	/* Create the callback semaphore */
	callbackSem = SDL_CreateSemaphore(0);

	/* Start callback thread */
	SDL_CreateThread(RunCallBackThread, "CDPlayer", NULL);

	{ /*try {*/
		ComponentDescription desc;

		desc.componentType         = kAudioUnitType_Output;
		desc.componentSubType      = kAudioUnitSubType_DefaultOutput;
		desc.componentManufacturer = kAudioUnitManufacturer_Apple;
		desc.componentFlags        = 0;
		desc.componentFlagsMask    = 0;

		Component comp = FindNextComponent(NULL, &desc);
		if (comp == NULL) {
			SDL_SetError("CheckInit: FindNextComponent returned NULL");
			if (result) {
				return -1; // throw(internalComponentErr);
			}
		}

		result = OpenAComponent(comp, &theUnit);
		if (result) {
			return -1; // THROW_RESULT("CheckInit: OpenAComponent")
		}

		// you need to initialize the output unit before you set it as a
		// destination
		result = AudioUnitInitialize(theUnit);
		if (result) {
			return -1; // THROW_RESULT("CheckInit:
			           // AudioUnitInitialize")
		}

		playBackWasInit = true;
	}
	/*catch (...)
	  {
	      return -1;
	  }*/

	return 0;
}

static void FilePlayNotificationHandler(void* inRefCon, OSStatus inStatus)
{
	if (inStatus == kAudioFilePlay_FileIsFinished) {
		/* notify non-CA thread to perform the callback */
		SDL_SemPost(callbackSem);

	} else if (inStatus == kAudioFilePlayErr_FilePlayUnderrun) {
		SDL_SetError("CDPlayer Notification: buffer underrun");
	} else if (inStatus == kAudioFilePlay_PlayerIsUninitialized) {
		SDL_SetError("CDPlayer Notification: player is uninitialized");
	} else {
		SDL_SetError("CDPlayer Notification: unknown error %ld", inStatus);
	}
}

static int RunCallBackThread(void* param)
{
	for (;;) {
		SDL_SemWait(callbackSem);

		if (completionProc && theCDROM) {
#	if DEBUG_CDROM
			printf("callback!\n");
#	endif
			(*completionProc)(theCDROM);
		} else {
#	if DEBUG_CDROM
			printf("callback?\n");
#	endif
		}
	}

#	if DEBUG_CDROM
	printf("thread dying now...\n");
#	endif

	return 0;
}

#endif /* SDL_CDROM_MACOSX */

/*}; // extern "C" */
