//
// Asura_AX.cpp
//
// Created by KOR - 10 October 2006
//
// Copyright (C) 2006 Rebellion, All Rights Reserved.
//

#include "Core/Asura.h"

// This file's header.
#include "Sound/Asura_AX.h"

// Asura headers
#include "Core/Asura_Maths.h"
#include "Core/Memory/Asura_Mem.h"
#include "FileAccess/Asura_FileAccess.h"

// Asura Wii headers
#include "Core/Asura_Wii_MEM2.h"
#include "Sound/Asura_Wii_Sound.h"

// Library headers
#include <revolution/ai.h>
#include <revolution/mix.h>
#include <math.h>

const u_int ZEROBUFFER_BYTES = 256;
const u_int uASURA_AX_VOICE_PRIORITY = 1; // should be 1 (lowest) to 31 (highest)

#define Bytes2Nibbles(n) (n << 1)

Asura_AX_ActiveSound Asura_AX::s_axActiveSounds[ASURA_WII_MAX_NUMBER_OF_ACTIVE_SOUNDS];
Asura_AX_LoadedSound* Asura_AX::s_apxLoadedSounds[ASURA_MAX_NUMBER_OF_LOADED_SOUNDS];
bool			Asura_AX::s_bInitialised = false;
char*			Asura_AX::s_pcZeroBuffer;
u_int			Asura_AX::s_uZeroBuffer;
int				Asura_AX::s_iSoundFXVolume;

static void AXAudioFrameCallBack()
{
	Asura_Wii_Sound::StreamingSoundCallback();
    MIXUpdateSettings();
	// Adjust effects here
}

void Asura_AX::Initialise()
{
	// Double to get nibble address
	s_pcZeroBuffer = static_cast<char*>(Asura_AllocateAlign(ZEROBUFFER_BYTES, ASURA_ALIGN_8_BYTE));
	Asura_Mem::Set(s_pcZeroBuffer, 0, ZEROBUFFER_BYTES);
	DCFlushRange(s_pcZeroBuffer, ZEROBUFFER_BYTES);
	s_uZeroBuffer = Bytes2Nibbles(OSCachedToPhysical(s_pcZeroBuffer)) + 2;
	
    //ARQInit();
    AIInit(NULL);
    AXInit();
    MIXInit();
	
	AISetDSPSampleRate(AI_SAMPLERATE_32KHZ);

	AXRegisterCallback(&AXAudioFrameCallBack);

	Asura_Mem::Set(s_apxLoadedSounds, 0, sizeof(s_apxLoadedSounds));
	for(u_int u = 0; u < ASURA_WII_MAX_NUMBER_OF_ACTIVE_SOUNDS; ++u)
	{
		s_axActiveSounds[u].m_bIsPaused = false;
		s_axActiveSounds[u].m_pxAXVPB = NULL;
		s_axActiveSounds[u].m_uLoadedSoundIndex = 0xffffffff;
	}
	s_bInitialised = true;

	// Should we be getting this from the console settings?
	const u_int uSoundMode = SCGetSoundMode();
	switch(uSoundMode)
	{
		default: Asura_Assert(0, "Unrecognised sound mode");
		case SC_SOUND_MODE_MONO:
		{
			AXSetMode(AX_MODE_STEREO);
			MIXSetSoundMode(MIX_SOUND_MODE_MONO);
			break;
		}
		case SC_SOUND_MODE_STEREO:
		{
			AXSetMode(AX_MODE_STEREO);
			MIXSetSoundMode(MIX_SOUND_MODE_STEREO);
			break;
		}
		case SC_SOUND_MODE_SURROUND:
		{
			AXSetMode(AX_MODE_DPL2);
			MIXSetSoundMode(MIX_SOUND_MODE_DPL2);
			break;
		}
	}
}

void Asura_AX::Shutdown()
{
	MIXQuit();
    AXQuit();

	Asura_DeallocateAlign(s_pcZeroBuffer);
}

void Asura_AX::Reset()
{
	// KOR - 08/01/2007 - I don't think it should be necessary to reset all systems between levels.
}

void Asura_AX::Update()
{
	// Not actually called, it seems.
	// Actual update is done in AXAudioFrameCallBack.
}

bool Asura_AX::LoadSound(const char* filename, Asura_Handle iSoundHandle)
{
	return false;
}

