/*****************************************************************************
                    The Dark Mod GPL Source Code
 
 This file is part of the The Dark Mod Source Code, originally based 
 on the Doom 3 GPL Source Code as published in 2011.
 
 The Dark Mod Source Code is free software: you can redistribute it 
 and/or modify it under the terms of the GNU General Public License as 
 published by the Free Software Foundation, either version 3 of the License, 
 or (at your option) any later version. For details, see LICENSE.TXT.
 
 Project: The Dark Mod (http://www.thedarkmod.com/)
 
 $Revision$ (Revision of last commit) 
 $Date$ (Date of last commit)
 $Author$ (Author of last commit)
 
******************************************************************************/
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <malloc.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
// OSS sound interface
// http://www.opensound.com/
#include <sys/soundcard.h>

#include "../../idlib/precompiled.h"
#include "../../sound/snd_local.h"
#include "../posix/posix_public.h"
#include "sound.h"

const char	*s_driverArgs[]	= { "best", "oss", "alsa", NULL };

#ifndef NO_ALSA
static idCVar s_driver( "s_driver", s_driverArgs[0], CVAR_SYSTEM | CVAR_ARCHIVE, "sound driver. 'best' will attempt to use alsa and fallback to OSS if not available", s_driverArgs, idCmdSystem::ArgCompletion_String<s_driverArgs> );
#else
static idCVar s_driver( "s_driver", "oss", CVAR_SYSTEM | CVAR_ARCHIVE | CVAR_ROM, "sound driver. only OSS is supported in this build" );
#endif

idAudioHardware *idAudioHardware::Alloc() {
#ifndef NO_ALSA
	if ( !strcmp( s_driver.GetString(), "best" ) ) {
		idAudioHardwareALSA *test = new idAudioHardwareALSA;
		if ( test->DLOpen() ) {
			common->Printf( "Alsa is available\n" );
			return test;
		}
		common->Printf( "Alsa is not available\n" );
		delete test;
		return new idAudioHardwareOSS;
	}
	if ( !strcmp( s_driver.GetString(), "alsa" ) ) {
		return new idAudioHardwareALSA;
	}
#endif
	return new idAudioHardwareOSS;
}

// OSS sound ----------------------------------------------------

/*
===============
idAudioHardware::~idAudioHardware
===============
*/
idAudioHardware::~idAudioHardware() { }
	
/*
=================
idAudioHardwareOSS::~idAudioHardwareOSS
=================	
*/	
idAudioHardwareOSS::~idAudioHardwareOSS() {
	Release();
}

/*
=================
idAudioHardwareOSS::Release
=================	
*/	
void idAudioHardwareOSS::Release( bool bSilent ) {
	if (m_audio_fd) {
		if (!bSilent) {
			common->Printf("------ OSS Sound Shutdown ------\n");
		}
		if (m_buffer) {
			free( m_buffer );
			m_buffer = NULL;
			m_buffer_size = 0;
		}
		common->Printf("close sound device\n");	
		if (close(m_audio_fd) == -1) {
			common->Warning( "failed to close sound device: %s", strerror(errno) );
		}
		m_audio_fd = 0;
		if (!bSilent) {
			common->Printf("--------------------------------\n");
		}
	}
}	

/*
=================
idAudioHardwareOSS::InitFailed
=================	
*/	
void idAudioHardwareOSS::InitFailed() {
	Release( true );
	cvarSystem->SetCVarBool( "s_noSound", true );
	common->Warning( "sound subsystem disabled" );
	common->Printf( "--------------------------------------\n" );
}

/*
=================
idAudioHardwareOSS::ExtractOSSVersion
=================	
*/	
void idAudioHardwareOSS::ExtractOSSVersion( int version, idStr &str ) const {
	sprintf( str, "%d.%d.%d", ( version & 0xFF0000 ) >> 16, ( version & 0xFF00 ) >> 8, version & 0xFF );
}

