#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ogcsys.h>
#include <math.h>

#include "sound.h"
#include "wavsource.h"
#include "modulesource.h"
#include "mp3source.h"
#include "vorbissource.h"
#include "flacsource.h"

static SOUND_BUFFER soundBuf ATTRIBUTE_ALIGN(32);
static f32 VSA = (1.0/4294967295.0);

// We want multiple accessor classes to the same sound driver
int Sound::instance = 0;
	
File Sound::sourceFile;
SoundSource *Sound::soundSource = NULL;
SoundSource **Sound::availableSources = NULL;

FFT Sound::fft(OUTPUT_SIZE / 4);
Light Sound::light;
bool Sound::_lenabled = false;

int Sound::started;
int Sound::finished;
int Sound::paused;
int Sound::naturalFinish;

static void getExtension(const char *url, char *ext)
{
	// Grab extensions
	char tExt[10];
	tExt[0] = 0;
	
	// Only look a little ways back
	for(int i = 0; i < 9; i++)
	{
		if(url[strlen(url) - i] == '.')
		{
			// Found it!
			strcpy(tExt, &url[strlen(url) - i] + 1);
			
			break;
		}
	}
	
	// Lowercase
	strlwr(tExt);
	
	// Copy out
	strcpy(ext, tExt);
}

static void DMACallBack()
{
	// Start DMA transfer of next sound chunk
	AUDIO_StopDMA();
	AUDIO_InitDMA((u32)soundBuf.outBuffer[soundBuf.currentBuffer],OUTPUT_SIZE);
	AUDIO_StartDMA();

	// Swap buffers for next fill
	soundBuf.currentBuffer ^= 1;
	
	// Queue decoding
	soundBuf.needsUpdate = 1;
}

static void Init3BandState(EQState *es, s32 lowfreq, s32 highfreq, s32 mixfreq)
{
	memset(es,0,sizeof(EQState));

	es->lg = 1.0;
	es->mg = 1.0;
	es->hg = 1.0;
	
	es->lf = 2.0F*sin(M_PI*((f32)lowfreq/(f32)mixfreq));
	es->hf = 2.0F*sin(M_PI*((f32)highfreq/(f32)mixfreq));
}

static s16 Do3Band(EQState *es, s16 sample)
{
	f32 l,m,h;

	es->f1p0 += (es->lf*((f32)sample - es->f1p0))+VSA;
	es->f1p1 += (es->lf*(es->f1p0 - es->f1p1));
	es->f1p2 += (es->lf*(es->f1p1 - es->f1p2));
	es->f1p3 += (es->lf*(es->f1p2 - es->f1p3));
	l = es->f1p3;

	es->f2p0 += (es->hf*((f32)sample - es->f2p0))+VSA;
	es->f2p1 += (es->hf*(es->f2p0 - es->f2p1));
	es->f2p2 += (es->hf*(es->f2p1 - es->f2p2));
	es->f2p3 += (es->hf*(es->f2p2 - es->f2p3));
	h = es->sdm3 - es->f2p3;

	m = es->sdm3 - (h+l);

	l *= es->lg;
	m *= es->mg;
	h *= es->hg;

	es->sdm3 = es->sdm2;
	es->sdm2 = es->sdm1;
	es->sdm1 = (f32)sample;

	return (s16)(l+m+h);
}

static void Resample(s16 *samples, int numSamples, EQState eqs[2], u32 src_samplerate)
{
	u16 val16;
	u32 val32;
	dword pos;
	s32 incr;

	pos.adword = 0;
	incr = (u32)(((f32)src_samplerate/48000.0F)*65536.0F);
	
	while(pos.aword.hi < numSamples) 
	{
		// Channel 1 resample
		val16 = Do3Band(&eqs[0], samples[pos.aword.hi << 1]);
		val32 = (val16<<16);
		
		// Channel 2 resample
		val16 = Do3Band(&eqs[1], samples[(pos.aword.hi << 1) + 1]);
		val32 |= val16;
		
		// Place into output ring buffer
		soundBuf.ringBuf.put(&val32, sizeof(u32));
		
		// Next sample
		pos.adword += incr;
	}
}

