/** SGX

The   sss    gggg x   x   
     s      g      x x    Engine
      sss   g  gg   x
         s  g   g  x x          Copyright 1995-2009.
     ssss    gggg x   x         Steven Goodwin. All Rights Reserved.
    
This version has been released under the GNU Public License v2.0.

Please see the license files for details.

File: sgx/src/sgx/audio/soundengine.cpp

*/

#include "sgx/core/core.hpp"
#include "sgx/audio/audio.hpp"
#include "domains/filesystem/fileutils.hpp"
#include <cmath>

using std::log10;

CSGXAudioEngine *CSGXAudioEngine::m_pSingleton = NULL;

CSGXAudioEngine *CSGXAudioEngine::create()
{
	m_pSingleton = new CSGXAudioEngine();
	if (m_pSingleton) {
		m_pSingleton->initialize();
	}
	return m_pSingleton;
}

CSGXAudioEngine *CSGXAudioEngine::get()
{
	if (!m_pSingleton) {
		return create();
	}
	return m_pSingleton;
}

CSGXAudioEngine::CSGXAudioEngine() 
{
	m_bMute = FALSE;
}

void 
CSGXAudioEngine::initialize()
{
tUINT32	iTableSize;
tREAL32 fScale, fLog;

	CSGXEngine::initialize();

    iTableSize = sizeof(m_VolumeTable)/sizeof(m_VolumeTable);
    fScale = 600.0f; // number of 'units' in a 6 dB drop

    for (tMEM_SIZE i = 1; i <= iTableSize; i++) 
    {
        fLog = log10((tREAL32)i/iTableSize) / log10(2.0f);
        m_VolumeTable[i] = tUINT32(fScale * fLog);
   }

	prepareLogicalChannels(AUDIO_DEFAULT_LOGICAL_CHANNELS);
	prepareNormalizationGroups(AUDIO_DEFAULT_NORMALIZATION_GROUPS);

}

void
CSGXAudioEngine::prepareLogicalChannels(const tUINT32 numChannels)
{
	m_NumChannels = numChannels;

	m_pChannels = new CLogicalChannel *[numChannels];
	m_MixOutput = new CMixOutput*[numChannels];
	m_pMixChannels = new CMixChannel*[numChannels];

	for(tUINT32 i=0;i<numChannels;++i) {
		m_MixOutput[i] = new CMixOutput();
		m_pChannels[i] = new CLogicalChannel(i);
		m_pMixChannels[i] = new CMixChannel();
	}
}

tUINT32
CSGXAudioEngine::getDefaultNormalizationGroup() {
	return m_DefaultNormalizationGroup;
}

void
CSGXAudioEngine::setDefaultNormalizationGroup(const tUINT32 group) {
	sgxAssert(group < m_NumNormalizationGroups);
	m_DefaultNormalizationGroup = group;
}

tREAL32
CSGXAudioEngine::setNormalizationLevel(const tUINT32 group, const tREAL32 level) {
	sgxAssert(group < m_NumNormalizationGroups);

	tREAL32 oldLevel = m_pNormalization[group];
	m_pNormalization[group] = level;
	return oldLevel;
}

tREAL32
CSGXAudioEngine::getNormalizationLevel(const tUINT32 group) {
	sgxAssert(group < m_NumNormalizationGroups);
	return m_pNormalization[group];
}

void 
CSGXAudioEngine::prepareNormalizationGroups(const tUINT32 numGroups) 
{
	// The normalization group adjusts the volumes of various sounds
	// to be relatively equivalent, since all sounds are normalized
	// off-line.
	m_NumNormalizationGroups = numGroups;
	m_pNormalization = new float[numGroups];
	for(tUINT32 i=0;i<numGroups;++i) {
		m_pNormalization[i] = 1.0f;
	}
}

void
CSGXAudioEngine::addSampleData(const sgxString &filename, CSampleData *pSample) {
	// Store a hashmap of all these samples, so the filename
	// can be used to refer directly to them
	sgxString pathname;
	sgxString basename;
	sgxFilename::splitPathAndFilename(pathname, basename, filename);

	m_SampleList[sgxGetCRC32(basename)] = pSample;
}

