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

    This library is free software; you can redistribute it and/or
    modify it under the terms of the GNU Lesser General Public
    License as published by the Free Software Foundation; either
    version 2.1 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
    Lesser General Public License for more details.

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

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

#ifdef SDL_CDROM_MACOS

/* MacOS functions for system-level CD-ROM audio control */

#include <Devices.h>
#include <Files.h>
#include <LowMem.h> /* Use entry table macros, not functions in InterfaceLib  */

#include "SDL_cdrom.h"
#include "../SDL_syscdrom.h"
#include "SDL_syscdrom_c.h"

/* Added by Matt Slot */
#if !defined(LMGetUnitTableEntryCount)
  #define LMGetUnitTableEntryCount()   *(short *)0x01D2
#endif

/* The maximum number of CD-ROM drives we'll detect */
#define MAX_DRIVES	26	

/* A list of available CD-ROM drives */
static long SDL_cdversion = 0;
static struct {
	short		dRefNum;
	short		driveNum;
	long		frames;
	char		name[256];
	Boolean		hasAudio;
	} SDL_cdlist[MAX_DRIVES];
static StringPtr gDriverName = "\p.AppleCD";

/* The system-dependent CD control functions */
static const char *SDL_SYS_CDName(int drive);
static int SDL_SYS_CDOpen(int drive);
static int SDL_SYS_CDGetTOC(SDL_CD *cdrom);
static CDstatus SDL_SYS_CDStatus(SDL_CD *cdrom, int *position);
static int SDL_SYS_CDPlay(SDL_CD *cdrom, int start, int length);
static int SDL_SYS_CDPause(SDL_CD *cdrom);
static int SDL_SYS_CDResume(SDL_CD *cdrom);
static int SDL_SYS_CDStop(SDL_CD *cdrom);
static int SDL_SYS_CDEject(SDL_CD *cdrom);
static void SDL_SYS_CDClose(SDL_CD *cdrom);

static short SDL_SYS_ShortToBCD(short value)
{
	return((value % 10) + (value / 10) * 0x10); /* Convert value to BCD */
}

static short SDL_SYS_BCDToShort(short value)
{
	return((value % 0x10) + (value / 0x10) * 10); /* Convert value from BCD */
}

int  SDL_SYS_CDInit(void)
{
	SInt16			dRefNum = 0;
	SInt16			first, last;

	SDL_numcds = 0;

	/* Check that the software is available */
	if (Gestalt(kGestaltAudioCDSelector, &SDL_cdversion) || 
			!SDL_cdversion) return(0);

	/* Fill in our driver capabilities */
	SDL_CDcaps.Name = SDL_SYS_CDName;
	SDL_CDcaps.Open = SDL_SYS_CDOpen;
	SDL_CDcaps.GetTOC = SDL_SYS_CDGetTOC;
	SDL_CDcaps.Status = SDL_SYS_CDStatus;
	SDL_CDcaps.Play = SDL_SYS_CDPlay;
	SDL_CDcaps.Pause = SDL_SYS_CDPause;
	SDL_CDcaps.Resume = SDL_SYS_CDResume;
	SDL_CDcaps.Stop = SDL_SYS_CDStop;
	SDL_CDcaps.Eject = SDL_SYS_CDEject;
	SDL_CDcaps.Close = SDL_SYS_CDClose;

	/* Walk the list, count each AudioCD driver, and save the refnums */
	first = -1;
	last = 0 - LMGetUnitTableEntryCount();
	for(dRefNum = first; dRefNum >= last; dRefNum--) {
		Str255		driverName;
		StringPtr	namePtr;
		DCtlHandle	deviceEntry;

		deviceEntry = GetDCtlEntry(dRefNum);
		if (! deviceEntry) continue;
		
		/* Is this an .AppleCD ? */
		namePtr = (*deviceEntry)->dCtlFlags & (1L << dRAMBased) ?
				((StringPtr) ((DCtlPtr) deviceEntry)->dCtlDriver + 18) :
				((StringPtr) (*deviceEntry)->dCtlDriver + 18);
		BlockMoveData(namePtr, driverName, namePtr[0]+1);
		if (driverName[0] > gDriverName[0]) driverName[0] = gDriverName[0];
		if (! EqualString(driverName, gDriverName, false, false)) continue;

		/* Record the basic info for each drive */
		SDL_cdlist[SDL_numcds].dRefNum = dRefNum;
		BlockMoveData(namePtr + 1, SDL_cdlist[SDL_numcds].name, namePtr[0]);
		SDL_cdlist[SDL_numcds].name[namePtr[0]] = 0;
		SDL_cdlist[SDL_numcds].hasAudio = false;
		SDL_numcds++;
	}
	return(0);
}

