//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose: 
//
//===========================================================================//

#include "audio_pch.h"

#if !DEDICATED

#include "tier0/dynfunction.h"
#include "video/ivideoservices.h"
#include "sys_dll.h"

// prevent some conflicts in SDL headers...
#undef M_PI
#include <stdint.h>
#ifndef _STDINT_H_
#define _STDINT_H_ 1
#endif

#include "SDL.h"

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

extern bool snd_firsttime;
extern bool MIX_ScaleChannelVolume( paintbuffer_t *ppaint, channel_t *pChannel, int volume[CCHANVOLUMES], int mixchans );
extern void S_SpatializeChannel( /*int nSlot,*/ int volume[6], int master_vol, const Vector *psourceDir, float gain, float mono );

// 64K is about 1/3 second at 16-bit, stereo, 44100 Hz
// 44k: UNDONE - need to double buffers now that we're playing back at 44100?
#define	WAV_BUFFERS             64
#define	WAV_MASK				(WAV_BUFFERS - 1)
#define	WAV_BUFFER_SIZE			0x0400

#if 0
#define debugsdl printf
#else
static inline void debugsdl(const char *fmt, ...) {}
#endif


//-----------------------------------------------------------------------------
//
// NOTE: This only allows 16-bit, stereo wave out  (!!! FIXME: but SDL supports 7.1, etc, too!)
//
//-----------------------------------------------------------------------------
class CAudioDeviceSDLAudio : public CAudioDeviceBase
{
public:
	CAudioDeviceSDLAudio();
	virtual ~CAudioDeviceSDLAudio();

	bool		IsActive( void );
	bool		Init( void );
	void		Shutdown( void );
	void		PaintEnd( void );
	int			GetOutputPosition( void );
	void		ChannelReset( int entnum, int channelIndex, float distanceMod );
	void		Pause( void );
	void		UnPause( void );
	float		MixDryVolume( void );
	bool		Should3DMix( void );
	void		StopAllSounds( void );

	int			PaintBegin( float mixAheadTime, int soundtime, int paintedtime );
	void		ClearBuffer( void );
	void		MixBegin( int sampleCount );
	void		MixUpsample( int sampleCount, int filtertype );
	void		Mix8Mono( channel_t *pChannel, char *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress );
	void		Mix8Stereo( channel_t *pChannel, char *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress );
	void		Mix16Mono( channel_t *pChannel, short *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress );
	void		Mix16Stereo( channel_t *pChannel, short *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress );

	void		TransferSamples( int end );
	void		SpatializeChannel( int nSlot, int volume[CCHANVOLUMES/2], int master_vol, const Vector& sourceDir, float gain, float mono);
	void		ApplyDSPEffects( int idsp, portable_samplepair_t *pbuffront, portable_samplepair_t *pbufrear, portable_samplepair_t *pbufcenter, int samplecount );

	const char *DeviceName( void )			{ return "SDL"; }
	int			DeviceChannels( void )		{ return 2; }
	int			DeviceSampleBits( void )	{ return 16; }
	int			DeviceSampleBytes( void )	{ return 2; }
	int			DeviceDmaSpeed( void )		{ return SOUND_DMA_SPEED; }
	int			DeviceSampleCount( void )	{ return m_deviceSampleCount; }

private:
	SDL_AudioDeviceID m_devId;

	static void SDLCALL AudioCallbackEntry(void *userdata, Uint8 * stream, int len);
	void AudioCallback(Uint8 *stream, int len);

	void	OpenWaveOut( void );
	void	CloseWaveOut( void );
	void	AllocateOutputBuffers();
	void	FreeOutputBuffers();
	bool	ValidWaveOut( void ) const;

	int			m_deviceSampleCount;

	int			m_buffersSent;
	int			m_pauseCount;
	int			m_readPos;
	int			m_partialWrite;

	// Memory for the wave data
	uint8_t		*m_pBuffer;
};

static CAudioDeviceSDLAudio *g_wave = NULL;