bool Asura_AX::LoadSoundFromResource(Asura_Handle iFileHandle, int iFileSize, Asura_Handle iLoadedSoundHandle)
{
	if(!s_bInitialised)
	{
		Asura_Assert(0, "Tried to load sound before initialisation.\n");
		Initialise();
	}
	s_apxLoadedSounds[iLoadedSoundHandle] = new Asura_AX_LoadedSound;
	Asura_Assert(s_apxLoadedSounds[iLoadedSoundHandle], "Failed to allocate sound");
	Asura_AX_LoadedSound& xLoadedSound = *s_apxLoadedSounds[iLoadedSoundHandle];

	// Read header
	Asura_FileAccess::ReadData(iFileHandle, &xLoadedSound.m_xADPCMHeader, 96);
	
	const u_int uNumBytesPaddedTo4 = iFileSize - 96;

	xLoadedSound.m_pcADPCMData = static_cast<char*>(ASURA_WII_MEM2_ALLOC_SILENTFAIL(uNumBytesPaddedTo4));
	if(!xLoadedSound.m_pcADPCMData)
	{
		Asura_Assert(0, "Could not allocate from MEM2 for sound data\n");
		return false;
	}
	Asura_FileAccess::ReadData(iFileHandle, xLoadedSound.m_pcADPCMData, uNumBytesPaddedTo4);

	return true;
}

bool Asura_AX::UnloadSound(Asura_Handle iSoundHandle)
{
	Asura_AX_LoadedSound& xLoadedSound = *s_apxLoadedSounds[iSoundHandle];
	ASURA_WII_MEM2_FREE(xLoadedSound.m_pcADPCMData);
	delete s_apxLoadedSounds[iSoundHandle];
	s_apxLoadedSounds[iSoundHandle] = NULL;
	return true;
}

bool Asura_AX::IsSoundLoaded(Asura_Handle iSoundHandle)
{
	return (s_apxLoadedSounds[iSoundHandle] != NULL);
}

//#define DEBUG_DROPPED_VOICES (1)

#ifdef DEBUG_DROPPED_VOICES
#include "Sound/Asura_Sound.h"
#endif

void Asura_AX::VoiceDropCallback(void* pvAXVPB)
{
#ifdef DEBUG_DROPPED_VOICES
	// We're dropping voices when we only have a 20-30 active; trying to find out why.
	u_int uActiveVoiceCount = 0;
	float fSrcRatioSum = 0.f;
	for(u_int u = 0; u < ASURA_WII_MAX_NUMBER_OF_ACTIVE_SOUNDS; ++u)
	{
		Asura_AX_ActiveSound& xActiveSound = s_axActiveSounds[u];
		if(xActiveSound.m_pxAXVPB)
		{
			fSrcRatioSum += static_cast<float>(xActiveSound.m_pxAXVPB->pb.src.ratioHi);
			fSrcRatioSum += static_cast<float>(xActiveSound.m_pxAXVPB->pb.src.ratioLo) / 0xffff;
			++uActiveVoiceCount;
		}
	}
	const u_int uNumStreamingSounds = Asura_Sound_System::GetNumberActiveStreamingSounds();
	OSReport("Voice dropped (currently %i active (%f), %u streaming vs. %i theoretical max).\n", uActiveVoiceCount, fSrcRatioSum, uNumStreamingSounds, AXGetMaxVoices());
#endif
	const AXVPB* const pxAXVPB = reinterpret_cast<AXVPB*>(pvAXVPB);
	const u_int uActiveSoundIndex = pxAXVPB->userContext;
	Asura_Assert(uActiveSoundIndex < ASURA_WII_MAX_NUMBER_OF_ACTIVE_SOUNDS, "Dropped active sound index out of range");
	Asura_AX_ActiveSound& xActiveSound = s_axActiveSounds[uActiveSoundIndex];
	Asura_Assert(xActiveSound.m_pxAXVPB == pvAXVPB, "Unexpected pointer for dropped voice");
	xActiveSound.m_pxAXVPB = NULL;
}