Sound::Sound()
{
	if(!instance)
	{
		// Set buffer defaults
		soundBuf.currentBuffer = 0;
		soundBuf.needsUpdate = 0;
		
		// Remember that the finish was natural or not
		naturalFinish = 0;
		
		// Set up fft smoothing
		soundBuf.lastPassedResult = 0;
		soundBuf.countPassedResult = 0;
		soundBuf.totalPassedResult = 0;
		
		// Initialize audio
		AUDIO_Init(NULL);
		AUDIO_SetDSPSampleRate(AI_SAMPLERATE_48KHZ);
		
		// Set fft results to nothing
		memset(soundBuf.fftResult[0], 0, 16 * sizeof(int));
		memset(soundBuf.fftResult[1], 0, 16 * sizeof(int));
		
		// Create audio sources (this is the only place with any knowledge of the actual file source names)
		availableSources = (SoundSource **)malloc(6 * sizeof(SoundSource *));
		
		// Add each to list
		availableSources[0] = new WavSource();
		availableSources[1] = new ModuleSource();
		availableSources[2] = new Mp3Source();
		availableSources[3] = new VorbisSource();
		availableSources[4] = new FlacSource();
		availableSources[5] = NULL;
	}
	
	// Instance tracking
	instance++;
}

Sound::~Sound()
{
	// Instance tracking
	instance--;
	
	if(!instance)
	{
		// Kill all of the audio renderers
		int i = 0;
		
		while(availableSources[i])
		{
			// Remove source from memory
			delete availableSources[i];
			
			// Go to next source
			i++;
		}
	}
}

void Sound::update()
{
	// Check for update needed
	if(!soundBuf.needsUpdate)
	{
		// Nothing needs to be updated yet
		if(soundSource) { soundSource->readAhead(); }
		
		return;
	}
	
	if(paused)
	{
		// Sound should be empty
		memset(soundBuf.outBuffer[soundBuf.currentBuffer], 0, OUTPUT_SIZE);
		DCFlushRange(soundBuf.outBuffer[soundBuf.currentBuffer], OUTPUT_SIZE);
		
		// Set fft results to nothing
		memset(soundBuf.fftResult[0], 0, 16 * sizeof(int));
		memset(soundBuf.fftResult[1], 0, 16 * sizeof(int));
		
		if(finished) { paused = 0; }
		
		return;
	}
	
	if(finished > 0)
	{
		// Finished with sound
		if(finished == 1)
		{
			// Force a DMA update
			DMACallBack();
			
			// Update the state
			finished = 2;
		}
		else if(finished == 2)
		{
			AUDIO_StopDMA();
			AUDIO_RegisterDMACallback(NULL);
			
			// Set fft results to nothing
			memset(soundBuf.fftResult[0], 0, 16 * sizeof(int));
			memset(soundBuf.fftResult[1], 0, 16 * sizeof(int));
			
			// Set done state
			finished = 3;
			paused = 0;
			started = 0;
		}
		
		return;
	}
	
	// Fill current buffer with next bit of audio
	if(soundSource) 
	{ 
		// Loop until no more data available (if we hit the end first before finishing the resample)		
		int result = 1;
		
		// Loop and add samples after resampling
		while(soundBuf.ringBuf.amount() < OUTPUT_SIZE && result)
		{
			// Grab samples
			s16 samples[INPUT_SIZE];
			int numSamples = INPUT_SIZE / 4;
			
			result = soundSource->renderChunk((uint8_t *)samples, numSamples);
			
			// Resample to add to ring buffer
			Resample(samples, numSamples, soundBuf.eqs, soundSource->getFrequency());
		}
		
		if(!result)
		{
			// Pad end with zeros
			s16 samples[OUTPUT_SIZE];
			memset(samples, 0, OUTPUT_SIZE);
			
			// Enough to write over enough data
			soundBuf.ringBuf.put(samples, OUTPUT_SIZE);
			
			// Remember end state
			paused = 1;
			finished = 1;
			naturalFinish = 1;
		}
		
		// Copy out
		soundBuf.ringBuf.get(soundBuf.outBuffer[soundBuf.currentBuffer], OUTPUT_SIZE);
		
		// Ensure range is updated
		DCFlushRange(soundBuf.outBuffer[soundBuf.currentBuffer], OUTPUT_SIZE);
		
		// FFT
		fft.setAudioData((int16_t *)soundBuf.outBuffer[soundBuf.currentBuffer]);
		fft.getFFT(soundBuf.fftResult[soundBuf.currentBuffer]);
	}
	
	// Remember that we updated
	soundBuf.needsUpdate = 0;
}