//-----------------------------------------------------------------------------
// Constructor (just lookup SDL entry points, real work happens in this->Init())
//-----------------------------------------------------------------------------
CAudioDeviceSDLAudio::CAudioDeviceSDLAudio()
{
	m_devId = 0;
}

//-----------------------------------------------------------------------------
// Destructor. Make sure our global pointer gets set to NULL.
//-----------------------------------------------------------------------------
CAudioDeviceSDLAudio::~CAudioDeviceSDLAudio()
{
	g_wave = NULL;
}

//-----------------------------------------------------------------------------
// Class factory
//-----------------------------------------------------------------------------
IAudioDevice *Audio_CreateSDLAudioDevice( void )
{
	if ( !g_wave )
	{
		g_wave = new CAudioDeviceSDLAudio;
		Assert( g_wave );
	}

	if ( g_wave && !g_wave->Init() )
	{
		delete g_wave;
		g_wave = NULL;
	}

	return g_wave;
}


//-----------------------------------------------------------------------------
// Init, shutdown
//-----------------------------------------------------------------------------
bool CAudioDeviceSDLAudio::Init( void )
{
	// If we've already got a device open, then return. This allows folks to call
	//	Audio_CreateSDLAudioDevice() multiple times. CloseWaveOut() will free the
	//  device, and set m_devId to 0.
	if( m_devId )
		return true;

	m_bSurround = false;
	m_bSurroundCenter = false;
	m_bHeadphone = false;
	m_buffersSent = 0;
	m_pauseCount = 0;
	m_pBuffer = NULL;
	m_readPos = 0;
	m_partialWrite = 0;
	m_devId = 0;

	OpenWaveOut();

	if ( snd_firsttime )
	{
		DevMsg( "Wave sound initialized\n" );
	}

	return ValidWaveOut();
}

void CAudioDeviceSDLAudio::Shutdown( void )
{
	CloseWaveOut();
}


//-----------------------------------------------------------------------------
// WAV out device
//-----------------------------------------------------------------------------
inline bool CAudioDeviceSDLAudio::ValidWaveOut( void ) const 
{ 
	return m_devId != 0;
}


//-----------------------------------------------------------------------------
// Opens the windows wave out device
//-----------------------------------------------------------------------------
void CAudioDeviceSDLAudio::OpenWaveOut( void )
{
	debugsdl("SDLAUDIO: OpenWaveOut...\n");

#ifndef WIN32
	char appname[ 256 ];
	KeyValues *modinfo = new KeyValues( "ModInfo" );

	if ( modinfo->LoadFromFile( g_pFileSystem, "gameinfo.txt" ) )
		Q_strncpy( appname, modinfo->GetString( "game" ), sizeof( appname ) );
	else
		Q_strncpy( appname, "Source1 Game", sizeof( appname ) );

	modinfo->deleteThis();
	modinfo = NULL;

	// Set these environment variables, in case we're using PulseAudio.
	setenv("PULSE_PROP_application.name", appname, 1);
	setenv("PULSE_PROP_media.role", "game", 1);
#endif

	// !!! FIXME: specify channel map, etc
	// !!! FIXME: set properties (role, icon, etc).

	//#define SDLAUDIO_FAIL(fnstr) do { DevWarning(fnstr " failed"); CloseWaveOut(); return; } while (false)
	//#define SDLAUDIO_FAIL(fnstr) do { printf("SDLAUDIO: " fnstr " failed: %s\n", SDL_GetError ? SDL_GetError() : "???"); CloseWaveOut(); return; } while (false)
	#define SDLAUDIO_FAIL(fnstr) do { const char *err = SDL_GetError(); printf("SDLAUDIO: " fnstr " failed: %s\n", err ? err : "???"); CloseWaveOut(); return; } while (false)

	if (!SDL_WasInit(SDL_INIT_AUDIO))
	{
		if (SDL_InitSubSystem(SDL_INIT_AUDIO))
			SDLAUDIO_FAIL("SDL_InitSubSystem(SDL_INIT_AUDIO)");
	}

	debugsdl("SDLAUDIO: Using SDL audio target '%s'\n", SDL_GetCurrentAudioDriver());

	// Open an audio device...
	//  !!! FIXME: let user specify a device?
	// !!! FIXME: we can handle quad, 5.1, 7.1, etc here.
	SDL_AudioSpec desired, obtained;
	memset(&desired, '\0', sizeof (desired));
	desired.freq = SOUND_DMA_SPEED;
	desired.format = AUDIO_S16SYS;
	desired.channels = 2;
	desired.samples = 2048;
	desired.callback = &CAudioDeviceSDLAudio::AudioCallbackEntry;
	desired.userdata = this;
	m_devId = SDL_OpenAudioDevice(NULL, 0, &desired, &obtained, SDL_AUDIO_ALLOW_ANY_CHANGE);

	if (!m_devId)
		SDLAUDIO_FAIL("SDL_OpenAudioDevice()");

	#undef SDLAUDIO_FAIL

	// We're now ready to feed audio data to SDL!
	AllocateOutputBuffers();
	SDL_PauseAudioDevice(m_devId, 0);

#if defined( BINK_VIDEO ) && defined( LINUX )
	// Tells Bink to use SDL for its audio decoding
	if ( g_pVideo != NULL) 
	{
		g_pVideo->SoundDeviceCommand( VideoSoundDeviceOperation::SET_SDL_PARAMS, NULL, (void *)&obtained );
	
	}
#endif
}