static const char *SDL_SYS_CDName(int drive)
{
	return(SDL_cdlist[drive].name);
}

static int get_drivenum(int drive)
{
	QHdr *driveQ = GetDrvQHdr();
	DrvQEl *driveElem;

	/* Update the drive number */
	SDL_cdlist[drive].driveNum = 0;
	if ( driveQ->qTail ) {
		driveQ->qTail->qLink = 0;
	}
	for ( driveElem=(DrvQEl *)driveQ->qHead; driveElem;
	      driveElem = (DrvQEl *)driveElem->qLink ) {
		if ( driveElem->dQRefNum == SDL_cdlist[drive].dRefNum ) {
			SDL_cdlist[drive].driveNum = driveElem->dQDrive;
			break;
		}
	}
	return(SDL_cdlist[drive].driveNum);
}

static int SDL_SYS_CDOpen(int drive)
{
	return(drive);
}

static int SDL_SYS_CDGetTOC(SDL_CD *cdrom)
{
	CDCntrlParam		cdpb;
	CDTrackData			tracks[SDL_MAX_TRACKS];
	long				i, leadout;

	/* Get the number of tracks on the CD by examining the TOC */
	SDL_memset(&cdpb, 0, sizeof(cdpb));
	cdpb.ioVRefNum = SDL_cdlist[cdrom->id].driveNum;
	cdpb.ioCRefNum = SDL_cdlist[cdrom->id].dRefNum;
	cdpb.csCode = kReadTOC;
	cdpb.csParam.words[0] = kGetTrackRange;
	if ( PBControlSync((ParmBlkPtr)&cdpb) != noErr ) {
		SDL_SetError("PBControlSync() failed");
		return(-1);
	}

	cdrom->numtracks = 
			SDL_SYS_BCDToShort(cdpb.csParam.bytes[1]) - 
			SDL_SYS_BCDToShort(cdpb.csParam.bytes[0]) + 1;
	if ( cdrom->numtracks > SDL_MAX_TRACKS )
		cdrom->numtracks = SDL_MAX_TRACKS;
	cdrom->status = CD_STOPPED;
	cdrom->cur_track = 0; /* Apparently these are set elsewhere */
	cdrom->cur_frame = 0; /* Apparently these are set elsewhere */


	/* Get the lead out area of the CD by examining the TOC */
	SDL_memset(&cdpb, 0, sizeof(cdpb));
	cdpb.ioVRefNum = SDL_cdlist[cdrom->id].driveNum;
	cdpb.ioCRefNum = SDL_cdlist[cdrom->id].dRefNum;
	cdpb.csCode = kReadTOC;
	cdpb.csParam.words[0] = kGetLeadOutArea;
	if ( PBControlSync((ParmBlkPtr)&cdpb) != noErr ) {
		SDL_SetError("PBControlSync() failed");
		return(-1);
	}

	leadout = MSF_TO_FRAMES(
			SDL_SYS_BCDToShort(cdpb.csParam.bytes[0]),
			SDL_SYS_BCDToShort(cdpb.csParam.bytes[1]),
			SDL_SYS_BCDToShort(cdpb.csParam.bytes[2]));

	/* Get an array of track locations by examining the TOC */
	SDL_memset(tracks, 0, sizeof(tracks));
	SDL_memset(&cdpb, 0, sizeof(cdpb));
	cdpb.ioVRefNum = SDL_cdlist[cdrom->id].driveNum;
	cdpb.ioCRefNum = SDL_cdlist[cdrom->id].dRefNum;
	cdpb.csCode = kReadTOC;
	cdpb.csParam.words[0] = kGetTrackEntries;	/* Type of Query */
	* ((long *) (cdpb.csParam.words+1)) = (long) tracks;				
	cdpb.csParam.words[3] = cdrom->numtracks * sizeof(tracks[0]);		
	* ((char *) (cdpb.csParam.words+4)) = 1;	/* First track */
	if ( PBControlSync((ParmBlkPtr)&cdpb) != noErr ) {
		SDL_SetError("PBControlSync() failed");
		return(-1);
	}

	/* Read all the track TOC entries */
	SDL_cdlist[cdrom->id].hasAudio = false;
	for ( i=0; i<cdrom->numtracks; ++i ) 
		{
		cdrom->track[i].id = i+1;
		if (tracks[i].entry.control & kDataTrackMask)
			cdrom->track[i].type = SDL_DATA_TRACK;
		else
			{
			cdrom->track[i].type = SDL_AUDIO_TRACK;
			SDL_cdlist[SDL_numcds].hasAudio = true;
			}
		
		cdrom->track[i].offset = MSF_TO_FRAMES(
				SDL_SYS_BCDToShort(tracks[i].entry.min),
				SDL_SYS_BCDToShort(tracks[i].entry.min),
				SDL_SYS_BCDToShort(tracks[i].entry.frame));
		cdrom->track[i].length = MSF_TO_FRAMES(
				SDL_SYS_BCDToShort(tracks[i+1].entry.min),
				SDL_SYS_BCDToShort(tracks[i+1].entry.min),
				SDL_SYS_BCDToShort(tracks[i+1].entry.frame)) -
				cdrom->track[i].offset;
		}
	
	/* Apparently SDL wants a fake last entry */
	cdrom->track[i].offset = leadout;
	cdrom->track[i].length = 0;

	return(0);
}