/*
=================
idAudioHardwareOSS::Initialize

http://www.4front-tech.com/pguide/index.html
though OSS API docs (1.1) advertise AFMT_S32_LE, AFMT_S16_LE is the only output format I've found in kernel emu10k1 headers

BSD NOTE: With the GNU library, you can use free to free the blocks that memalign, posix_memalign, and valloc return.
That does not work in BSD, however--BSD does not provide any way to free such blocks.
=================	
*/
idCVar s_device( "s_dsp", "/dev/dsp", CVAR_SYSTEM | CVAR_ARCHIVE, "" );

bool idAudioHardwareOSS::Initialize( ) {
	common->Printf("------ OSS Sound Initialization ------\n");

	int requested_sample_format, caps, oss_version;
	idStr s_compiled_oss_version, s_oss_version;
	struct audio_buf_info info;

	memset( &info, 0, sizeof( info ) );
	
	if (m_audio_fd) {
		Release();
	}
	
	// open device ------------------------------------------------
	if ((m_audio_fd = open( s_device.GetString(), O_WRONLY | O_NONBLOCK, 0)) == -1) {
		m_audio_fd = 0;
		common->Warning( "failed to open sound device '%s': %s", s_device.GetString(), strerror(errno) );
		InitFailed();
		return false;
	}
	// make it blocking - so write overruns don't fail with 'Resource temporarily unavailable'
	int flags;
	if ( ( flags = fcntl( m_audio_fd, F_GETFL ) ) == -1 ) {
		common->Warning( "failed fcntl F_GETFL on sound device '%s': %s", s_device.GetString(), strerror( errno ) );
		InitFailed();
		return false;
	}
	flags &= ~O_NONBLOCK;
	if ( fcntl( m_audio_fd, F_SETFL, flags ) == -1 ) {
		common->Warning( "failed to clear O_NONBLOCK on sound device '%s': %s", s_device.GetString(), strerror( errno ) );
		InitFailed();
		return false;
	}
	
	common->Printf("opened sound device '%s'\n", s_device.GetString());
	
	// verify capabilities -----------------------------------------

	// may only be available starting with OSS API v4.0
	// http://www.fi.opensound.com/developer/SNDCTL_SYSINFO.html
	// NOTE: at OSS API 4.0 headers, replace OSS_SYSINFO with SNDCTL_SYSINFO
	oss_sysinfo si;
	if ( ioctl( m_audio_fd, OSS_SYSINFO, &si ) == -1 ) {
		common->Printf( "ioctl SNDCTL_SYSINFO failed: %s\nthis ioctl is only available in OSS/Linux implementation. If you run OSS/Free, don't bother.", strerror( errno ) );
	} else {
		common->Printf( "%s: %s %s\n", s_device.GetString(), si.product, si.version );
	}

	if ( ioctl( m_audio_fd, SNDCTL_DSP_GETCAPS, &caps ) == -1 ) {
		common->Warning( "ioctl SNDCTL_DSP_GETCAPS failed - driver too old?" );
		InitFailed();
		return false;
	}
	common->DPrintf("driver rev %d - capabilities %d\n", caps & DSP_CAP_REVISION, caps);
	if (ioctl( m_audio_fd, OSS_GETVERSION, &oss_version ) == -1) {
		common->Warning( "ioctl OSS_GETVERSION failed" );
		InitFailed();
		return false;
	}
	ExtractOSSVersion( oss_version, s_oss_version );
	ExtractOSSVersion( SOUND_VERSION, s_compiled_oss_version );
	common->DPrintf( "OSS interface version %s - compile time %s\n", s_oss_version.c_str(), s_compiled_oss_version.c_str() );
	if (!(caps & DSP_CAP_MMAP)) {
		common->Warning( "driver doesn't have DSP_CAP_MMAP capability" );
		InitFailed();
		return false;
	}
	if (!(caps & DSP_CAP_TRIGGER)) {
		common->Warning( "driver doesn't have DSP_CAP_TRIGGER capability" );
		InitFailed();
		return false;
	}
	
	// sample format -----------------------------------------------
	requested_sample_format = AFMT_S16_LE;
	m_sample_format = requested_sample_format;
	if (ioctl(m_audio_fd, SNDCTL_DSP_SETFMT, &m_sample_format) == -1) {
		common->Warning( "ioctl SNDCTL_DSP_SETFMT %d failed: %s", requested_sample_format, strerror(errno) );
		InitFailed();
		return false;
	}
	if ( m_sample_format != requested_sample_format ) {
		common->Warning( "ioctl SNDCTL_DSP_SETFMT failed to get the requested sample format %d, got %d", requested_sample_format, m_sample_format );
		InitFailed();
		return false;
	}
	
	// channels ----------------------------------------------------

	// sanity over number of speakers
	m_channels = idSoundSystemLocal::s_numberOfSpeakers.GetInteger();
	if ( m_channels != 6 && m_channels != 2 && m_channels != 8) {
		common->Warning( "invalid value for s_numberOfSpeakers. Use either 2, 6 or 8" );
		idSoundSystemLocal::s_numberOfSpeakers.SetInteger( 2 );
		m_channels = 2;
	}

	if ( ioctl( m_audio_fd, SNDCTL_DSP_CHANNELS, &m_channels ) == -1 ) {
		common->Warning( "ioctl SNDCTL_DSP_CHANNELS %d failed: %s", idSoundSystemLocal::s_numberOfSpeakers.GetInteger(), strerror(errno) );
		InitFailed();
		return false;
	}
	if ( m_channels != (unsigned int)idSoundSystemLocal::s_numberOfSpeakers.GetInteger() ) {
		common->Warning( "ioctl SNDCTL_DSP_CHANNELS failed to get the %d requested channels, got %d", idSoundSystemLocal::s_numberOfSpeakers.GetInteger(), m_channels );
		if ( m_channels != 2 && idSoundSystemLocal::s_numberOfSpeakers.GetInteger() != 2 ) {
			// we didn't request 2 channels, some drivers reply 1 channel on error but may still let us still get 2 if properly asked
			m_channels = 2;
			if ( ioctl( m_audio_fd, SNDCTL_DSP_CHANNELS, &m_channels ) == -1 ) {
				common->Warning( "ioctl SNDCTL_DSP_CHANNELS fallback to 2 failed: %s", strerror(errno) );
				InitFailed();
				return false;
			}
		}
		if ( m_channels == 2 ) {
			// tell the system to mix 2 channels
			common->Warning( "falling back to stereo" );
			idSoundSystemLocal::s_numberOfSpeakers.SetInteger( 2 );
		} else {
			// disable sound
			InitFailed();
			return false;
		}
	}
	assert( (int)m_channels == idSoundSystemLocal::s_numberOfSpeakers.GetInteger() );
	
	// sampling rate ------------------------------------------------
	m_speed = PRIMARYFREQ;
	if ( ioctl( m_audio_fd, SNDCTL_DSP_SPEED, &m_speed ) == -1 ) {
		common->Warning( "ioctl SNDCTL_DSP_SPEED %d failed: %s", PRIMARYFREQ, strerror(errno) );
		InitFailed();
		return false;
	}
	// instead of an exact match, do a very close to
	// there is some horrible Ensonic ES1371 which replies 44101 for a 44100 request
	if ( abs( m_speed - PRIMARYFREQ ) > 5 ) {
		common->Warning( "ioctl SNDCTL_DSP_SPEED failed to get the requested frequency %d, got %d", PRIMARYFREQ, m_speed );
		InitFailed();
		return false;
	}
	common->Printf("%s - bit rate: %d, channels: %d, frequency: %d\n", s_device.GetString(), m_sample_format, m_channels, m_speed);
	
	// output buffer ------------------------------------------------
	// allocate a final buffer target, the sound engine locks, writes, and we write back to the device
	// we want m_buffer_size ( will have to rename those )
	// ROOM_SLICES_IN_BUFFER is fixed ( system default, 10 )
	// MIXBUFFER_SAMPLES is the number of samples found in a slice
	// each sample is m_channels * sizeof( float ) bytes
	// in AsyncUpdate we only write one block at a time, so we'd only need to have a final mix buffer sized of a single block
	m_buffer_size = MIXBUFFER_SAMPLES * m_channels * 2;
	m_buffer = malloc( m_buffer_size );
	common->Printf( "allocated a mix buffer of %d bytes\n", m_buffer_size );

	// toggle sound -------------------------------------------------
	
	// toggle off before toggling on. that's what OSS source code samples recommends
	int flag = 0;
	if (ioctl(m_audio_fd, SNDCTL_DSP_SETTRIGGER, &flag) == -1) {
		common->Warning( "ioctl SNDCTL_DSP_SETTRIGGER 0 failed: %s", strerror(errno) );
	}
	flag = PCM_ENABLE_OUTPUT;
	if (ioctl(m_audio_fd, SNDCTL_DSP_SETTRIGGER, &flag) == -1) {
		common->Warning( "ioctl SNDCTL_DSP_SETTRIGGER PCM_ENABLE_OUTPUT failed: %s", strerror(errno) );
	}

	common->Printf("--------------------------------------\n");
	return true;
}