CSampleData *
CSGXAudioEngine::getSampleData(const sgxString &name) {
	return m_SampleList[sgxGetCRC32(name)];
}

tUINT32	
CSGXAudioEngine::registerScenarioSound(const char *pSoundlist[], const CSGXAudioEngine::ResourceContext context/*=eDefault*/) {
tUINT32 count = 0;
	while(pSoundlist[count]) {
		registerScenarioSound(pSoundlist[count], context);
		++count;
	}
	return count;
}

CSampleData *
CSGXAudioEngine::registerScenarioSound(const sgxString &filename, const CSGXAudioEngine::ResourceContext context/*=eDefault*/) 
{
	// The NULL driver should load the sound and parse it to
	// determine its duration.
	CSampleData *pSample = createSample(filename, context);
	if (pSample) {
		registerScenarioObject(pSample);
		addSampleData(filename, pSample);
	}
	return pSample;
}

CSampleData *
CSGXAudioEngine::registerGlobalSound(const sgxString &filename, const CSGXAudioEngine::ResourceContext context/*=eDefault*/) 
{
	// The NULL driver should load the sound and parse it to
	// determine its duration.
	CSampleData *pSample = createSample(filename, context);
	if (pSample) {
		registerGlobalObject(pSample);
		addSampleData(filename, pSample);
	}
	return pSample;
}

void
CSGXAudioEngine::unregisterScenarioObjects()  {
	// stop any scenario sounds playing
	sgxVector<CGameHandler *>::iterator it = m_ScenarioObjects.begin();
	while(it != m_ScenarioObjects.end()) {
		stopAllSounds(SGX_STATIC_CAST(CSampleData *, *it));
		++it;
	}

	CSGXEngine::unregisterScenarioObjects();
}

CSampleData *
CSGXAudioEngine::createSample(const sgxString &filename, const CSGXAudioEngine::ResourceContext context/*=eDefault*/) 
{
	return new CSampleData(filename, context);
}


tUINT32	
CSGXAudioEngine::getMaxChannels()  const
{
	return m_NumChannels;
}

CLogicalChannel *
CSGXAudioEngine::getFreeChannel()  const
{
	for(tUINT32 i=0;i<m_NumChannels;i++) {
		if (m_pChannels[i]->m_pSampleData == NULL && !m_pChannels[i]->m_bReserved) {
			return m_pChannels[i];
		}
	}
	return NULL;
}

CLogicalChannel *
CSGXAudioEngine::getDisposableChannel()  const
{
	tUINT32 current = 0;
	tREAL32 quietestSound = m_MixOutput[current]->m_fVolume;

	for(tUINT32 i=1;i<m_NumChannels;i++) {

		if (m_MixOutput[i]->m_fVolume < quietestSound) {
			quietestSound = m_MixOutput[i]->m_fVolume;
			current = i;
		}
	}

	return m_pChannels[current];
}


CLogicalChannel *
CSGXAudioEngine::playSound(const sgxString &name, CLogicalChannel *pChannel/*=NULL*/, CMixChannel *pInitialMix/*=NULL*/)
{
	CSampleData *pSample = getSampleData(name);
	return playSoundAt(pSample, sgxVector3::Null, pChannel, pInitialMix);
}

CLogicalChannel *
CSGXAudioEngine::playSound( CSampleData *pSound, CLogicalChannel *pChannel/*=NULL*/, CMixChannel *pInitialMix/*=NULL*/)
{
	return playSoundAt(pSound, sgxVector3::Null, pChannel, pInitialMix);
}

