//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose: 
//
// $NoKeywords: $
//
//=============================================================================//
#include "audio_pch.h"
#include "snd_mp3_source.h"
#include "snd_dma.h"
#include "snd_wave_mixer_mp3.h"
#include "filesystem_engine.h"
#include "utldict.h"

// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"

#ifndef DEDICATED  // have to test this because VPC is forcing us to compile this file.

// How many bytes initial data bytes of the mp3 should be saved in the soundcache, in addition to the small amount of
// metadata (playbackrate, etc). This will increase memory usage by
// ( N * number-of-precached-mp3-sounds-in-the-whole-game ) at all times, as the soundcache is held in memory.
//
// Right now we're setting this to zero. The IsReadyToMix() logic at the data layer will delay mixing of the sound until
// it arrives. Setting this to anything above zero, however, will allow the sound to start, so it needs to either be
// enough to cover SND_ASYNC_LOOKAHEAD_SECONDS or none at all.
#define MP3_STARTUP_DATA_SIZE_BYTES 0

CUtlDict< CSentence *, int> g_PhonemeFileSentences;
bool g_bAllPhonemesLoaded;

void PhonemeMP3Shutdown( void )
{
	g_PhonemeFileSentences.PurgeAndDeleteElements();
	g_bAllPhonemesLoaded = false;
}

void AddPhonemesFromFile( const char *pszFileName )
{
	// If all Phonemes are loaded, do not load anymore
	if ( g_bAllPhonemesLoaded && g_PhonemeFileSentences.Count() != 0 )
		return;

	// Empty file name implies stop loading more phonemes
	if ( pszFileName == NULL )
	{
		g_bAllPhonemesLoaded = true;
		return;
	}

	// Load this file
	g_bAllPhonemesLoaded = false;

	CUtlBuffer buf( 0, 0, CUtlBuffer::TEXT_BUFFER );
	if ( g_pFileSystem->ReadFile( pszFileName, "MOD", buf ) )
	{
		while ( 1 )
		{
			char token[4096];
			buf.GetString( token );

			V_FixSlashes( token );

			int iIndex = g_PhonemeFileSentences.Find( token );

			if ( iIndex != g_PhonemeFileSentences.InvalidIndex() )
			{
				delete g_PhonemeFileSentences.Element( iIndex );
				g_PhonemeFileSentences.Remove( token );
			}

			CSentence *pSentence = new CSentence;
			g_PhonemeFileSentences.Insert( token, pSentence );

			buf.GetString( token );

			if ( strlen( token ) <= 0 )
				break;

			if ( !stricmp( token, "{" ) )
			{
				pSentence->InitFromBuffer( buf );
			}
		}
	}
}

CAudioSourceMP3::CAudioSourceMP3( CSfxTable *pSfx )
{
	m_sampleRate = 0;
	m_pSfx = pSfx;
	m_refCount = 0;

	m_dataStart = 0;

	intp file = g_pSndIO->open( pSfx->GetFileName() );
	if ( file != -1 )
	{
		m_dataSize = g_pSndIO->size( file );
		g_pSndIO->close( file );
	}
	else
	{
		// No sound cache, the file isn't here, print this so that the relatively deep failure points that are about to
		// spew make a little more sense
		Warning( "MP3 is completely missing, sound system will be upset to learn of this [ %s ]\n", pSfx->GetFileName() );
		m_dataSize = 0;
	}


	m_nCachedDataSize = 0;
	m_bIsPlayOnce = false;
	m_bIsSentenceWord = false;
	m_bCheckedForPendingSentence = false;
}

CAudioSourceMP3::CAudioSourceMP3( CSfxTable *pSfx, CAudioSourceCachedInfo *info )
{
	m_pSfx = pSfx;
	m_refCount = 0;

	m_sampleRate = info->SampleRate();
	m_dataSize = info->DataSize();
	m_dataStart = info->DataStart();

	m_nCachedDataSize = 0;
	m_bIsPlayOnce = false;
	m_bCheckedForPendingSentence = false;

	CheckAudioSourceCache();
}

CAudioSourceMP3::~CAudioSourceMP3()
{
}

// mixer's references
void CAudioSourceMP3::ReferenceAdd( CAudioMixer * )
{
	m_refCount++;
}

void CAudioSourceMP3::ReferenceRemove( CAudioMixer * )
{
	m_refCount--;
	if ( m_refCount == 0 && IsPlayOnce() )
	{
		SetPlayOnce( false ); // in case it gets used again
		CacheUnload();
	}
}

//-----------------------------------------------------------------------------
// Purpose: 
// Output : Returns true on success, false on failure.
//-----------------------------------------------------------------------------
bool CAudioSourceMP3::IsAsyncLoad()
{
	// If there's a bit of "cached data" then we don't have to lazy/async load (we still async load the remaining data,
	//  but we run from the cache initially)
	return ( m_nCachedDataSize > 0 ) ? false : true;
}

