/*
    SDL - Simple DirectMedia Layer
    Copyright (C) 1997, 1998, 1999, 2000, 2001, 2002  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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

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

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

/* 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;
static SDL_CD *theCDROM;

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

#pragma mark -- Prototypes --

static OSStatus CheckInit();

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

static int RunCallBackThread(void *inRefCon);


#pragma mark -- Public Functions --

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

void
Unlock()
{
    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;
    SInt16 forkRefNum;
    SInt64 forkSize;
    Ptr forkData = 0;
    ByteCount actualRead;
    CFDataRef dataRef = 0;
    CFPropertyListRef propertyListRef = 0;

    FSRefParam fsRefPB;
    FSRef tocPlistFSRef;

    const char *error = "Unspecified Error";

    /* get stuff from .TOC.plist */
    fsRefPB.ioCompletion = NULL;
    fsRefPB.ioNamePtr = "\p.TOC.plist";
    fsRefPB.ioVRefNum = theVolume;
    fsRefPB.ioDirID = 0;
    fsRefPB.newRef = &tocPlistFSRef;

    theErr = PBMakeFSRefSync(&fsRefPB);
    if (theErr != noErr) {
        error = "PBMakeFSRefSync";
        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;

        CFDataRef theRawTOCDataRef;
        CFArrayRef theSessionArrayRef;
        CFIndex numSessions;
        CFIndex index;

        /* This is how we get the Raw TOC Data */
        theRawTOCDataRef =
            (CFDataRef) 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 sessionNumber;
                CFNumberRef startBlock;
                CFBooleanRef isDataTrack;
                UInt32 value;

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

                trackNumber =
                    (CFNumberRef) CFDictionaryGetValue(theTrackDict,
                                                       CFSTR
                                                       (kPointKeyString));
                sessionNumber =
                    (CFNumberRef) 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()
{
    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()
{
    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()
{
    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()
{
    int frame;

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

    return frame;
}


#pragma mark -- Private Functions --

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

    OSStatus result = noErr;

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

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

    {                           /*try { */
        ComponentDescription desc;

        desc.componentType = kAudioUnitComponentType;
        desc.componentSubType = kAudioUnitSubType_Output;
        desc.componentManufacturer = kAudioUnitID_DefaultOutput;
        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;
}

/*}; // extern "C" */
/* vi: set ts=4 sw=4 expandtab: */