bool Asura_AX::CreateSound(Asura_Handle iActiveSoundHandle, Asura_Handle iLoadedSoundHandle)
{
	Asura_AX_ActiveSound& xActiveSound = s_axActiveSounds[iActiveSoundHandle];
	Asura_AX_LoadedSound& xLoadedSound = *s_apxLoadedSounds[iLoadedSoundHandle];
	Asura_Assert(!xActiveSound.m_pxAXVPB, "Already a sound at this handle");
	if(xActiveSound.m_pxAXVPB)
	{
		return true;
	}
	xActiveSound.m_uLoadedSoundIndex = iLoadedSoundHandle;
	//OSReport("Creating sound %i.\n", iActiveSoundHandle);
	xActiveSound.m_pxAXVPB = AXAcquireVoice(uASURA_AX_VOICE_PRIORITY, &VoiceDropCallback, iActiveSoundHandle);
	if(xActiveSound.m_pxAXVPB)
	{
		AXPBADPCM       xAXPBAdpcm;
		AXPBADPCMLOOP	xAXPBAdpcmLoop;

		// Initialise in front centre.
		MIXInitChannel(
			xActiveSound.m_pxAXVPB,		// pointer to voice
			MIX_MODE_MUTE,				// initial aux A, B, mute modes
			0,							// initial input atten / gain
			-960,						// initial aux A atten / gain
			-960,						// initial aux B atten / gain
			-960,						// initial aux C atten / gain
			64,							// initial pan 
			127,						// initial span
			0);							// initial fader atten / gain

		Asura_Mem::Copy(xAXPBAdpcm.a, xLoadedSound.m_xADPCMHeader.coef, 8 * 2 * sizeof(u16));
		xAXPBAdpcm.gain              = xLoadedSound.m_xADPCMHeader.gain;
		xAXPBAdpcm.pred_scale        = xLoadedSound.m_xADPCMHeader.ps;
		xAXPBAdpcm.yn1               = xLoadedSound.m_xADPCMHeader.yn1;
		xAXPBAdpcm.yn2               = xLoadedSound.m_xADPCMHeader.yn2;
        AXSetVoiceAdpcm(xActiveSound.m_pxAXVPB, &xAXPBAdpcm);

		xAXPBAdpcmLoop.loop_pred_scale	= xLoadedSound.m_xADPCMHeader.ps;
		xAXPBAdpcmLoop.loop_yn1			= xLoadedSound.m_xADPCMHeader.yn1;
		xAXPBAdpcmLoop.loop_yn2			= xLoadedSound.m_xADPCMHeader.yn2;
		AXSetVoiceAdpcmLoop(xActiveSound.m_pxAXVPB, &xAXPBAdpcmLoop);

        AXSetVoiceType(xActiveSound.m_pxAXVPB, AX_PB_TYPE_NORMAL);
        AXSetVoiceSrcType(xActiveSound.m_pxAXVPB, AX_SRC_TYPE_4TAP_16K);
		AXSetVoiceSrcRatio(xActiveSound.m_pxAXVPB, (f32)xLoadedSound.m_xADPCMHeader.sample_rate/32000.0f);
		AXSetVoiceState(xActiveSound.m_pxAXVPB, AX_PB_STATE_STOP);
		return true;
	}
	return false;
}

DEBUGVAR float dbg_fMinLengthToUpdate = 0.5f;

