//////////////////////////////////////////////////////////////////////////////
//
// Asura_XAudio2_Stream.cpp
//
// Copyright (c) 2008 Rebellion. All rights reserved.
//
//////////////////////////////////////////////////////////////////////////////

/******************************************************************************/
/* Included Files                                                             */
/******************************************************************************/

#include "Core/Asura.h"

#if defined ASURA_USE_AUDIO_SYSTEM

#include "Sound/Asura_XAudio2_Stream.h"

// Asura Platform-independent
#include "ChunkSystem/Asura_Chunk_Stream.h"
#include "Core/Asura_Multithread.h"
#include "Core/Memory/Asura_Mem.h"
#include "Core/Threads/Asura_ProducerConsumerQueue.h"
#include "Debugging/Asura_Assert.h"
#include "FileAccess/Asura_FileAccess.h"
#include "Sound/Asura_XAudio2.h"
#include "Sound/Asura_XAudio2_Buffer.h"

// Misc. This is only needed for the version check on XAUDIO2_BUFFER::pAudioData, and can be
// removed once we're all above the November 2008 SDKs
#if defined _XBOX
	#include <xdk.h>
#else
	#include "Core/Asura_MemoryManagement_DebugOff.h"
	#include <d3dx9.h>
	#include "Core/Asura_MemoryManagement_DebugOn.h"
#endif

/******************************************************************************/
/* Global Variables                                                           */
/******************************************************************************/

static Asura_ProducerConsumerQueue g_xStreamingQueue(4);

/******************************************************************************/
/* Asura_XAudio2_Stream Class Functions                                       */
/******************************************************************************/

////////////////////////////////////////////////////////////////////////////////
//
// Asura_XAudio2_Stream::Asura_XAudio2_Stream()
//
// Constructor. Note that these are stored in a tracking handle pool, so this
// gets executed only once, when the pool is created
//
////////////////////////////////////////////////////////////////////////////////

Asura_XAudio2_Stream::Asura_XAudio2_Stream()
{
	Asura_Mem::Set( &m_xFmt, 0, sizeof( m_xFmt ) );

	for( u_int u = 0; u < 2; ++u )
	{
		// For now, assume we'll always be streaming XMA2 data on 360, so allocate physical memory
		m_apAudioData[u] = Asura_XAudio2::Platform_Allocate( WAVE_FORMAT_XMA2,
			ASURA_AUDIO_STREAMING_BUFFER_SIZE );

		Asura_Assert( m_apAudioData[u], "Failed to allocate streaming audio buffer." );
	}

	Reset();
}

////////////////////////////////////////////////////////////////////////////////
//
// Asura_XAudio2_Stream::~Asura_XAudio2_Stream()
//
// Destructor
//
////////////////////////////////////////////////////////////////////////////////

Asura_XAudio2_Stream::~Asura_XAudio2_Stream()
{
	for( u_int u = 0; u < 2; ++u )
	{
		Asura_XAudio2::Platform_Deallocate( WAVE_FORMAT_XMA2, m_apAudioData[u] );
	}

	CloseFile();
}

////////////////////////////////////////////////////////////////////////////////
//
// Asura_XAudio2_Stream::Reset()
//
// Reinitialises the members of a stream. Called instead of placement new when
// allocating new streams from the pool. That's because we don't want to redo
// the allocation of the buffers
//
////////////////////////////////////////////////////////////////////////////////

void Asura_XAudio2_Stream::Reset()
{
	m_pxSourceVoice 				= NULL;
	m_uCurrBuffer   				= 0;
	m_uStartLocation				= 0;
	m_uBytesLeft    				= 0;
	m_uTotalDataSize				= 0;
	m_uBytesTillSeek				= 0;
	m_uSeekTarget   				= 0;
	m_iFileHandle   				= ASURA_INVALID_HANDLE;
	m_bLooping    					= false;
	m_bSeekPending					= false;
	m_bReadPending					= false;
	m_bReleased   					= false;
}