int Sound::finishedNaturally()
{
	// Only return if we did actually finish
	if(getSoundState() == SOUND_STOPPED)
	{
		if(naturalFinish)
		{
			// Yup
			return 1;
		}
	}
	
	// Not finished or no natural finish
	return 0;
}

void Sound::resetNaturalFinish()
{
	// To control from filelist to assure we handled the event
	naturalFinish = 0;
}

int Sound::playsFormat(const char *url)
{
	// Get extension
	char ext[10];
	getExtension(url, ext);
	
	// Loop through available sources and find one that works
	int i = 0;
	
	while(availableSources[i])
	{
		// Test to see if it supports it
		if(availableSources[i]->supportsFormat(ext)) { return 1; }
		
		// Go to next source
		i++;
	}
	
	// Couldn't find a valid source
	return 0;
}

int Sound::loadSound(const char *url)
{	
	// Remember that the finish was natural or not
	naturalFinish = 0;
	
	// Grab extensions
	char ext[10];
	getExtension(url, ext);
	
	// Temporary sound source
	SoundSource *tSoundSource = NULL;
	
	// Loop through available sources and find one that works
	int i = 0;
	
	while(availableSources[i])
	{
		// Test to see if it supports it
		if(availableSources[i]->supportsFormat(ext))
		{ 
			// Remember the working source
			tSoundSource = availableSources[i];
			
			// Exit out of loop
			break;
		}
		
		// Go to next source
		i++;
	}
	
	if(!tSoundSource)
	{
		// No renderer
		return 0;
	}
	
	// Close old sound
	closeSound();
	
	// Assign new buffer
	soundSource = tSoundSource;
	
	// Open file
	if(sourceFile.open(url, "rb"))
	{
		// Ensure no blips
		memset(soundBuf.outBuffer[0], 0, OUTPUT_SIZE);
		DCFlushRange(soundBuf.outBuffer[0], OUTPUT_SIZE);
		
		memset(soundBuf.outBuffer[1], 0, OUTPUT_SIZE);
		DCFlushRange(soundBuf.outBuffer[1], OUTPUT_SIZE);
		
		// No sound source
		soundSource = NULL;
		
		// Stop current
		stopSound();
		
		return 0;
	}
	
	// Pass opened file to the sound source
	bool found = false;
	
	while(!found)
	{
		switch(soundSource->openLocation(&sourceFile))
		{
			case OPEN_FAILURE:
				// Error
				closeSound();
				
				return 0;
			case OPEN_SUCCESS:
				// Opened!
				found = true;
				
				break;
			case OPEN_TRYAGAIN:
				// Unhandled
				return 0;
			case OPEN_ANOTHERDECODER:
				// Find new decoder from extension
				char tExt[128];				
				soundSource->getFormat(tExt);
				
				// Ensure no false positives
				soundSource = NULL;
				
				int i = 0;
				
				while(availableSources[i])
				{
					// Test to see if it supports it
					if(availableSources[i]->supportsFormat(tExt))
					{ 
						// Remember the working source
						tSoundSource = availableSources[i];
						
						// Exit out of loop
						break;
					}
					
					// Go to next source
					i++;
				}
				
				if(!tSoundSource)
				{
					// No renderer
					return 0;
				}
				
				// Set renderer and retry loop
				soundSource = tSoundSource;
				break;
		}
	}
	
	// Initialize 3band resampler
	Init3BandState(&soundBuf.eqs[0], 880, 5000, 48000);
	Init3BandState(&soundBuf.eqs[1], 880, 5000, 48000);
	
	// Initialize output ring buffer
	soundBuf.ringBuf.init();
	
	// Set defaults
	started = 0;
	paused = 0;
	finished = 0;
	
	return 1;
}

void Sound::closeSound()
{
	// Stop current
	stopSound();
	
	// Kill current audio if needed (but don't delete)
	if(soundSource) 
	{
		soundSource->close();
	}
	
	// Close audio file
	sourceFile.close();
}