/* Get CD-ROM status */
static CDstatus SDL_SYS_CDStatus(SDL_CD *cdrom, int *position)
{
	CDCntrlParam cdpb;
	CDstatus status = CD_ERROR;
	Boolean spinning = false;

	if (position) *position = 0;

	/* Get the number of tracks on the CD by examining the TOC */
	if ( ! get_drivenum(cdrom->id) ) {
		return(CD_TRAYEMPTY);
	}
	SDL_memset(&cdpb, 0, sizeof(cdpb));
	cdpb.ioVRefNum = SDL_cdlist[cdrom->id].driveNum;
	cdpb.ioCRefNum = SDL_cdlist[cdrom->id].dRefNum;
	cdpb.csCode = kReadTOC;
	cdpb.csParam.words[0] = kGetTrackRange;
	if ( PBControlSync((ParmBlkPtr)&cdpb) != noErr ) {
		SDL_SetError("PBControlSync() failed");
		return(CD_ERROR);
	}

	cdrom->numtracks = 
			SDL_SYS_BCDToShort(cdpb.csParam.bytes[1]) - 
			SDL_SYS_BCDToShort(cdpb.csParam.bytes[0]) + 1;
	if ( cdrom->numtracks > SDL_MAX_TRACKS )
		cdrom->numtracks = SDL_MAX_TRACKS;
	cdrom->cur_track = 0; /* Apparently these are set elsewhere */
	cdrom->cur_frame = 0; /* Apparently these are set elsewhere */


	if (1 || SDL_cdlist[cdrom->id].hasAudio) {
		/* Get the current playback status */
		SDL_memset(&cdpb, 0, sizeof(cdpb));
		cdpb.ioVRefNum = SDL_cdlist[cdrom->id].driveNum;
		cdpb.ioCRefNum = SDL_cdlist[cdrom->id].dRefNum;
		cdpb.csCode = kAudioStatus;
		if ( PBControlSync((ParmBlkPtr)&cdpb) != noErr ) {
			SDL_SetError("PBControlSync() failed");
			return(-1);
		}
	
		switch(cdpb.csParam.cd.status) {
			case kStatusPlaying:
				status = CD_PLAYING;
				spinning = true;
				break;
			case kStatusPaused:
				status = CD_PAUSED;
				spinning = true;
				break;
			case kStatusMuted:
				status = CD_PLAYING; /* What should I do here? */
				spinning = true;
				break;
			case kStatusDone:
				status = CD_STOPPED;
				spinning = true;
				break;
			case kStatusStopped:
				status = CD_STOPPED;
				spinning = false;
				break;
			case kStatusError:
			default:
				status = CD_ERROR;
				spinning = false;
				break;
			}

		if (spinning && position) *position = MSF_TO_FRAMES(
				SDL_SYS_BCDToShort(cdpb.csParam.cd.minute),
				SDL_SYS_BCDToShort(cdpb.csParam.cd.second),
				SDL_SYS_BCDToShort(cdpb.csParam.cd.frame));
		}
	else
		status = CD_ERROR; /* What should I do here? */

	return(status);
}

