/*****************************************************************************
                    The Dark Mod GPL Source Code
 
 This file is part of the The Dark Mod Source Code, originally based 
 on the Doom 3 GPL Source Code as published in 2011.
 
 The Dark Mod Source Code 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 3 of the License, 
 or (at your option) any later version. For details, see LICENSE.TXT.
 
 Project: The Dark Mod (http://www.thedarkmod.com/)
 
 $Revision$ (Revision of last commit) 
 $Date$ (Date of last commit)
 $Author$ (Author of last commit)
 
******************************************************************************/

#include "../../idlib/precompiled.h"
#include "../../sound/snd_local.h"

#include <Carbon/Carbon.h>
#include <CoreAudio/CoreAudio.h>

idCVar s_device( "s_device", "-1", CVAR_SYSTEM | CVAR_ARCHIVE | CVAR_INTEGER, "Sound device to use. -1 for default device" );

class idAudioHardwareOSX : public idAudioHardware {
public:
	idAudioHardwareOSX();
    ~idAudioHardwareOSX();

    bool	Initialize( );

	// OSX driver doesn't support memory map API
	bool	Lock( void **pDSLockedBuffer, ulong *dwDSLockedBufferSize ) { return false; }
	bool	Unlock( void *pDSLockedBuffer, dword dwDSLockedBufferSize ) { return false; }
	bool	GetCurrentPosition( ulong *pdwCurrentWriteCursor ) { return false; }
	int		GetMixBufferSize( void )  { return 0; }
	
	int		GetNumberOfSpeakers( void );

	// OSX driver doesn't support write API
	bool	Flush( void ) { return false; }
	void	Write( bool ) { }
	short*	GetMixBuffer( void ) { return NULL; }
	
private:
	AudioDeviceID		selectedDevice;
	bool				activeIOProc;

	void				Reset( void );
	void				InitFailed( void );
	const char*			ExtractStatus( OSStatus status );
	void				GetAvailableNominalSampleRates( void );

	// AudioDevicePropertyListenerProc
	static OSStatus		DeviceListener(	AudioDeviceID			inDevice,
	   									UInt32					inChannel,
										Boolean					isInput,
										AudioDevicePropertyID	inPropertyID,
										void*					inClientData );

	// AudioDeviceIOProc
	static OSStatus		DeviceIOProc( AudioDeviceID				inDevice,
									  const AudioTimeStamp*		inNow,
									  const AudioBufferList*	inInputData,
									  const AudioTimeStamp*		inInputTime,
									  AudioBufferList*			outOutputData, 
									  const AudioTimeStamp*		inOutputTime,
									  void*						inClientData );
};

/*
==========
iAudioHardware::Alloc
==========
*/
idAudioHardware *idAudioHardware::Alloc() { return new idAudioHardwareOSX; }

/*
==========
idAudioHardware::~idAudioHardware
==========
*/
idAudioHardware::~idAudioHardware() { }

/*
==========
idAudioHardwareOSX::idAudioHardwareOSX
==========
*/
idAudioHardwareOSX::idAudioHardwareOSX() {
	selectedDevice = kAudioDeviceUnknown;
	activeIOProc = false;
}

/*
==========
idAudioHardwareOSX::~idAudioHardwareOSX
==========
*/
idAudioHardwareOSX::~idAudioHardwareOSX() {
	Reset();
}

/*
==========
idAudioHardwareOSX::Reset
==========
*/
void idAudioHardwareOSX::Reset() {
	OSStatus status;

	if ( activeIOProc ) {
		status = AudioDeviceStop( selectedDevice, DeviceIOProc );
		if ( status != kAudioHardwareNoError ) {
			common->Warning( "idAudioHardwareOSX::Reset: AudioDeviceStop failed. status: %s", ExtractStatus( status ) );
		}
		status = AudioDeviceRemoveIOProc( selectedDevice, DeviceIOProc );
		if ( status != kAudioHardwareNoError ) {
			common->Warning( "idAudioHardwareOSX::Reset: AudioDeviceRemoveIOProc failed. status %s", ExtractStatus( status ) );
		}
		activeIOProc = false;
	}
	selectedDevice = kAudioDeviceUnknown;
	AudioHardwareUnload();
}

/*
=================
idAudioHardwareOSX::InitFailed
=================	
*/	
void idAudioHardwareOSX::InitFailed() {
	Reset();
	cvarSystem->SetCVarBool( "s_noSound", true );
	common->Warning( "sound subsystem disabled" );
	common->Printf( "------------------------------------------------\n" );
}