//-----------------------------------------------------------------------------
// Closes the windows wave out device
//-----------------------------------------------------------------------------
void CAudioDeviceSDLAudio::CloseWaveOut( void ) 
{ 
	// none of these SDL_* functions are available to call if this is false.
	if (m_devId)
	{
		SDL_CloseAudioDevice(m_devId);
		m_devId = 0;
	}
	SDL_QuitSubSystem(SDL_INIT_AUDIO);
	FreeOutputBuffers();
}

//-----------------------------------------------------------------------------
// Allocate output buffers
//-----------------------------------------------------------------------------
void CAudioDeviceSDLAudio::AllocateOutputBuffers()
{
	// Allocate and lock memory for the waveform data.  
	const int nBufferSize = WAV_BUFFER_SIZE * WAV_BUFFERS;
	m_pBuffer = new uint8_t[nBufferSize];
	memset(m_pBuffer, '\0', nBufferSize);
	m_readPos = 0;
	m_partialWrite = 0;
	m_deviceSampleCount = nBufferSize / DeviceSampleBytes();
}


//-----------------------------------------------------------------------------
// Free output buffers
//-----------------------------------------------------------------------------
void CAudioDeviceSDLAudio::FreeOutputBuffers()
{
	delete[] m_pBuffer;
	m_pBuffer = NULL;
}


//-----------------------------------------------------------------------------
// Mixing setup
//-----------------------------------------------------------------------------
int CAudioDeviceSDLAudio::PaintBegin( float mixAheadTime, int soundtime, int paintedtime )
{
	//  soundtime - total samples that have been played out to hardware at dmaspeed
	//  paintedtime - total samples that have been mixed at speed
	//  endtime - target for samples in mixahead buffer at speed
	unsigned int endtime = soundtime + mixAheadTime * DeviceDmaSpeed();
	
	int samps = DeviceSampleCount() >> (DeviceChannels()-1);

	if ((int)(endtime - soundtime) > samps)
		endtime = soundtime + samps;

	if ((endtime - paintedtime) & 0x3)
	{
		// The difference between endtime and painted time should align on 
		// boundaries of 4 samples.  This is important when upsampling from 11khz -> 44khz.
		endtime -= (endtime - paintedtime) & 0x3;
	}

	return endtime;
}

void CAudioDeviceSDLAudio::AudioCallbackEntry(void *userdata, Uint8 *stream, int len)
{
	((CAudioDeviceSDLAudio *) userdata)->AudioCallback(stream, len);
}