CLogicalChannel *
CSGXAudioEngine::playSoundAt(CSampleData *pSound, const sgxVector3 &pos, CLogicalChannel *pChannel, CMixChannel *pInitialMix)
{
	if (pSound == NULL) {
		sgxTrace("Trying to play a NULL sound... Aborted fn");
		return NULL;
	}
	if (pChannel == NULL) {
		pChannel = getFreeChannel();
	}
	if (pChannel == NULL) {
		pChannel = getDisposableChannel();
	}

	pChannel->m_pSampleData = pSound;
	pChannel->m_pMixChannel = m_pMixChannels[pChannel->m_iIndex];
	if (pInitialMix) {
		pChannel->m_pMixChannel->set(pInitialMix);
	} else {
		pChannel->m_pMixChannel->reset();
	}

	if ((pChannel->m_pDeviceChannel = pSound->play(pChannel)) != NULL) {
		pChannel->m_vPosition.set(pos);
		pChannel->m_bIsPlaying = TRUE;
		pChannel->m_fPlayPtr = 0;
	} else {
		pChannel->m_pSampleData = NULL;
		pChannel = NULL;
	}

	return pChannel;
}

tBOOL
CSGXAudioEngine::stopSound(CLogicalChannel *pLogical) {
	if (pLogical) {
		pLogical->stop();
		return TRUE;
	}
	return FALSE;
}

tBOOL			
CSGXAudioEngine::stopAllSounds(CSampleData *pSound) {
	tBOOL rt = TRUE;

	for(tUINT32 i=0;i<m_NumChannels;i++) {
		if (m_pChannels[i]->m_pSampleData == pSound || pSound == NULL) {
			rt &= stopSound(m_pChannels[i]);
		}
	}
	return TRUE;
}

void 
CSGXAudioEngine::setListenerPosition(const sgxVector3 &pos)
{
	m_Listener.vPos.set(pos);
	m_Listener.vPosNormal.set(pos);
	m_Listener.vPosNormal.normalise();
}


void 
CSGXAudioEngine::setListenerOrientation(const sgxVector3 &dir)
{
	m_Listener.vOrientation.set(dir);
	m_Listener.vOrientationNormal.set(dir);
	m_Listener.vOrientationNormal.normalise();
}

void
CSGXAudioEngine::update(const tREAL32 time_elapsed)
{
	for(tUINT32 i=0;i<m_NumChannels;i++) {

 		m_MixOutput[i]->reset();
		
		if (m_pChannels[i]->m_pSampleData == NULL) {
			continue;
		}

		CLogicalChannel *pLogical = m_pChannels[i];

		pLogical->update(time_elapsed);

		 // The mix output acculumates all the volume and
		 // pan settings as they're applied.
		m_MixOutput[i]->m_pChannel = pLogical->m_pDeviceChannel;

		evaluateMixerChain(m_MixOutput[i], pLogical, time_elapsed);
		  
		if (pLogical->hasFinished()) {
			pLogical->stop();
			return;
		}


	}

    updateHardwareMixer(time_elapsed);

}

void
CSGXAudioEngine::evaluateMixerChain(CMixOutput *pOutputMix, 
        const CLogicalChannel * const pLogical, const tREAL32 time_elapsed)
{
	processGameplayMixer(pOutputMix, pLogical, time_elapsed);
	processTechnologyMixer(pOutputMix, pLogical, time_elapsed);
	processUserMixer(pOutputMix, pLogical, time_elapsed);

	pOutputMix->validate();
}

//
// 1. Gameplay Mixer
//
void 
CSGXAudioEngine::processGameplayMixer(CMixOutput *pChannel, 
        const CLogicalChannel * const pLogical, const tREAL32 time_elapsed)
{
	processGameplayLogicalMixer(pChannel, pLogical, time_elapsed);
	processGameplayControlMixer(pChannel, pLogical, time_elapsed);
}