/*
==========
idAudioHardwareOSX::DeviceListener
==========
*/
OSStatus idAudioHardwareOSX::DeviceListener(	AudioDeviceID			inDevice,
												UInt32					inChannel,
												Boolean					isInput,
												AudioDevicePropertyID	inPropertyID,
												void*					inClientData) {
	common->Printf( "DeviceListener\n" );
	return kAudioHardwareNoError;
}

/*
==========
idAudioHardwareOSX::DeviceIOProc
==========
*/
OSStatus idAudioHardwareOSX::DeviceIOProc( AudioDeviceID			inDevice,
										   const AudioTimeStamp*	inNow,
										   const AudioBufferList*	inInputData,
										   const AudioTimeStamp*	inInputTime,
										   AudioBufferList*			outOutputData, 
										   const AudioTimeStamp*	inOutputTime,
										   void*					inClientData ) {

	// setup similar to async thread
	Sys_EnterCriticalSection();
	soundSystem->AsyncMix( (int)inOutputTime->mSampleTime, (float*)outOutputData->mBuffers[ 0 ].mData );
	Sys_LeaveCriticalSection();

	// doom mixes sound to -32768.0f 32768.0f range, scale down to -1.0f 1.0f
	SIMDProcessor->Mul( (Float32*)outOutputData->mBuffers[ 0 ].mData, 1.0f / 32768.0f, (Float32*)outOutputData->mBuffers[ 0 ].mData, MIXBUFFER_SAMPLES * 2 );

	return kAudioHardwareNoError;
}

/*
==========
idAudioHardwareOSX::ExtractStatus
==========
*/
const char*	idAudioHardwareOSX::ExtractStatus( OSStatus status ) {
	static char buf[ sizeof( OSStatus ) + 1 ];
	strncpy( buf, (const char *)&status, sizeof( OSStatus ) );
	buf[ sizeof( OSStatus ) ] = '\0';
	return buf;
}