bool Asura_AX::PlaySound(Asura_Handle iSoundHandle, bool bLooping)
{
	
	//OSReport("Playing sound %i\n", iSoundHandle);
	Asura_AX_ActiveSound& xActiveSound = s_axActiveSounds[iSoundHandle];
	Asura_AX_LoadedSound& xLoadedSound = *s_apxLoadedSounds[xActiveSound.m_uLoadedSoundIndex];

	if(xActiveSound.m_pxAXVPB)
	{
	    // Address in sound RAM in nibbles of start of sound data
		char* pcAddress = xLoadedSound.m_pcADPCMData;
        Asura_Assert( pcAddress, "uAddress is zero!" );
        if ( !pcAddress )
        {
            return false;
        }
		const u_int uMRAMStart = OSCachedToPhysical(pcAddress);
		const u_int uMRAMBufferStart	= (xLoadedSound.m_xADPCMHeader.sa + Bytes2Nibbles(uMRAMStart));
		const u_int uMRAMBufferEnd		= (xLoadedSound.m_xADPCMHeader.ea + Bytes2Nibbles(uMRAMStart));
		// All of this should really be precomputed.
		AXPBADDR xAXPBAddr;
		if(bLooping)
		{
			xAXPBAddr.loopFlag           = AXPBADDR_LOOP_ON;
			xAXPBAddr.loopAddressHi      = (u16)(uMRAMBufferStart >> 16);       
			xAXPBAddr.loopAddressLo      = (u16)(uMRAMBufferStart & 0xFFFF);
//			OSReport("Playing looping sound at %u\n", uAddress);
		}
		else
		{
			xAXPBAddr.loopFlag           = AXPBADDR_LOOP_OFF;
			xAXPBAddr.loopAddressHi      = (u16)(s_uZeroBuffer >> 16);       
			xAXPBAddr.loopAddressLo      = (u16)(s_uZeroBuffer & 0xFFFF);
//			OSReport("Playing non-looping sound at %u\n", uAddress);
		}
		xAXPBAddr.format             = AX_PB_FORMAT_ADPCM;
		xAXPBAddr.endAddressHi       = (u16)(uMRAMBufferEnd  >> 16);
		xAXPBAddr.endAddressLo       = (u16)(uMRAMBufferEnd  & 0xFFFF);       
		xAXPBAddr.currentAddressHi   = (u16)(uMRAMBufferStart >> 16);       
		xAXPBAddr.currentAddressLo   = (u16)(uMRAMBufferStart & 0xFFFF);
		AXSetVoiceAddr(xActiveSound.m_pxAXVPB, &xAXPBAddr);
		AXSetVoiceState(xActiveSound.m_pxAXVPB, AX_PB_STATE_RUN);
		
		xActiveSound.m_bIsPaused = false;
#ifdef ASURA_WII_DONT_UPDATE_SHORT_SOUNDS
		const float fLength = static_cast<float>(xLoadedSound.m_xADPCMHeader.num_samples) / xLoadedSound.m_xADPCMHeader.sample_rate;
		xActiveSound.m_bShouldUpdate = (fLength > dbg_fMinLengthToUpdate);
#endif
		return true;
	}
	return false;
}

bool Asura_AX::PauseSound(Asura_Handle iSoundHandle)
{
	Asura_AX_ActiveSound& xActiveSound = s_axActiveSounds[iSoundHandle];
	if(xActiveSound.m_pxAXVPB)
	{
		MIXMute(xActiveSound.m_pxAXVPB);
		AXSetVoiceState(xActiveSound.m_pxAXVPB, AX_PB_STATE_STOP);
		xActiveSound.m_bIsPaused = true;
		return true;
	}
	return false;
}

bool Asura_AX::UnpauseSound(Asura_Handle iSoundHandle)
{
	Asura_AX_ActiveSound& xActiveSound = s_axActiveSounds[iSoundHandle];
	if(xActiveSound.m_pxAXVPB)
	{
		unsigned int uAddress = ((unsigned int)xActiveSound.m_pxAXVPB->pb.addr.currentAddressHi << 16) + (unsigned int)xActiveSound.m_pxAXVPB->pb.addr.currentAddressLo;
		// Don't start if at the zero buffer
		if(uAddress != s_uZeroBuffer)
		{
			MIXUnMute(xActiveSound.m_pxAXVPB);
			AXSetVoiceState(xActiveSound.m_pxAXVPB, AX_PB_STATE_RUN);
		}
		xActiveSound.m_bIsPaused = false;
		return true;
	}
	return false;
}

bool Asura_AX::StopSound(Asura_Handle iSoundHandle)
{
	Asura_AX_ActiveSound& xActiveSound = s_axActiveSounds[iSoundHandle];
	if(xActiveSound.m_pxAXVPB)
	{
		AXSetVoiceState(xActiveSound.m_pxAXVPB, AX_PB_STATE_STOP);
		return true;
	}
	return false;
}

bool Asura_AX::DestroySound(Asura_Handle iSoundHandle)
{
	Asura_AX_ActiveSound& xActiveSound = s_axActiveSounds[iSoundHandle];
	//OSReport("Destroying sound %i\n", iSoundHandle);
	if(xActiveSound.m_pxAXVPB)
	{
		//OSReport("Destroying sound %i.\n", iSoundHandle);
		MIXReleaseChannel(xActiveSound.m_pxAXVPB);
		AXFreeVoice(xActiveSound.m_pxAXVPB);
		xActiveSound.m_pxAXVPB = NULL;
	}
	return true;
}

bool Asura_AX::IsSoundPlaying(Asura_Handle iSoundHandle)
{
	Asura_AX_ActiveSound& xActiveSound = s_axActiveSounds[iSoundHandle];
	if(xActiveSound.m_pxAXVPB)
	{
		if(xActiveSound.m_bIsPaused)
		{
			return true;
		}
		else
		{
			return (xActiveSound.m_pxAXVPB->pb.state == AX_PB_STATE_RUN);
		}
	}
	return false;
}