/* Start play */
static int SDL_SYS_CDPlay(SDL_CD *cdrom, int start, int length)
{
	CDCntrlParam cdpb;

	/* Pause the current audio playback to avoid audible artifacts */
	if ( SDL_SYS_CDPause(cdrom) < 0 ) {
		return(-1);
	}

	/* Specify the AudioCD playback mode */
	SDL_memset(&cdpb, 0, sizeof(cdpb));
	cdpb.ioVRefNum = SDL_cdlist[cdrom->id].driveNum;
	cdpb.ioCRefNum = SDL_cdlist[cdrom->id].dRefNum;
	cdpb.csCode = kSetPlayMode;
	cdpb.csParam.bytes[0] = false;			/* Repeat? */
	cdpb.csParam.bytes[1] = kPlayModeSequential;	/* Play mode */
	/* Treat as soft error, NEC Drive doesnt support this call  */
	PBControlSync((ParmBlkPtr) &cdpb);

#if 1
	/* Specify the end of audio playback */
	SDL_memset(&cdpb, 0, sizeof(cdpb));
	cdpb.ioVRefNum = SDL_cdlist[cdrom->id].driveNum;
	cdpb.ioCRefNum = SDL_cdlist[cdrom->id].dRefNum;
	cdpb.csCode = kAudioStop;
	cdpb.csParam.words[0] = kBlockPosition;		/* Position Mode */
	*(long *) (cdpb.csParam.words + 1) = start+length-1; /* Search Address */
	if ( PBControlSync((ParmBlkPtr)&cdpb) != noErr ) {
		SDL_SetError("PBControlSync() failed");
		return(-1);
	}

	/* Specify the start of audio playback, and start it */
	SDL_memset(&cdpb, 0, sizeof(cdpb));
	cdpb.ioVRefNum = SDL_cdlist[cdrom->id].driveNum;
	cdpb.ioCRefNum = SDL_cdlist[cdrom->id].dRefNum;
	cdpb.csCode = kAudioPlay;
	cdpb.csParam.words[0] = kBlockPosition;			/* Position Mode */
	*(long *) (cdpb.csParam.words + 1) = start+1;	/* Search Address */
	cdpb.csParam.words[3] = false;					/* Stop address? */
	cdpb.csParam.words[4] = kStereoPlayMode;		/* Audio Play Mode */
	if ( PBControlSync((ParmBlkPtr)&cdpb) != noErr ) {
		SDL_SetError("PBControlSync() failed");
		return(-1);
	}
#else
	/* Specify the end of audio playback */
	FRAMES_TO_MSF(start+length, &m, &s, &f);
	SDL_memset(&cdpb, 0, sizeof(cdpb));
	cdpb.ioVRefNum = SDL_cdlist[cdrom->id].driveNum;
	cdpb.ioCRefNum = SDL_cdlist[cdrom->id].dRefNum;
	cdpb.csCode = kAudioStop;
	cdpb.csParam.words[0] = kTrackPosition;			/* Position Mode */
	cdpb.csParam.words[1] = 0;						/* Search Address (hiword)*/
	cdpb.csParam.words[2] = 						/* Search Address (loword)*/
			SDL_SYS_ShortToBCD(cdrom->numtracks);	
	if ( PBControlSync((ParmBlkPtr)&cdpb) != noErr ) {
		SDL_SetError("PBControlSync() failed");
		return(-1);
	}

	/* Specify the start of audio playback, and start it */
	FRAMES_TO_MSF(start, &m, &s, &f);
	SDL_memset(&cdpb, 0, sizeof(cdpb));
	cdpb.ioVRefNum = SDL_cdlist[cdrom->id].driveNum;
	cdpb.ioCRefNum = SDL_cdlist[cdrom->id].dRefNum;
	cdpb.csCode = kAudioPlay;
	cdpb.csParam.words[0] = kTrackPosition;			/* Position Mode */
	cdpb.csParam.words[1] = 0;						/* Search Address (hiword)*/
	cdpb.csParam.words[2] = SDL_SYS_ShortToBCD(1);	/* Search Address (loword)*/
	cdpb.csParam.words[3] = false;					/* Stop address? */
	cdpb.csParam.words[4] = kStereoPlayMode;		/* Audio Play Mode */
	if ( PBControlSync((ParmBlkPtr)&cdpb) != noErr ) {
		SDL_SetError("PBControlSync() failed");
		return(-1);
	}
#endif

	return(0);
}