/*
==========
idAudioHardwareOSX::Initialize
==========
*/
bool idAudioHardwareOSX::Initialize( ) {

	UInt32			size;
	OSStatus		status;
	int				i, deviceCount;
	AudioDeviceID	*deviceList;
	char			buf[ 1024 ];

	status = AudioHardwareGetPropertyInfo( kAudioHardwarePropertyDevices, &size, NULL );
	if ( status != kAudioHardwareNoError ) {
		common->Warning( "AudioHardwareGetPropertyInfo kAudioHardwarePropertyDevices failed. status: %s", ExtractStatus( status ) );
		InitFailed();
		return false;
	}

	deviceCount = size / sizeof( AudioDeviceID );
	if ( !deviceCount ) {
		common->Printf( "No sound device found\n" );
		InitFailed();
		return false;
	}

	deviceList = (AudioDeviceID*)malloc( size );
	status = AudioHardwareGetProperty( kAudioHardwarePropertyDevices, &size, deviceList );
	if ( status != kAudioHardwareNoError ) {
		common->Warning( "AudioHardwareGetProperty kAudioHardwarePropertyDevices failed. status: %s", ExtractStatus( status ) );
		free( deviceList );
		InitFailed();
		return false;
	}

	common->Printf( "%d sound device(s)\n", deviceCount );
	for( i = 0; i < deviceCount; i++ ) {
		size = 1024;
		status = AudioDeviceGetProperty( deviceList[ i ], 0, false, kAudioDevicePropertyDeviceName, &size, buf );
		if ( status != kAudioHardwareNoError ) {
			common->Warning( "AudioDeviceGetProperty kAudioDevicePropertyDeviceName %d failed. status: %s", i, ExtractStatus( status ) );
			free( deviceList );
			InitFailed();
			return false;
		}
		common->Printf( "  %d: ID %d, %s - ", i, deviceList[ i ], buf );
		size = 1024;
		status = AudioDeviceGetProperty( deviceList[ i ], 0, false, kAudioDevicePropertyDeviceManufacturer, &size, buf );
		if ( status != kAudioHardwareNoError ) {
			common->Warning( "AudioDeviceGetProperty kAudioDevicePropertyDeviceManufacturer %d failed. status: %s", i, ExtractStatus( status ) );
			free( deviceList );
			InitFailed();
			return false;
		}
		common->Printf( "%s\n", buf );
	}

	if ( s_device.GetInteger() != -1 && s_device.GetInteger() < deviceCount ) {
		selectedDevice = deviceList[ s_device.GetInteger() ];
		common->Printf( "s_device: device ID %d\n", selectedDevice );
	} else {
		size = sizeof( selectedDevice );
		status = AudioHardwareGetProperty( kAudioHardwarePropertyDefaultOutputDevice, &size, &selectedDevice );
		if ( status != kAudioHardwareNoError ) {
			common->Warning( "AudioHardwareGetProperty kAudioHardwarePropertyDefaultOutputDevice failed. status: %s", ExtractStatus( status ) );

			free( deviceList );
			InitFailed();
			return false;
		}
		common->Printf( "select default device, ID %d\n", selectedDevice );
	}

	free( deviceList );
	deviceList = NULL;

	/*
	// setup a listener to watch for changes to properties
	status = AudioDeviceAddPropertyListener( selectedDevice, 0, false, kAudioDeviceProcessorOverload, DeviceListener, this );
	if ( status != kAudioHardwareNoError ) {
		common->Warning( "AudioDeviceAddPropertyListener kAudioDeviceProcessorOverload failed. status: %s", ExtractStatus( status ) );
		InitFailed();
		return;
	}
	*/

	Float64 sampleRate;
	size = sizeof( sampleRate );
	status = AudioDeviceGetProperty( selectedDevice, 0, false, kAudioDevicePropertyNominalSampleRate, &size, &sampleRate );
	if ( status != kAudioHardwareNoError ) {
		common->Warning( "AudioDeviceGetProperty %d kAudioDevicePropertyNominalSampleRate failed. status: %s", selectedDevice, ExtractStatus( status ) );
		InitFailed();
		return false;
	}
	common->Printf( "current nominal rate: %g\n", sampleRate );

	if ( sampleRate != PRIMARYFREQ ) {

		GetAvailableNominalSampleRates();

		sampleRate = PRIMARYFREQ;
		common->Printf( "setting rate to: %g\n", sampleRate );		
		status = AudioDeviceSetProperty( selectedDevice, NULL, 0, false, kAudioDevicePropertyNominalSampleRate, size, &sampleRate );
		if ( status != kAudioHardwareNoError ) {
			common->Warning( "AudioDeviceSetProperty %d kAudioDevicePropertyNominalSampleRate %g failed. status: %s", selectedDevice, sampleRate, ExtractStatus( status ) );
			InitFailed();
			return false;
		}
	}

	UInt32 frameSize;
	size = sizeof( UInt32 );
	status = AudioDeviceGetProperty( selectedDevice, 0, false, kAudioDevicePropertyBufferFrameSize, &size, &frameSize );
	if ( status != kAudioHardwareNoError ) {
		common->Warning( "AudioDeviceGetProperty %d kAudioDevicePropertyBufferFrameSize failed.status: %s", selectedDevice, ExtractStatus( status ) );
		InitFailed();
		return false;
	}
	common->Printf( "current frame size: %d\n", frameSize );

	// get the allowed frame size range
	AudioValueRange frameSizeRange;
	size = sizeof( AudioValueRange );
	status = AudioDeviceGetProperty( selectedDevice, 0, false, kAudioDevicePropertyBufferFrameSizeRange, &size, &frameSizeRange );
	if ( status != kAudioHardwareNoError ) {
		common->Warning( "AudioDeviceGetProperty %d kAudioDevicePropertyBufferFrameSizeRange failed. status: %s", selectedDevice, ExtractStatus( status ) );
		InitFailed();
		return false;
	}
	common->Printf( "frame size allowed range: %g %g\n", frameSizeRange.mMinimum, frameSizeRange.mMaximum );

	if ( frameSizeRange.mMaximum < MIXBUFFER_SAMPLES ) {
		common->Warning( "can't obtain the required frame size of %d bits", MIXBUFFER_SAMPLES );
		InitFailed();
		return false;
	}

	if ( frameSize != (unsigned int)MIXBUFFER_SAMPLES ) {
		frameSize = MIXBUFFER_SAMPLES;
		common->Printf( "setting frame size to: %d\n", frameSize );
		size = sizeof( frameSize );
		status = AudioDeviceSetProperty( selectedDevice, NULL, 0, false, kAudioDevicePropertyBufferFrameSize, size, &frameSize );
		if ( status != kAudioHardwareNoError ) {
			common->Warning( "AudioDeviceSetProperty %d kAudioDevicePropertyBufferFrameSize failed. status: %s", selectedDevice, ExtractStatus( status ) );
			InitFailed();
			return false;
		}
	}

	if ( idSoundSystemLocal::s_numberOfSpeakers.GetInteger() != 2 ) {
		common->Warning( "only stereo sound currently supported" );
		idSoundSystemLocal::s_numberOfSpeakers.SetInteger( 2 );
	}
	UInt32 channels[ 2 ];
	size = 2 * sizeof( UInt32 );
	status = AudioDeviceGetProperty( selectedDevice, 0, false, 	kAudioDevicePropertyPreferredChannelsForStereo, &size, &channels );
	if ( status != kAudioHardwareNoError ) {
		common->Warning( "AudioDeviceGetProperty %d kAudioDevicePropertyPreferredChannelsForStereo failed. status: %s", selectedDevice, ExtractStatus( status ) );
		InitFailed();
		return false;
	}
	common->Printf( "using stereo channel IDs %d %d\n", channels[ 0 ], channels[ 1 ] );

	status = AudioDeviceAddIOProc( selectedDevice, DeviceIOProc, NULL );
	if ( status != kAudioHardwareNoError ) {
		common->Warning( "AudioDeviceAddIOProc failed. status: %s", ExtractStatus( status ) );
		InitFailed();
		return false;
	}
	activeIOProc = true;

	status = AudioDeviceStart( selectedDevice, DeviceIOProc );
	if ( status != kAudioHardwareNoError ) {
		common->Warning( "AudioDeviceStart failed. status: %s", ExtractStatus( status ) );
		InitFailed();
		return false;
	}

	/*
	// allocate the mix buffer
	// it has the space for ROOM_SLICES_IN_BUFFER DeviceIOProc loops
	mixBufferSize =  dwSpeakers * dwSampleSize * dwPrimaryBitRate * ROOM_SLICES_IN_BUFFER / 8;
	mixBuffer = malloc( mixBufferSize );
	memset( mixBuffer, 0, mixBufferSize );
	*/

	return true;
}