// check reference count, return true if nothing is referencing this
bool CAudioSourceMP3::CanDelete( void )
{
	return m_refCount > 0 ? false : true;
}

//-----------------------------------------------------------------------------
// Purpose: 
// Output : int
//-----------------------------------------------------------------------------
int CAudioSourceMP3::GetType()
{
	return AUDIO_SOURCE_MP3;
}

//-----------------------------------------------------------------------------
void CAudioSourceMP3::SetSentence( CSentence *pSentence )
{
	CAudioSourceCachedInfo *info = m_AudioCacheHandle.FastGet();

	if ( !info )
		return;

	if ( info && info->Sentence() )
		return;

	CSentence *pNewSentence = new CSentence;

	pNewSentence->Append( 0.0f, *pSentence );
	pNewSentence->MakeRuntimeOnly();

	info->SetSentence( pNewSentence );
}

int CAudioSourceMP3::SampleRate()
{
	if ( !m_sampleRate )
	{
		// This should've come from the sound cache. We can avoid sync I/O jank if and only if we've started streaming
		// data already for some other reason. (Despite the name, CreateWaveDataMemory is just creating a wrapper class
		// that manages access to the wave data cache)
		IWaveData *pData = CreateWaveDataMemory( *this );
		if ( !pData->IsReadyToMix() && SND_IsInGame() )
		{
			// If you hit this, you're creating a sound source that isn't in the sound cache, and asking for its sample
			// rate before it has streamed enough data in to read it from the underlying file. Your options are:
			// - Rebuild sound cache or figure out why this sound wasn't included.
			// - Precache this sound at level load so this doesn't happen during gameplay.
			// - Somehow call CacheLoad() on this source earlier so it has time to get data into memory so the data
			//   shows up as IsReadyToMix here, and this crutch won't jank.
			Warning( "MP3 initialized with no sound cache, this may cause janking. [ %s ]\n", GetFileName() );
			// The code below will still go fine, but the mixer will emit a jank warning that the data wasn't ready and
			// do sync I/O
		}
		CAudioMixerWaveMP3 *pMixer = new CAudioMixerWaveMP3( pData );
		m_sampleRate = pMixer->GetStreamOutputRate();
		// pData ownership is passed to, and free'd by, pMixer
		delete pMixer;
	}
	return m_sampleRate;
}

void CAudioSourceMP3::GetCacheData( CAudioSourceCachedInfo *info )
{
	// Don't want to replicate our cached sample rate back into the new cache, ensure we recompute it.
	CAudioMixerWaveMP3 *pTempMixer = new CAudioMixerWaveMP3( CreateWaveDataMemory(*this) );
	m_sampleRate = pTempMixer->GetStreamOutputRate();
	delete pTempMixer;

	AssertMsg( m_sampleRate, "Creating cache with invalid sample rate data" );
	if ( !m_sampleRate )
	{
		Warning( "Failed to find sample rate creating cache data for MP3, cache will be invalid [ %s ]\n", GetFileName() );
	}

	info->SetSampleRate( m_sampleRate );
	info->SetDataStart( 0 );

	intp file = g_pSndIO->open( m_pSfx->GetFileName() );
	if ( !file )
	{
		Warning( "Failed to find file for building soundcache [ %s ]\n", m_pSfx->GetFileName() );
		// Don't re-use old cached value
		m_dataSize = 0;
	}
	else
	{
		m_dataSize = (int)g_pSndIO->size( file );
	}

	Assert( m_dataSize > 0 );

	// Do we need to actually load any audio data?
#if MP3_STARTUP_DATA_SIZE_BYTES > 0 // We may have defined the startup data to nothingness
	if ( info->s_bIsPrecacheSound && m_dataSize > 0 )
	{
		// Ideally this would mimic the wave startup data code and figure out this calculation:
		//   int bytesNeeded = m_channels * ( m_bits >> 3 ) * m_rate * SND_ASYNC_LOOKAHEAD_SECONDS;
		// (plus header)
		int dataSize = min( MP3_STARTUP_DATA_SIZE_BYTES, m_dataSize );
		byte *data = new byte[ dataSize ]();
		int readSize = g_pSndIO->read( data, dataSize, file );
		if ( readSize != dataSize )
		{
			Warning( "Building soundcache, expected %i bytes of data but got %i [ %s ]\n", dataSize, readSize, m_pSfx->GetFileName() );
			dataSize = readSize;
		}
		info->SetCachedDataSize( dataSize );
		info->SetCachedData( data );
	}
#endif // MP3_STARTUP_DATA_SIZE_BYTES > 0

	g_pSndIO->close( file );

	// Data size gets computed in GetStartupData!!!
	info->SetDataSize( m_dataSize );
}