/* Pause play */
static int SDL_SYS_CDPause(SDL_CD *cdrom)
{
	CDCntrlParam cdpb;

	SDL_memset(&cdpb, 0, sizeof(cdpb));
	cdpb.ioVRefNum = SDL_cdlist[cdrom->id].driveNum;
	cdpb.ioCRefNum = SDL_cdlist[cdrom->id].dRefNum;
	cdpb.csCode = kAudioPause;
	cdpb.csParam.words[0] = 0;	/* Pause/Continue Flag (hiword) */
	cdpb.csParam.words[1] = 1;	/* Pause/Continue Flag (loword) */
	if ( PBControlSync((ParmBlkPtr)&cdpb) != noErr ) {
		SDL_SetError("PBControlSync() failed");
		return(-1);
	}
	return(0);
}

/* Resume play */
static int SDL_SYS_CDResume(SDL_CD *cdrom)
{
	CDCntrlParam cdpb;

	SDL_memset(&cdpb, 0, sizeof(cdpb));
	cdpb.ioVRefNum = SDL_cdlist[cdrom->id].driveNum;
	cdpb.ioCRefNum = SDL_cdlist[cdrom->id].dRefNum;
	cdpb.csCode = kAudioPause;
	cdpb.csParam.words[0] = 0;	/* Pause/Continue Flag (hiword) */
	cdpb.csParam.words[1] = 0;	/* Pause/Continue Flag (loword) */
	if ( PBControlSync((ParmBlkPtr)&cdpb) != noErr ) {
		SDL_SetError("PBControlSync() failed");
		return(-1);
	}
	return(0);
}

/* Stop play */
static int SDL_SYS_CDStop(SDL_CD *cdrom)
{
	CDCntrlParam cdpb;

	SDL_memset(&cdpb, 0, sizeof(cdpb));
	cdpb.ioVRefNum = SDL_cdlist[cdrom->id].driveNum;
	cdpb.ioCRefNum = SDL_cdlist[cdrom->id].dRefNum;
	cdpb.csCode = kAudioStop;
	cdpb.csParam.words[0] = 0;		/* Position Mode */
	cdpb.csParam.words[1] = 0;		/* Search Address (hiword) */
	cdpb.csParam.words[2] = 0;		/* Search Address (loword) */
	if ( PBControlSync((ParmBlkPtr)&cdpb) != noErr ) {
		SDL_SetError("PBControlSync() failed");
		return(-1);
	}
	return(0);
}

/* Eject the CD-ROM */
static int SDL_SYS_CDEject(SDL_CD *cdrom)
{
	Boolean	disk = false;
	QHdr *driveQ = GetDrvQHdr();
	DrvQEl *driveElem;
	HParamBlockRec hpb;
	ParamBlockRec cpb;

	for ( driveElem = (DrvQEl *) driveQ->qHead; driveElem; driveElem = 
			  (driveElem) ? ((DrvQEl *) driveElem->qLink) : 
			  ((DrvQEl *) driveQ->qHead) ) {
		if ( driveQ->qTail ) {
			driveQ->qTail->qLink = 0;
		}
		if ( driveElem->dQRefNum != SDL_cdlist[cdrom->id].dRefNum ) {
			continue;
		}
	
		/* Does drive contain mounted volume? If not, skip */
		SDL_memset(&hpb, 0, sizeof(hpb));
		hpb.volumeParam.ioVRefNum = driveElem->dQDrive;
		if ( PBHGetVInfoSync(&hpb) != noErr ) {
			continue;
		}
		if ( (UnmountVol(0, driveElem->dQDrive) == noErr) && 
		     (Eject(0, driveElem->dQDrive) == noErr) ) {
			driveElem = 0; /* Clear pointer to reset our loop */
			disk = true;
		}
	}

	/* If no disk is present, just eject the tray */
	if (! disk) {
		SDL_memset(&cpb, 0, sizeof(cpb));
		cpb.cntrlParam.ioVRefNum = 0; /* No Drive */
		cpb.cntrlParam.ioCRefNum = SDL_cdlist[cdrom->id].dRefNum;
		cpb.cntrlParam.csCode = kEjectTheDisc;
		if ( PBControlSync((ParmBlkPtr)&cpb) != noErr ) {
			SDL_SetError("PBControlSync() failed");
			return(-1);
		}
	}
	return(0);
}

/* Close the CD-ROM handle */
static void SDL_SYS_CDClose(SDL_CD *cdrom)
{
	return;
}

void SDL_SYS_CDQuit(void)
{
	while(SDL_numcds--) 
		SDL_memset(SDL_cdlist + SDL_numcds, 0, sizeof(SDL_cdlist[0]));
}

#endif /* SDL_CDROM_MACOS */