void CAudioDeviceSDLAudio::AudioCallback(Uint8 *stream, int len)
{
	if (!m_devId)
	{
		debugsdl("SDLAUDIO: uhoh, no audio device!\n");
		return;  // can this even happen?
	}

	const int totalWriteable = len;
#if defined( BINK_VIDEO ) && defined( LINUX )
	Uint8 *stream_orig = stream;
#endif
	debugsdl("SDLAUDIO: writable size is %d.\n", totalWriteable);

	Assert(len <= (WAV_BUFFERS * WAV_BUFFER_SIZE));

	while (len > 0)
	{
		// spaceAvailable == bytes before we overrun the end of the ring buffer.
		const int spaceAvailable = ((WAV_BUFFERS * WAV_BUFFER_SIZE) - m_readPos);
		const int writeLen = (len < spaceAvailable) ? len : spaceAvailable;

		if (writeLen > 0)
		{
			const uint8_t *buf = m_pBuffer + m_readPos;
			debugsdl("SDLAUDIO: Writing %d bytes...\n", writeLen);

			#if 0
			static FILE *io = NULL;
			if (io == NULL) io = fopen("dumpplayback.raw", "wb");
			if (io != NULL) { fwrite(buf, writeLen, 1, io); fflush(io); }
			#endif

			memcpy(stream, buf, writeLen);
			stream += writeLen;
			len -= writeLen;
			Assert(len >= 0);
		}

		m_readPos = len ? 0 : (m_readPos + writeLen);  // if still bytes to write to stream, we're rolling around the ring buffer.
	}

#if defined( BINK_VIDEO ) && defined( LINUX )
	// Mix in Bink movie audio if that stuff is playing.
	if ( g_pVideo != NULL) 
	{
		g_pVideo->SoundDeviceCommand( VideoSoundDeviceOperation::SDLMIXER_CALLBACK, (void *)stream_orig, (void *)&totalWriteable );
	}
#endif

	// Translate between bytes written and buffers written.
	m_partialWrite += totalWriteable;
	m_buffersSent += m_partialWrite / WAV_BUFFER_SIZE;
	m_partialWrite %= WAV_BUFFER_SIZE;
}


//-----------------------------------------------------------------------------
// Actually performs the mixing
//-----------------------------------------------------------------------------
void CAudioDeviceSDLAudio::PaintEnd( void )
{
	debugsdl("SDLAUDIO: PaintEnd...\n");

#if 0  // !!! FIXME: this is the 1.3 headers, but not implemented yet in SDL.
	if (SDL_AudioDeviceConnected(m_devId) != 1)
	{
		debugsdl("SDLAUDIO: Audio device was disconnected!\n");
		Shutdown();
	}
#endif
}

int CAudioDeviceSDLAudio::GetOutputPosition( void )
{
	return (m_readPos >> SAMPLE_16BIT_SHIFT)/DeviceChannels();
}


//-----------------------------------------------------------------------------
// Pausing
//-----------------------------------------------------------------------------
void CAudioDeviceSDLAudio::Pause( void )
{
	m_pauseCount++;
	if (m_pauseCount == 1)
	{
		debugsdl("SDLAUDIO: PAUSE\n");
		SDL_PauseAudioDevice(m_devId, 1);
	}
}


void CAudioDeviceSDLAudio::UnPause( void )
{
	if ( m_pauseCount > 0 )
	{
		m_pauseCount--;
		if (m_pauseCount == 0)
		{
			debugsdl("SDLAUDIO: UNPAUSE\n");
			SDL_PauseAudioDevice(m_devId, 0);
		}
	}
}

bool CAudioDeviceSDLAudio::IsActive( void )
{
	return ( m_pauseCount == 0 );
}

float CAudioDeviceSDLAudio::MixDryVolume( void )
{
	return 0;
}


bool CAudioDeviceSDLAudio::Should3DMix( void )
{
	return false;
}


void CAudioDeviceSDLAudio::ClearBuffer( void )
{
	int		clear;

	if ( !m_pBuffer )
		return;

	clear = 0;

	Q_memset(m_pBuffer, clear, DeviceSampleCount() * DeviceSampleBytes() );
}