void Sound::playSound()
{
	if(!soundSource) { return; }
	
	if(!started || finished)
	{
		// Start for the first time
		AUDIO_RegisterDMACallback(DMACallBack);
		
		memset(soundBuf.outBuffer[0], 0, OUTPUT_SIZE);
		DCFlushRange(soundBuf.outBuffer[0], OUTPUT_SIZE);
		
		memset(soundBuf.outBuffer[1], 0, OUTPUT_SIZE);
		DCFlushRange(soundBuf.outBuffer[1], OUTPUT_SIZE);
		
		// Mark started
		started = 1;
		finished = 0;
		
		// Be sure we start from beginning
		soundSource->seekSeconds(0);
		
		// Set up fft smoothing
		soundBuf.lastPassedResult = soundBuf.currentBuffer;
		soundBuf.countPassedResult = 0;
		soundBuf.totalPassedResult = 0;
		
		// Remember that the finish was natural or not
		naturalFinish = 0;
		
		// Force a DMA update
		DMACallBack();
		
		// Ensure light doesn't stay on
		light.resetAverage();
	}
	else if(paused)
	{
		// Unpause
		paused = 0;
		
		// Ensure light doesn't stay on
		light.resetAverage();
	}
}

void Sound::stopSound()
{
	// Stop stream
    AUDIO_StopDMA();
	AUDIO_RegisterDMACallback(NULL);
	
	// Set fft results to nothing
	memset(soundBuf.fftResult[0], 0, 16 * sizeof(int));
	memset(soundBuf.fftResult[1], 0, 16 * sizeof(int));
	
	// Ensure light doesn't stay on
	light.resetAverage();
	
	// Set flags to not paused, not started
	started = 0;
	paused = 0;
	finished = 1;
}

void Sound::pauseSound()
{
	// Pause
	paused ^= 1;
	
	// Ensure light doesn't stay on
	light.resetAverage();
}

int Sound::getSoundLength()
{
	// Ensure no null dereference
	if(!soundSource) { return 0; }
	
	return soundSource->getLength();
}

int Sound::getSoundPosition()
{
	// Ensure no null dereference
	if(!soundSource) { return 0; }
	
	// Figure out actual length
	int len = soundSource->tellSeconds();
	
	// Always return -1 if it's the case
	if(len < 0) { return len; }
	
	// Return 0 if sound is stopped
	if(getSoundState() == SOUND_STOPPED) { return 0; }
	
	// Return actual value otherwise
	return len;
}

void Sound::setSoundPosition(int pos)
{
	// Ensure no null dereference
	if(!soundSource) { return; }
	
	soundSource->seekSeconds(pos);
}

void Sound::getNowPlaying(char *str)
{
	// Ensure no null dereference
	if(!soundSource) 
	{ 
		// No bad strings
		str[0] = 0;
		
		return; 
	}
	
	// Return current song (fix this to pull from global format options)
	soundSource->getNowPlaying("", str);
}

SoundState Sound::getSoundState()
{
	if(!soundSource)
	{
		// Sound isn't loaded
		return SOUND_UNLOADED;
	}
	
	if(!started)
	{
		// Not started
		return SOUND_STOPPED;
	}
	
	// Can either be playing or paused by here
	if(paused)
	{
		// It's paused
		return SOUND_PAUSED;
	}
	
	// Playing left
	return SOUND_PLAYING;
}

void Sound::getFFTResult(int *out)
{
	// Grab playing buffer fft
	int tBuffer = soundBuf.currentBuffer ^ 1;
	
	// Use data for light
	light.setLightData(soundBuf.fftResult[tBuffer]);
	
	// Figure out if we need to smooth
	if((soundBuf.totalPassedResult < 2) || (soundBuf.countPassedResult <= (soundBuf.totalPassedResult >> 1)))
	{
		// Copy it
		memcpy(out, soundBuf.fftResult[tBuffer], 16 * sizeof(int));
	}
	else
	{
		// Smooth it
		for(int i = 0; i < 16; i++)
		{
			// Average of the two
			out[i] = (soundBuf.fftResult[tBuffer][i] + soundBuf.fftResult[soundBuf.currentBuffer][i]) >> 1;
		}
	}
	
	// Remember last result
	if(soundBuf.lastPassedResult != soundBuf.currentBuffer)
	{
		// We swapped
		soundBuf.lastPassedResult = soundBuf.currentBuffer;
		
		// Remember how many frames
		soundBuf.totalPassedResult = soundBuf.countPassedResult;
		soundBuf.countPassedResult = 0;
	}
	else
	{
		// Remember this frame
		soundBuf.countPassedResult++;
	}
}

void Sound::enableDiskLight(bool enabled)
{
	// Pass on settings to fft which manages wii light
	light.setFlashEnabled(enabled);
	
	// Remember
	_lenabled = enabled;
}

bool Sound::getDiskLightEnabled()
{
	// return light status
	return _lenabled;
}