const float fASURA_AX_MIN_DECIBELS = -96.f;
const float fASURA_AX_MIN_LINEAR = 0.0000158489;// DecibelsToLinearVolume(fASURA_AX_MIN_DECIBELS)

float Asura_AX::LinearVolumeToDecibels(float fLinear)
{
	Asura_Assert(0.f <= fLinear && fLinear <= 1.f, "Linear volume out of range");
	return (fLinear > fASURA_AX_MIN_LINEAR) ? log10f(fLinear) * 20.f : fASURA_AX_MIN_DECIBELS;
}

float Asura_AX::DecibelsToLinearVolume(float fDecibels)
{
	Asura_Assert(fDecibels <= 0.f, "Decibels should be negative.");
	return Asura_Maths::ClampToRange(powf(10.f, (fDecibels * (1.f / 20.f))), 0.f, 1.f);
}

// Takes volume fade in range [0, 1]
bool Asura_AX::SetVolume(Asura_Handle iSoundHandle, float fVolume)
{
	Asura_AX_ActiveSound& xActiveSound = s_axActiveSounds[iSoundHandle];
	if(xActiveSound.m_pxAXVPB)
	{
		// Allow volume to be updated once.
#ifdef ASURA_WII_DONT_UPDATE_SHORT_SOUNDS
		if(xActiveSound.m_bShouldUpdate || MIXIsMute(xActiveSound.m_pxAXVPB))
#endif
		{
			const float fDecibels = LinearVolumeToDecibels(fVolume) + s_iSoundFXVolume;
			// MIXSetInput takes tenths of a decibel.
			MIXSetInput(xActiveSound.m_pxAXVPB, static_cast<int>(fDecibels * 10.f));
			MIXUnMute(xActiveSound.m_pxAXVPB);
		}
		return true;
	}
	return false;
}

void Asura_AX::SetSoundFXVolume(float fVolume)
{
	s_iSoundFXVolume = LinearVolumeToDecibels(fVolume);
}

bool Asura_AX::SetPitch(Asura_Handle iSoundHandle, float fPitch)
{
	Asura_AX_ActiveSound& xActiveSound = s_axActiveSounds[iSoundHandle];
	Asura_AX_LoadedSound& xLoadedSound = *s_apxLoadedSounds[xActiveSound.m_uLoadedSoundIndex];
	if(xActiveSound.m_pxAXVPB)
	{
		const float fSampleRatio = fPitch * static_cast<float>(xLoadedSound.m_xADPCMHeader.sample_rate) * (1.f / 32000.0f);
		AXSetVoiceSrcRatio(xActiveSound.m_pxAXVPB, fSampleRatio);
		return true;
	}
	return false;
}

bool Asura_AX::SetPan(Asura_Handle iSoundHandle, float fPan)
{
	Asura_AX_ActiveSound& xActiveSound = s_axActiveSounds[iSoundHandle];
	// Independent code should guarantee pan is in [0.f, 1.f]
	Asura_Assert(fPan >= 0.0f && fPan <= 1.0f, "pan out of range");
	if(xActiveSound.m_pxAXVPB)
	{
#ifdef ASURA_WII_DONT_UPDATE_SHORT_SOUNDS
		if(xActiveSound.m_bShouldUpdate || MIXGetPan(xActiveSound.m_pxAXVPB) == 64)
#endif
		{
			MIXSetPan(xActiveSound.m_pxAXVPB, int(fPan * 127));
		}
		return true;
	}
	return false;
}

bool Asura_AX::SetFade(Asura_Handle iSoundHandle, float fFade)
{
	Asura_AX_ActiveSound& xActiveSound = s_axActiveSounds[iSoundHandle];
	// Independent code should guarantee fade is in [0.f, 1.f]
	Asura_Assert(fFade >= 0.0f && fFade <= 1.0f, "fade out of range");
	if(xActiveSound.m_pxAXVPB)
	{
#ifdef ASURA_WII_DONT_UPDATE_SHORT_SOUNDS
		if(xActiveSound.m_bShouldUpdate || MIXGetSPan(xActiveSound.m_pxAXVPB) == 127)
#endif
		{
			MIXSetSPan(xActiveSound.m_pxAXVPB, int(fFade * 127));
		}
		return true;
	}
	return false;
}