void CAudioDeviceSDLAudio::MixBegin( int sampleCount )
{
	MIX_ClearAllPaintBuffers( sampleCount, false );
}


void CAudioDeviceSDLAudio::MixUpsample( int sampleCount, int filtertype )
{
	paintbuffer_t *ppaint = MIX_GetCurrentPaintbufferPtr();
	int ifilter = ppaint->ifilter;
	
	Assert (ifilter < CPAINTFILTERS);

	S_MixBufferUpsample2x( sampleCount, ppaint->pbuf, &(ppaint->fltmem[ifilter][0]), CPAINTFILTERMEM, filtertype );

	ppaint->ifilter++;
}

void CAudioDeviceSDLAudio::Mix8Mono( channel_t *pChannel, char *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress )
{
	int volume[CCHANVOLUMES];
	paintbuffer_t *ppaint = MIX_GetCurrentPaintbufferPtr();

	if (!MIX_ScaleChannelVolume( ppaint, pChannel, volume, 1))
		return;

	Mix8MonoWavtype( pChannel, ppaint->pbuf + outputOffset, volume, (byte *)pData, inputOffset, rateScaleFix, outCount );
}


void CAudioDeviceSDLAudio::Mix8Stereo( channel_t *pChannel, char *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress )
{
	int volume[CCHANVOLUMES];
	paintbuffer_t *ppaint = MIX_GetCurrentPaintbufferPtr();

	if (!MIX_ScaleChannelVolume( ppaint, pChannel, volume, 2 ))
		return;

	Mix8StereoWavtype( pChannel, ppaint->pbuf + outputOffset, volume, (byte *)pData, inputOffset, rateScaleFix, outCount );
}


void CAudioDeviceSDLAudio::Mix16Mono( channel_t *pChannel, short *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress )
{
	int volume[CCHANVOLUMES];
	paintbuffer_t *ppaint = MIX_GetCurrentPaintbufferPtr();

	if (!MIX_ScaleChannelVolume( ppaint, pChannel, volume, 1 ))
		return;

	Mix16MonoWavtype( pChannel, ppaint->pbuf + outputOffset, volume, pData, inputOffset, rateScaleFix, outCount );
}


void CAudioDeviceSDLAudio::Mix16Stereo( channel_t *pChannel, short *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress )
{
	int volume[CCHANVOLUMES];
	paintbuffer_t *ppaint = MIX_GetCurrentPaintbufferPtr();

	if (!MIX_ScaleChannelVolume( ppaint, pChannel, volume, 2 ))
		return;

	Mix16StereoWavtype( pChannel, ppaint->pbuf + outputOffset, volume, pData, inputOffset, rateScaleFix, outCount );
}


void CAudioDeviceSDLAudio::ChannelReset( int entnum, int channelIndex, float distanceMod )
{
}


void CAudioDeviceSDLAudio::TransferSamples( int end )
{
	int		lpaintedtime = g_paintedtime;
	int		endtime = end;
	
	// resumes playback...

	if ( m_pBuffer )
	{
		S_TransferStereo16( m_pBuffer, PAINTBUFFER, lpaintedtime, endtime );
	}
}

void CAudioDeviceSDLAudio::SpatializeChannel( int nSlot, int volume[CCHANVOLUMES/2], int master_vol, const Vector& sourceDir, float gain, float mono )
{
	VPROF("CAudioDeviceSDLAudio::SpatializeChannel");
	S_SpatializeChannel( /*nSlot,*/ volume, master_vol, &sourceDir, gain, mono );
}

void CAudioDeviceSDLAudio::StopAllSounds( void )
{
}


void CAudioDeviceSDLAudio::ApplyDSPEffects( int idsp, portable_samplepair_t *pbuffront, portable_samplepair_t *pbufrear, portable_samplepair_t *pbufcenter, int samplecount )
{
	//SX_RoomFX( endtime, filter, timefx );
	DSP_Process( idsp, pbuffront, pbufrear, pbufcenter, samplecount );
}

#endif // !DEDICATED