/*
==========
idAudioHardwareOSX::GetAvailableNominalSampleRates
==========
*/
void idAudioHardwareOSX::GetAvailableNominalSampleRates( void ) {
	UInt32				size;
	OSStatus			status;
	int			   		i, rangeCount;
	AudioValueRange		*rangeArray;

	status = AudioDeviceGetPropertyInfo( selectedDevice, 0, false, kAudioDevicePropertyAvailableNominalSampleRates, &size, NULL );
	if ( status != kAudioHardwareNoError ) {
		common->Warning( "AudioDeviceGetPropertyInfo %d kAudioDevicePropertyAvailableNominalSampleRates failed. status: %s", selectedDevice, ExtractStatus( status ) );
		return;
	}
	rangeCount = size / sizeof( AudioValueRange );
	rangeArray = (AudioValueRange *)malloc( size );

	common->Printf( "%d possible rate(s)\n", rangeCount );

	status = AudioDeviceGetProperty( selectedDevice, 0, false, kAudioDevicePropertyAvailableNominalSampleRates, &size, rangeArray );
	if ( status != kAudioHardwareNoError ) {
		common->Warning( "AudioDeviceGetProperty %d kAudioDevicePropertyAvailableNominalSampleRates failed. status: %s", selectedDevice, ExtractStatus( status ) );
		free( rangeArray );
		return;
	}

	for( i = 0; i < rangeCount; i++ ) {
		common->Printf( "  %d: min %g max %g\n", i, rangeArray[ i ].mMinimum, rangeArray[ i ].mMaximum );
	}

	free( rangeArray );
}

/*
==========
idAudioHardwareOSX::GetNumberOfSpeakers
==========
*/
int	idAudioHardwareOSX::GetNumberOfSpeakers() {
	return idSoundSystemLocal::s_numberOfSpeakers.GetInteger();
}

/*
 ===============
 Sys_LoadOpenAL
 ===============
 */
bool Sys_LoadOpenAL( void ) {
	OSErr	err;
	long gestaltOSVersion;
	err = Gestalt(gestaltSystemVersion, &gestaltOSVersion);
	if ( err || gestaltOSVersion < 0x1040 ) {
		return false;
	}
	return true;
}