//-----------------------------------------------------------------------------
// Purpose: 
// Output : char const
//-----------------------------------------------------------------------------
char const *CAudioSourceMP3::GetFileName()
{
	return m_pSfx ? m_pSfx->GetFileName() : "NULL m_pSfx";
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void CAudioSourceMP3::CheckAudioSourceCache()
{
	Assert( m_pSfx );

	if ( !m_pSfx->IsPrecachedSound() )
	{
		return;
	}

	// This will "re-cache" this if it's not in this level's cache already
	m_AudioCacheHandle.Get( GetType(), true, m_pSfx, &m_nCachedDataSize );
}

//-----------------------------------------------------------------------------
// Purpose: NULL the wave data pointer (we haven't loaded yet)
//-----------------------------------------------------------------------------
CAudioSourceMP3Cache::CAudioSourceMP3Cache( CSfxTable *pSfx ) : 
	CAudioSourceMP3( pSfx )
{
	m_hCache = 0;
}

CAudioSourceMP3Cache::CAudioSourceMP3Cache( CSfxTable *pSfx, CAudioSourceCachedInfo *info ) :
	CAudioSourceMP3( pSfx, info )
{
	m_hCache = 0;

	m_dataSize = info->DataSize();
	m_dataStart = info->DataStart();

	m_bNoSentence = false;
}

//-----------------------------------------------------------------------------
// Purpose: Free any wave data we've allocated
//-----------------------------------------------------------------------------
CAudioSourceMP3Cache::~CAudioSourceMP3Cache( void )
{
	CacheUnload();
}

int CAudioSourceMP3Cache::GetCacheStatus( void )
{
	bool bCacheValid;
	int loaded = wavedatacache->IsDataLoadCompleted( m_hCache, &bCacheValid ) ? AUDIO_IS_LOADED : AUDIO_NOT_LOADED;
	if ( !bCacheValid )
	{
		wavedatacache->RestartDataLoad( &m_hCache, m_pSfx->GetFileName(), m_dataSize, m_dataStart );
	}
	return loaded;
}


void CAudioSourceMP3Cache::CacheLoad( void )
{
	// Commence lazy load?
	if ( m_hCache != 0 )
	{
		GetCacheStatus();
		return;
	}

	m_hCache = wavedatacache->AsyncLoadCache( m_pSfx->GetFileName(), m_dataSize, m_dataStart );
}

void CAudioSourceMP3Cache::CacheUnload( void )
{
	if ( m_hCache != 0 )
	{
		wavedatacache->Unload( m_hCache );
	}
}

char *CAudioSourceMP3Cache::GetDataPointer( void )
{
	char *pMP3Data = NULL;
	bool dummy = false;

	if ( m_hCache == 0 )
	{
		CacheLoad();
	}

	wavedatacache->GetDataPointer( 
		m_hCache, 
		m_pSfx->GetFileName(), 
		m_dataSize, 
		m_dataStart, 
		(void **)&pMP3Data, 
		0, 
		&dummy );

	return pMP3Data;
}

int CAudioSourceMP3Cache::GetOutputData( void **pData, int samplePosition, int sampleCount, char copyBuf[AUDIOSOURCE_COPYBUF_SIZE] )
{
	// how many bytes are available ?
	int totalSampleCount = m_dataSize - samplePosition;

	// may be asking for a sample out of range, clip at zero
	if ( totalSampleCount < 0 )
		totalSampleCount = 0;

	// clip max output samples to max available
	if ( sampleCount > totalSampleCount )
		sampleCount = totalSampleCount;

	// if we are returning some samples, store the pointer
	if ( sampleCount )
	{
		// Starting past end of "preloaded" data, just use regular cache
		if ( samplePosition >= m_nCachedDataSize )
		{
			*pData = GetDataPointer();
		}
		else
		{
			// Start async loader if we haven't already done so
			CacheLoad();

			// Return less data if we are about to run out of uncached data
			if ( samplePosition + sampleCount >= m_nCachedDataSize )
			{
				sampleCount = m_nCachedDataSize - samplePosition;
			}

			// Point at preloaded/cached data from .cache file for now
			*pData = GetCachedDataPointer();
		}

		if ( *pData )
		{
			*pData = (char *)*pData + samplePosition;
		}
		else
		{
			// Out of data or file i/o problem
			sampleCount = 0;
		}
	}

	return sampleCount;
}

CAudioMixer	*CAudioSourceMP3Cache::CreateMixer( int initialStreamPosition )
{
	CAudioMixer *pMixer = new CAudioMixerWaveMP3( CreateWaveDataMemory(*this) );

	return pMixer;
}

CSentence *CAudioSourceMP3Cache::GetSentence( void )
{
	// Already checked and this wav doesn't have sentence data...
	if ( m_bNoSentence == true )
	{
		return NULL;
	}

	// Look up sentence from cache
	CAudioSourceCachedInfo *info = m_AudioCacheHandle.FastGet();
	if ( !info )
	{
		info = m_AudioCacheHandle.Get( CAudioSource::AUDIO_SOURCE_WAV, m_pSfx->IsPrecachedSound(), m_pSfx, &m_nCachedDataSize );
	}
	Assert( info );
	if ( !info )
	{
		m_bNoSentence = true;
		return NULL;
	}

	CSentence *sentence = info->Sentence();
	if ( !sentence )
	{
		if ( !m_bCheckedForPendingSentence )
		{
			int iSentence = g_PhonemeFileSentences.Find( m_pSfx->GetFileName() );
			if ( iSentence != g_PhonemeFileSentences.InvalidIndex() )
			{
				sentence = g_PhonemeFileSentences.Element( iSentence );
				SetSentence( sentence );
			}
			m_bCheckedForPendingSentence = true;
		}
	}

	if ( !sentence )
	{
		m_bNoSentence = true;
		return NULL;
	}

	if ( sentence->m_bIsValid )
	{
		return sentence;
	}

	m_bNoSentence = true;

	return NULL;
}

//-----------------------------------------------------------------------------
// CAudioSourceStreamMP3
//-----------------------------------------------------------------------------
CAudioSourceStreamMP3::CAudioSourceStreamMP3( CSfxTable *pSfx ) :
	CAudioSourceMP3( pSfx )
{
}

CAudioSourceStreamMP3::CAudioSourceStreamMP3( CSfxTable *pSfx, CAudioSourceCachedInfo *info ) :
	CAudioSourceMP3( pSfx, info )
{
	m_dataSize = info->DataSize();
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void CAudioSourceStreamMP3::Prefetch()
{
	PrefetchDataStream( m_pSfx->GetFileName(), 0, m_dataSize );
}

CAudioMixer	*CAudioSourceStreamMP3::CreateMixer( int intialStreamPosition )
{
	// BUGBUG: Source constructs the IWaveData, mixer frees it, fix this?
	IWaveData *pWaveData = CreateWaveDataStream( *this, static_cast<IWaveStreamSource *>( this ), m_pSfx->GetFileName(), 0, m_dataSize, m_pSfx, 0 );
	if ( pWaveData )
	{
		CAudioMixer *pMixer = new CAudioMixerWaveMP3( pWaveData );
		if ( pMixer )
		{
			if ( !m_bCheckedForPendingSentence )
			{
				int iSentence = g_PhonemeFileSentences.Find( m_pSfx->GetFileName() );

				if ( iSentence != g_PhonemeFileSentences.InvalidIndex() )
				{
					SetSentence( g_PhonemeFileSentences.Element( iSentence ) );
				}

				m_bCheckedForPendingSentence = true;
			}

			return pMixer;
		}

		// no mixer but pWaveData was deleted in mixer's destructor
		// so no need to delete
	}

	return NULL;
}

int	CAudioSourceStreamMP3::GetOutputData( void **pData, int samplePosition, int sampleCount, char copyBuf[AUDIOSOURCE_COPYBUF_SIZE] )
{
	return 0;
}

bool Audio_IsMP3( const char *pName )
{
	int len = strlen(pName);
	if ( len > 4 )
	{
		if ( !Q_strnicmp( &pName[len - 4], ".mp3", 4 ) )
		{
			return true;
		}
	}
	return false;
}


CAudioSource *Audio_CreateStreamedMP3( CSfxTable *pSfx )
{
	CAudioSourceStreamMP3 *pMP3 = NULL; 	
	CAudioSourceCachedInfo *info = audiosourcecache->GetInfo( CAudioSource::AUDIO_SOURCE_MP3, pSfx->IsPrecachedSound(), pSfx );
	if ( info )
	{
		pMP3 = new CAudioSourceStreamMP3( pSfx, info );
	}
	else
	{
		pMP3 = new CAudioSourceStreamMP3( pSfx );
	}
	return pMP3;
}


CAudioSource *Audio_CreateMemoryMP3( CSfxTable *pSfx )
{
	CAudioSourceMP3Cache *pMP3 = NULL;
	CAudioSourceCachedInfo *info = audiosourcecache->GetInfo( CAudioSource::AUDIO_SOURCE_MP3, pSfx->IsPrecachedSound(), pSfx );
	if ( info )
	{
		pMP3 = new CAudioSourceMP3Cache( pSfx, info );
	}
	else
	{
		pMP3 = new CAudioSourceMP3Cache( pSfx );
	}
	return pMP3;
}

#endif