/*
===============
idAudioHardwareOSS::Flush
===============
*/
bool idAudioHardwareOSS::Flush( void ) {
	audio_buf_info ospace;
	if ( ioctl( m_audio_fd, SNDCTL_DSP_GETOSPACE, &ospace ) == -1 ) {
		Sys_Printf( "ioctl SNDCTL_DSP_GETOSPACE failed: %s\n", strerror( errno ) );
		return false;
	}
	// how many chunks can we write to the audio device right now
	m_freeWriteChunks = ( ospace.bytes * MIXBUFFER_CHUNKS ) / ( MIXBUFFER_SAMPLES * m_channels * 2 );
	if ( m_writeChunks ) {
		// flush out any remaining chunks we could now
		Write( true );
	}
	return ( m_freeWriteChunks > 0 );
}

/*
=================
idAudioHardwareOSS::GetMixBufferSize
=================
*/	
int idAudioHardwareOSS::GetMixBufferSize() {
	//	return MIXBUFFER_SAMPLES * 2 * m_channels;
	return m_buffer_size;
}

/*
=================
idAudioHardwareOSS::GetMixBuffer
=================
*/	
short* idAudioHardwareOSS::GetMixBuffer() {
	return (short *)m_buffer;
}

/*
===============
idAudioHardwareOSS::Write
rely on m_freeWriteChunks which has been set in Flush() before engine did the mixing for this MIXBUFFER_SAMPLE
===============
*/
void idAudioHardwareOSS::Write( bool flushing ) {
	assert( m_audio_fd );
	int ret;
	if ( !flushing && m_writeChunks ) {
		// if we write after a new mixing loop, we should have m_writeChunk == 0
		// otherwise that last remaining chunk that was never flushed out to the audio device has just been overwritten
		Sys_Printf( "idAudioHardwareOSS::Write: %d samples were overflowed and dropped\n", m_writeChunks * MIXBUFFER_SAMPLES / MIXBUFFER_CHUNKS );
	}
	if ( !flushing ) {
		// if running after the mix loop, then we have a full buffer to write out
		m_writeChunks = MIXBUFFER_CHUNKS;
	}
	if ( m_freeWriteChunks == 0 ) {
		return;
	}
	// what to write and how much
	int pos = (int)m_buffer + ( MIXBUFFER_CHUNKS - m_writeChunks ) * m_channels * 2 * MIXBUFFER_SAMPLES / MIXBUFFER_CHUNKS;
	int len = Min( m_writeChunks, m_freeWriteChunks ) * m_channels * 2 * MIXBUFFER_SAMPLES / MIXBUFFER_CHUNKS;
	assert( len > 0 );
	if ( ( ret = write( m_audio_fd, (void*)pos, len ) ) == -1 ) {
		Sys_Printf( "write to audio fd failed: %s\n", strerror( errno ) );
		return;
	}
	if ( len != ret ) {
		Sys_Printf( "short write to audio fd: wrote %d out of %d\n", ret, m_buffer_size );
		return;
	}
	m_writeChunks -= Min( m_writeChunks, m_freeWriteChunks );
}

/*
 ===============
 Sys_LoadOpenAL
 -===============
 */
bool Sys_LoadOpenAL( void ) {
	return false;
}