void
CSGXAudioEngine::processGameplayLogicalMixer(CMixOutput *pChannel, 
        const CLogicalChannel * const pLogical, const tREAL32 time_elapsed)
{
	SGX_UNUSED(time_elapsed);

    pChannel->m_fVolume *= pLogical->m_pMixChannel->m_fBaseVolume;
    pChannel->m_fPan    += pLogical->m_pMixChannel->m_fBasePan;

	 // Calculate the position of this sound in the stereo field
	// BUGWARN: This doesn't consider upside down orientations
	 // TODO
#if 0
	 if (pLogical->m_vPosition == m_Listener.vPos) {
		 // identical positions
	 } else {
		sgxVector3 np(pLogical->m_vPosition);
		np.normalise();
		tREAL32 dot = sgxDot(m_Listener.vPosNormal, np);
		dot = sgxAbs(dot);
		pChannel->m_fPan += dot;
	 }
#endif
	// Vary volume, based on distance
	if (pLogical->m_pDeviceChannel) {
		tREAL32 fDist = pLogical->m_vPosition.getDistance(m_Listener.vPos);
		tREAL32 fInner = pLogical->m_pSampleData->getInnerRange();
		tREAL32 fOuter = pLogical->m_pSampleData->getOuterRange();
		
		if (fDist < fInner) {
			pChannel->m_fVolume *= 1.0f;
		} else if (fDist > fOuter) {
			pChannel->m_fVolume *= 0;
		} else {
			pChannel->m_fVolume *= (fDist-fInner) / (fOuter-fInner);
		}
	}
}


void
CSGXAudioEngine::processGameplayControlMixer(CMixOutput *pChannel, 
        const CLogicalChannel * const pLogical, const tREAL32 time_elapsed)
{
CMixChannel *pIn = pLogical->m_pMixChannel;

	SGX_UNUSED(time_elapsed);

    if (pIn->m_bFadingIn) {
        pIn->m_fFadeTime += time_elapsed;
        tREAL32 mix = pIn->m_fFadeTime / pIn->m_fFadeDuration;

        if (mix > 1.0f) {
            pIn->m_fVolume = mix = 1;
            pIn->m_bFadingIn = FALSE;
        }

        pChannel->m_fVolume *= mix;

    } else if (pIn->m_bFadingOut) {
        pIn->m_fFadeTime += time_elapsed;
        tREAL32 mix = pIn->m_fFadeTime / pIn->m_fFadeDuration;
			mix = 1 - mix;
        if (mix < 0) {
            pIn->m_fVolume = mix = 0;
            pIn->m_bFadingOut = FALSE;
        }

        pChannel->m_fVolume *= mix;
    }

    // Repeat with other control parameters...
	 // TODO- what others? fade out?
	pChannel->m_fVolume *= pIn->m_fVolume;
	pChannel->m_fPan += pIn->m_fPan;

}


//
// 2. Technology Mixer
//
void
CSGXAudioEngine::processTechnologyMixer(CMixOutput *pChannel, 
        const CLogicalChannel * const pLogical, const tREAL32 time_elapsed)
{
	SGX_UNUSED(time_elapsed);
	if (pLogical->m_pDeviceChannel) {
		tUINT32 n = pLogical->m_pSampleData->getNormalizationGroup();
		n = sgxRange<tUINT32>(n, 0, m_NumNormalizationGroups);
		pChannel->m_fVolume *= m_pNormalization[n];
	}
}


//
// 3. User Mixer
//
void 
CSGXAudioEngine::processUserMixer(CMixOutput *pChannel, 
        const CLogicalChannel * const pLogical, const tREAL32 time_elapsed)
{
	SGX_UNUSED(time_elapsed);


	if (m_bMute || pLogical->m_pMixChannel->m_bMute) {
		pChannel->m_fVolume = 0;
	}
}


// 
// 4. Hardware mixer
//
void 
CSGXAudioEngine::updateHardwareMixer(const tREAL32 time_elapsed)
{
	SGX_UNUSED(time_elapsed);

   // ... process according to m_MixOutput ...
}



void 
CSGXAudioEngine::setMute(tBOOL muted)
{
	m_bMute = muted;
}

tBOOL 
CSGXAudioEngine::getMute() const
{
	return m_bMute;
}

void
CSGXAudioEngine::suspendEngine()
{
}

void
CSGXAudioEngine::resumeEngine()
{
}


void
CSGXAudioEngine::release() 
{
}