////////////////////////////////////////////////////////////////////////////////
//
// Asura_XAudio2_Stream::OpenFile()
//
// Opens a wave file for streaming, parsing its header to get ready for playback
//
////////////////////////////////////////////////////////////////////////////////

bool Asura_XAudio2_Stream::OpenFile( const char* const szFilename, const u_int uOffset /*= 0*/ )
{
	m_iFileHandle = Asura_FileAccess::Open( szFilename );
	if( m_iFileHandle == ASURA_INVALID_HANDLE ) return false;

	Asura_Chunk_File_Stream xStream( m_iFileHandle );
	xStream.Skip( uOffset );

	Asura_XAudio2_WaveReader xReader( xStream, m_xFmt, m_uTotalDataSize );
	const bool bSuccess = xReader.ReadWave();

	m_uBytesLeft = m_uTotalDataSize;
	m_uStartLocation = xStream.GetPosition();

	return bSuccess;
}

////////////////////////////////////////////////////////////////////////////////
//
// Asura_XAudio2_Stream::CloseFile()
//
// Closes a streaming wave file
//
////////////////////////////////////////////////////////////////////////////////

void Asura_XAudio2_Stream::CloseFile()
{
	if( m_iFileHandle != ASURA_INVALID_HANDLE )
	{
		Asura_FileAccess::Close( m_iFileHandle );
		m_iFileHandle = ASURA_INVALID_HANDLE;
	}
}

////////////////////////////////////////////////////////////////////////////////
//
// Asura_XAudio2_Stream::Bind()
//
// Binds a stream to a given XAudio2 voice, so it knows where to submit
// subsequent buffers
//
////////////////////////////////////////////////////////////////////////////////

void Asura_XAudio2_Stream::Bind( IXAudio2SourceVoice* const pxVoice, const float fTimeOffset )
{
	m_pxSourceVoice = pxVoice;

	const u_int uSkipBytes = SecondsToBytes( fTimeOffset );
	if( uSkipBytes > 0 )
	{
		Asura_FileAccess::SkipData( m_iFileHandle, uSkipBytes );
		m_uBytesLeft -= uSkipBytes;
	}

	// Fill the first two buffers from the file
	Read();
	Read();
}

////////////////////////////////////////////////////////////////////////////////
//
// Asura_XAudio2_Stream::SeekAtPlaybackTime()
//
// Sets up a seek at a point in the future on an audio stream
//
////////////////////////////////////////////////////////////////////////////////

bool Asura_XAudio2_Stream::SeekAtPlaybackTime( const float fSeekTime, const float fPlaybackTime )
{
	// AUDIOTODO: use a mutex here to prevent the streaming thread accessing at the same time.
	// Or, rewrite using a state approach like PS3

	int iBytesTillSeek = static_cast<int>( SecondsToBytes( fPlaybackTime ) ) - (m_uTotalDataSize - m_uBytesLeft);
	Asura_Assert( iBytesTillSeek > 0, "Not enough warning given before seek" );

	iBytesTillSeek = Asura_Maths::Max( iBytesTillSeek, 0 );

	if( iBytesTillSeek >= 0 )
	{
		m_uSeekTarget = SecondsToBytes( fSeekTime );	
		m_uBytesTillSeek = static_cast<u_int>( iBytesTillSeek );
		m_bSeekPending = true;

		return true;
	}

	return false;
}

////////////////////////////////////////////////////////////////////////////////
//
// Asura_XAudio2_Stream::Read()
//
// Reads the next buffer's worth of data from a streaming wave
//
////////////////////////////////////////////////////////////////////////////////

void Asura_XAudio2_Stream::Read()
{
	// Lock mutex. Don't want another thread to destroy the voice during this process
	Asura_ScopedMutex xScopedLock( m_xMutex );

	void* const pBuffer = m_apAudioData[ m_uCurrBuffer ];

	if( m_bReleased || !m_pxSourceVoice || !pBuffer )
	{
		m_bReadPending = false;
		return;
	}

	int iSamplesSeeked = 0;
	int iSeekTarget = -1;
	if( m_uBytesLeft == 0 )
	{
		if( m_bLooping )
		{
			iSamplesSeeked = -( static_cast<int>( BytesToSeconds( m_uTotalDataSize ) ) * static_cast<int>( Asura_XAudio2::GetOutputSampleRate() ) );

			iSeekTarget = m_uStartLocation;
			m_uBytesLeft = m_uTotalDataSize;
		}
	}

	if( m_uBytesTillSeek == 0 && m_bSeekPending )
	{
		if( m_uSeekTarget > ( m_uTotalDataSize - m_uBytesLeft ) )
		{
			// Seek forward
			iSamplesSeeked = static_cast<int>( BytesToSeconds( m_uSeekTarget - ( m_uTotalDataSize - m_uBytesLeft ) ) ) * static_cast<int>( Asura_XAudio2::GetOutputSampleRate() );
		}
		else
		{
			// Seek backwards
			iSamplesSeeked = -( static_cast<int>( BytesToSeconds( ( m_uTotalDataSize - m_uBytesLeft ) - m_uSeekTarget ) ) * static_cast<int>( Asura_XAudio2::GetOutputSampleRate() ) );
		}

		iSeekTarget = m_uStartLocation + m_uSeekTarget;
		m_uBytesLeft = m_uTotalDataSize - m_uSeekTarget;
	}

	if( iSeekTarget >= 0 )
	{
#if defined ASURA_ASSERT
		const bool bSuccess =
#endif
			Asura_FileAccess::SeekFile( m_iFileHandle, iSeekTarget );

		Asura_Assert( bSuccess, "Failed to seek in audio stream." );

		m_bSeekPending = false;
	}

	u_int uBytesToRead = ASURA_AUDIO_STREAMING_BUFFER_SIZE;
	// align to the bock size
	uBytesToRead -= uBytesToRead % m_xFmt.m_xWFX.Format.nBlockAlign;
	uBytesToRead = Asura_Maths::Min( uBytesToRead, m_uBytesLeft );

	if( m_bSeekPending )
	{
		if(		( m_uBytesTillSeek < uBytesToRead ) 
			&&	( ( m_uBytesTillSeek % GetMinimumReadSize() ) != 0 ) ) 
		{
			// Round our buffer down to a multiple of the minimum read size
			m_uBytesTillSeek -= m_uBytesTillSeek % GetMinimumReadSize();
		}

		uBytesToRead = Asura_Maths::Min( uBytesToRead, m_uBytesTillSeek );
	}

	if( uBytesToRead > 0 )
	{
		// Asura_FileAccess doesn't like being asked to read 0 bytes
		if( !Asura_FileAccess::ReadData( m_iFileHandle, pBuffer, uBytesToRead ) )
		{
			Asura_Assert( false, "Failed to read from streaming audio file." );
			m_bReadPending = false;

			return;
		}

		m_uBytesLeft -= uBytesToRead;
		m_uBytesTillSeek -= uBytesToRead;

		// Submit the buffer again

		XAUDIO2_BUFFER xBufferParams = {0};

		xBufferParams.Flags = 0;
		xBufferParams.AudioBytes = uBytesToRead;

		// Use the context to store the time we have seeked this buffer so that when we get the callback 
		// for the buffer starting we can offset our sample counter
		xBufferParams.pContext = reinterpret_cast<int*>( iSamplesSeeked );

		#if defined NOVEMBER_2008_SDK
			xBufferParams.pAudioData = static_cast< BYTE* >( pBuffer );
		#else
			xBufferParams.pAudioData = static_cast< const BYTE* >( pBuffer );
		#endif

		#if defined ASURA_ASSERT
			const HRESULT xResult =
		#endif
			m_pxSourceVoice->SubmitSourceBuffer( &xBufferParams /*, wmaparams*/ );

		Asura_Assert( SUCCEEDED( xResult ), "Failed to submit streaming source buffer." );
	}

	m_uCurrBuffer = 1 - m_uCurrBuffer;
	m_bReadPending = false;
}

////////////////////////////////////////////////////////////////////////////////
//
// Asura_XAudio2_Stream::SecondsToBytes()
//
// Converts a time in seconds to a byte offset within the wave
//
////////////////////////////////////////////////////////////////////////////////

u_int Asura_XAudio2_Stream::SecondsToBytes( const float fSecs ) const
{
	const float fBytesPerSec = static_cast< float >( m_xFmt.m_xWFX.Format.nAvgBytesPerSec );
	const float fBytes = fSecs * fBytesPerSec;
	const u_int uBytes = Asura_Maths::FloatToInt( fBytes );

	// Round down to the minimum read size
	return ( uBytes - uBytes % GetMinimumReadSize() );
}

////////////////////////////////////////////////////////////////////////////////
//
// Asura_XAudio2_Stream::BytesToSeconds()
//
// Converts a byte offset within the wave to a time in seconds
//
////////////////////////////////////////////////////////////////////////////////

float Asura_XAudio2_Stream::BytesToSeconds( const u_int uBytes ) const
{
	const float fBytesPerSec = static_cast< float >( m_xFmt.m_xWFX.Format.nAvgBytesPerSec );
	const float fBytes = static_cast< float >( uBytes );

	return fBytes / fBytesPerSec;
}

////////////////////////////////////////////////////////////////////////////////
//
// Asura_XAudio2_Stream::EnqueueRead()
//
// Enqueues a read, to be performed some time later by another thread
//
////////////////////////////////////////////////////////////////////////////////

void Asura_XAudio2_Stream::EnqueueRead()
{
	if( g_xStreamingQueue.TryToAddToQueue( ReadFromStream, this, sizeof( *this ) ) )
	{
		m_bReadPending = true;
	}
	else
	{
		Asura_Assert( false, "Failed to add audio streaming call to queue." );
	}
}

////////////////////////////////////////////////////////////////////////////////
//
// Asura_XAudio2_Stream::GetMinimumReadSize()
//
// Return the minimum read size (in bytes) for valid data, for example, the 
// block size for XMA's, or the sameple size for PCM
//
////////////////////////////////////////////////////////////////////////////////

int Asura_XAudio2_Stream::GetMinimumReadSize () const
{
	switch( m_xFmt.m_xWFX.Format.wFormatTag )
	{
	case WAVE_FORMAT_PCM:	return static_cast<int>( m_xFmt.m_xWFX.Format.nBlockAlign );
	case WAVE_FORMAT_XMA2:	return static_cast<int>( m_xFmt.m_xXMA2.BytesPerBlock );
	case WAVE_FORMAT_ADPCM:	return static_cast<int>( m_xFmt.m_xADPCM.wSamplesPerBlock * m_xFmt.m_xADPCM.wfx.nBlockAlign );
	}

	Asura_Assert(false, "Unexpected format tag (does this stream contain valid data?)" );

	return 0;
}

/******************************************************************************/
/* Asura_XAudio2_Stream Static Class Function                                 */
/******************************************************************************/

////////////////////////////////////////////////////////////////////////////////
//
// Asura_XAudio2_Stream::ReadFromStream()
//
// Static wrapper for Asura_XAudio2_Stream::Read(), so it can be called from a
// producer consumer queue
//
////////////////////////////////////////////////////////////////////////////////

void Asura_XAudio2_Stream::ReadFromStream( void* pContext, u_int uDataSize )
{
	Asura_Assert( pContext, "NULL pointer passed to Asura_XAudio2_Stream::ReadFromStream()." );
	if( !pContext ) return;

	static_cast< Asura_XAudio2_Stream* >( pContext )->Read();
}

#endif // defined ASURA_USE_AUDIO_SYSTEM
