//-----------------------------------------------------------------------------
// Copyright (c) 2012 GarageGames, LLC
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
//-----------------------------------------------------------------------------

#include "platform/platform.h"
#include "platform/threads/mutex.h"
#include "sfx/fmod/sfxFMODDevice.h"
#include "sfx/fmod/sfxFMODBuffer.h"
#include "sfx/sfxSystem.h"
#include "platform/async/asyncUpdate.h"
#include "console/consoleTypes.h"
#include "core/volume.h"


bool                 SFXFMODDevice::smPrefDisableSoftware = false;
bool                 SFXFMODDevice::smPrefUseSoftwareOcclusion = true;
bool                 SFXFMODDevice::smPrefUseSoftwareHRTF = true;
bool                 SFXFMODDevice::smPrefEnableProfile = false;
bool                 SFXFMODDevice::smPrefGeometryUseClosest = false;
const char*          SFXFMODDevice::smPrefDSoundHRTF = "full";
const char*          SFXFMODDevice::smPrefPluginPath = "";
U32                  SFXFMODDevice::smStatMemUsageCore;
U32                  SFXFMODDevice::smStatMemUsageEvents;
U32                  SFXFMODDevice::smStatNumEventSources;
SFXFMODDevice*       SFXFMODDevice::smInstance;
FMOD_SYSTEM*         SFXFMODDevice::smSystem;
FMOD_EVENTSYSTEM*    SFXFMODDevice::smEventSystem;
FModFNTable*         SFXFMODDevice::smFunc;
Mutex*               FModFNTable::mutex;


//-----------------------------------------------------------------------------

String FMODResultToString( FMOD_RESULT result )
{
   switch( result )
   {
      #define FMOD_ERROR( n ) case n: return #n;
      #include "fmodErrors.h"
      #undef FMOD_ERROR
      
      default:
         break;
   }
   
   return String();
}

//------------------------------------------------------------------------------
// FMOD filesystem wrappers.
//FIXME: these are not thread-safe and cannot be used as such

FMOD_RESULT F_CALLBACK fmodFileOpenCallback( const char* name, int unicode, unsigned int* filesize, void** handle, void** userdata )
{
   String fileName;
   if( unicode )
      fileName = String( ( UTF16* ) name );
   else
      fileName = String( name );
      
   Torque::FS::FileRef file = Torque::FS::OpenFile( fileName, Torque::FS::File::Read );
   if( !file )
      return FMOD_ERR_FILE_NOTFOUND;
   else if( file->getStatus() != Torque::FS::File::Open )
      return FMOD_ERR_FILE_BAD;
      
   // Add a reference so we can pass it into FMOD.
   file->incRefCount();
      
   *filesize = U32( file->getSize() );
   *handle = file.getPointer();

   return FMOD_OK;
}

FMOD_RESULT F_CALLBACK fmodFileCloseCallback( void* handle, void* userdata )
{
   Torque::FS::File* file = reinterpret_cast< Torque::FS::File* >( handle );
   file->decRefCount();
   return FMOD_OK;
}

FMOD_RESULT F_CALLBACK fmodFileReadCallback( void* handle, void* buffer, unsigned int sizebytes, unsigned int* bytesread, void* userdata )
{
   Torque::FS::File* file = reinterpret_cast< Torque::FS::File* >( handle );

   U32 numRead = file->read( buffer, sizebytes );
   *bytesread = numRead;
   
   if( file->getStatus() == Torque::FS::File::EndOfFile )
      return FMOD_ERR_FILE_EOF;
   else if( file->getStatus() != Torque::FS::File::Open )
      return FMOD_ERR_FILE_BAD;
      
   return FMOD_OK;
}

FMOD_RESULT F_CALLBACK fmodFileSeekCallback( void* handle, unsigned int pos, void* userdata )
{
   Torque::FS::File* file = reinterpret_cast< Torque::FS::File* >( handle );
   
   if( file->setPosition( pos, Torque::FS::File::Begin ) != pos )
      return FMOD_ERR_FILE_COULDNOTSEEK;
      
   return FMOD_OK;
}

//-----------------------------------------------------------------------------

SFXFMODDevice::SFXFMODDevice( SFXProvider* provider, 
                              FModFNTable *fmodFnTbl, 
                              int deviceIdx, 
                              String name )
   :  SFXDevice( name, provider, false, 32 ),
      m3drolloffmode( FMOD_3D_INVERSEROLLOFF ),
      mDeviceIndex( deviceIdx )
{
	// Store off the function pointers for later use.
	smFunc = fmodFnTbl;

   smStatMemUsageCore = 0;
   smStatMemUsageEvents = 0;
   smStatNumEventSources = 0;
      
   // Register our SFXSystem plugin.
   
   SFX->addPlugin( &mPlugin );
   
   smInstance = this;
}

//-----------------------------------------------------------------------------

SFXFMODDevice::~SFXFMODDevice()
{
   _releaseAllResources();
   
   SFX->removePlugin( &mPlugin );
   
   if( smEventSystem )
   {
      smFunc->FMOD_EventSystem_Release( smEventSystem );
      smEventSystem = NULL;
      smSystem = NULL;
   }
   else
      smFunc->FMOD_System_Close( smSystem );
      
   smInstance = NULL;
}

//-----------------------------------------------------------------------------

bool SFXFMODDevice::_init()
{
   #define FMOD_CHECK( message )                               \
      if( result != FMOD_OK )                                  \
      {                                                        \
         Con::errorf( "SFXFMODDevice::_init() - %s (%s)",      \
            message,                                           \
            FMOD_ErrorString( result ) );                      \
         return false;                                         \
      }

	AssertISV(smSystem, 
      "SFXFMODDevice::_init() - can't init w/o an existing FMOD system handle!");

   FMOD_RESULT result;
   
   // Get some prefs.
   
   if( smPrefPluginPath && smPrefPluginPath[ 0 ] )
   {
      char fullPath[ 4096 ];
      Platform::makeFullPathName( smPrefPluginPath, fullPath, sizeof( fullPath ) );
      
      smFunc->FMOD_System_SetPluginPath( smSystem, fullPath );
   }
   else
   {
      smFunc->FMOD_System_SetPluginPath( smSystem, Platform::getExecutablePath() );
   }
   
	// Initialize everything from fmod.
	FMOD_SPEAKERMODE speakermode;
	FMOD_CAPS        caps;
	result = smFunc->FMOD_System_GetDriverCaps(smSystem, 0, &caps, ( int* ) 0, &speakermode);
   FMOD_CHECK( "SFXFMODDevice::init - Failed to get driver caps" );

	result = smFunc->FMOD_System_SetDriver(smSystem, mDeviceIndex);
   FMOD_CHECK( "SFXFMODDevice::init - Failed to set driver" );

	result = smFunc->FMOD_System_SetSpeakerMode(smSystem, speakermode);
   FMOD_CHECK( "SFXFMODDevice::init - Failed to set the user selected speaker mode" );

	if (caps & FMOD_CAPS_HARDWARE_EMULATED)             /* The user has the 'Acceleration' slider set to off!  This is really bad for latency!. */
	{                                                   /* You might want to warn the user about this. */
		result = smFunc->FMOD_System_SetDSPBufferSize(smSystem, 1024, 10);
      FMOD_CHECK( "SFXFMODDevice::init - Failed to set DSP buffer size" );
	}
   
   Con::printf( "\nFMOD Device caps:" );
   #define PRINT_CAP( name )              \
      if( caps & FMOD_CAPS_ ## name )     \
         Con::printf( #name );
         
   PRINT_CAP( HARDWARE );
   PRINT_CAP( HARDWARE_EMULATED );
   PRINT_CAP( OUTPUT_MULTICHANNEL );
   PRINT_CAP( OUTPUT_FORMAT_PCM8 );
   PRINT_CAP( OUTPUT_FORMAT_PCM16 );
   PRINT_CAP( OUTPUT_FORMAT_PCM24 );
   PRINT_CAP( OUTPUT_FORMAT_PCM32 );
   PRINT_CAP( OUTPUT_FORMAT_PCMFLOAT );
   PRINT_CAP( REVERB_LIMITED );
   
   Con::printf( "" );
   
   bool tryAgain;
   do
   {
      tryAgain = false;
      
      FMOD_INITFLAGS flags = FMOD_INIT_NORMAL | FMOD_INIT_VOL0_BECOMES_VIRTUAL;
      
      if( smPrefDisableSoftware )
         flags |= FMOD_INIT_SOFTWARE_DISABLE;
      if( smPrefUseSoftwareOcclusion )
         flags |= FMOD_INIT_OCCLUSION_LOWPASS;
      if( smPrefUseSoftwareHRTF )
         flags |= FMOD_INIT_HRTF_LOWPASS;
      if( smPrefEnableProfile )
         flags |= FMOD_INIT_ENABLE_PROFILE;
      if( smPrefGeometryUseClosest )
         flags |= FMOD_INIT_GEOMETRY_USECLOSEST;
      
      if( smEventSystem )
         result = smFunc->FMOD_EventSystem_Init( smEventSystem, 100, flags, ( void* ) 0, FMOD_EVENT_INIT_NORMAL );
      else
         result = smFunc->FMOD_System_Init( smSystem, 100, flags, ( void* ) 0 );
         
      if( result == FMOD_ERR_OUTPUT_CREATEBUFFER )         /* Ok, the speaker mode selected isn't supported by this soundcard.  Switch it back to stereo... */
      {
         result = smFunc->FMOD_System_SetSpeakerMode( smSystem, FMOD_SPEAKERMODE_STEREO );
         FMOD_CHECK( "SFXFMODDevice::init - failed on fallback speaker mode setup" );
         tryAgain = true;
      }
   } while( tryAgain );
   FMOD_CHECK( "SFXFMODDevice::init - failed to init system" );
   
   // Print hardware channel info.

   if( caps & FMOD_CAPS_HARDWARE )
   {
      int num3D, num2D, numTotal;
      
      if( smFunc->FMOD_System_GetHardwareChannels( smSystem, &num2D, &num3D, &numTotal ) == FMOD_OK )
         Con::printf( "FMOD Hardware channels: 2d=%i, 3d=%i, total=%i", num2D, num3D, numTotal );
   }

   // Set up filesystem.
   
   //FIXME: Don't do this for now.  Crashes on Windows.
   #if 0
   smFunc->FMOD_System_SetFileSystem( smSystem, fmodFileOpenCallback, fmodFileCloseCallback, fmodFileReadCallback, fmodFileSeekCallback, -1 );
   #endif
      
   // Set capabilities.
   
   mCaps = CAPS_Reverb | CAPS_VoiceManagement;
   if( smEventSystem )
      mCaps |= CAPS_FMODDesigner;

   // Start the update thread.
   
   #ifndef TORQUE_DEDICATED // Avoid dependency on platform/async for Linx dedicated.
   
   if( !Con::getBoolVariable( "$_forceAllMainThread" ) )
   {
      SFXInternal::gUpdateThread = new AsyncPeriodicUpdateThread
         ( "FMOD Update Thread", SFXInternal::gBufferUpdateList,
           Con::getIntVariable( "$pref::SFX::updateInterval", SFXInternal::DEFAULT_UPDATE_INTERVAL ) );
      SFXInternal::gUpdateThread->start();
   }
   
   #endif
   
   return true;
}

//-----------------------------------------------------------------------------

SFXBuffer* SFXFMODDevice::createBuffer( const ThreadSafeRef< SFXStream >& stream, SFXDescription* description )
{
   AssertFatal( stream, "SFXFMODDevice::createBuffer() - Got a null stream!" );
   AssertFatal( description, "SFXFMODDevice::createBuffer() - Got null description!" );

   SFXFMODBuffer *buffer = SFXFMODBuffer::create( stream, description );
   if ( buffer )
      _addBuffer( buffer );

   return buffer;
}

//-----------------------------------------------------------------------------

SFXBuffer* SFXFMODDevice::createBuffer( const String& filename, SFXDescription* description )
{
   AssertFatal( filename.isNotEmpty(), "SFXFMODDevice::createBuffer() - Got an empty filename!" );
   AssertFatal( description, "SFXFMODDevice::createBuffer() - Got null description!" );

   SFXFMODBuffer* buffer = SFXFMODBuffer::create( filename, description );
   if( buffer )
      _addBuffer( buffer );
      
   return buffer;
}

//-----------------------------------------------------------------------------

SFXVoice* SFXFMODDevice::createVoice( bool is3D, SFXBuffer* buffer )
{
   AssertFatal( buffer, "SFXFMODDevice::createVoice() - Got null buffer!" );

   SFXFMODBuffer* fmodBuffer = dynamic_cast<SFXFMODBuffer*>( buffer );
   AssertFatal( fmodBuffer, "SFXFMODDevice::createVoice() - Got bad buffer!" );

   SFXFMODVoice* voice = SFXFMODVoice::create( this, fmodBuffer );
   if ( !voice )
      return NULL;

   _addVoice( voice );
	return voice;
}

//-----------------------------------------------------------------------------

void SFXFMODDevice::update()
{
   Parent::update();

   if( smEventSystem )
   {
      FModAssert( smFunc->FMOD_EventSystem_Update( smEventSystem ), "Failed to update event system!" );
   }
   else
   {
      FModAssert(smFunc->FMOD_System_Update(smSystem), "Failed to update system!");
   }
}

//-----------------------------------------------------------------------------

void SFXFMODDevice::setNumListeners( U32 num )
{
   smFunc->FMOD_System_Set3DNumListeners( smSystem, num );
}

//-----------------------------------------------------------------------------

void SFXFMODDevice::setListener( U32 index, const SFXListenerProperties& listener )
{
   FMOD_VECTOR position, forward, up, velocity;
   
   TorqueTransformToFMODVectors( listener.getTransform(), position, forward, up );
   TorqueVectorToFMODVector( listener.getVelocity(), velocity );

	// Do the listener state update, then update!
	smFunc->FMOD_System_Set3DListenerAttributes( smSystem, index, &position, &velocity, &forward, &up );
}

//-----------------------------------------------------------------------------

void SFXFMODDevice::setDistanceModel( SFXDistanceModel model )
{
   switch( model )
   {
      case SFXDistanceModelLinear:
         m3drolloffmode = FMOD_3D_LINEARROLLOFF;
         break;
         
      case SFXDistanceModelLogarithmic:
         m3drolloffmode = FMOD_3D_INVERSEROLLOFF;
         break;
         
      default:
         AssertWarn( false, "SFXFMODDevice::setDistanceModel - model not implemented" );
   }
}

//-----------------------------------------------------------------------------

void SFXFMODDevice::setDopplerFactor( F32 factor )
{
   F32 dopplerFactor;
   F32 distanceFactor;
   F32 rolloffFactor;
   
   smFunc->FMOD_System_Get3DSettings( smSystem, &dopplerFactor, &distanceFactor, &rolloffFactor );
   dopplerFactor = factor;
   smFunc->FMOD_System_Set3DSettings( smSystem, dopplerFactor, distanceFactor, rolloffFactor );
}

//-----------------------------------------------------------------------------

void SFXFMODDevice::setRolloffFactor( F32 factor )
{
   F32 dopplerFactor;
   F32 distanceFactor;
   F32 rolloffFactor;
   
   smFunc->FMOD_System_Get3DSettings( smSystem, &dopplerFactor, &distanceFactor, &rolloffFactor );
   rolloffFactor = factor;
   smFunc->FMOD_System_Set3DSettings( smSystem, dopplerFactor, distanceFactor, rolloffFactor );
}

//-----------------------------------------------------------------------------

void SFXFMODDevice::setReverb( const SFXReverbProperties& reverb )
{
   FMOD_REVERB_PROPERTIES prop = FMOD_PRESET_GENERIC;

   prop.Environment           = 0;
   prop.EnvDiffusion          = reverb.mEnvDiffusion;
   prop.Room                  = reverb.mRoom;
   prop.RoomHF                = reverb.mRoomHF;
   prop.RoomLF                = reverb.mRoomLF;
   prop.DecayTime             = reverb.mDecayTime;
   prop.DecayLFRatio          = reverb.mDecayLFRatio;
   prop.DecayHFRatio          = reverb.mDecayHFRatio;
   prop.Reflections           = reverb.mReflections;
   prop.ReflectionsDelay      = reverb.mReflectionsDelay;
   prop.Reverb                = reverb.mReverb;
   prop.ReverbDelay           = reverb.mReverbDelay;
   prop.ModulationTime        = reverb.mModulationTime;
   prop.ModulationDepth       = reverb.mModulationDepth;
   prop.HFReference           = reverb.mHFReference;
   prop.LFReference           = reverb.mLFReference;
   prop.Diffusion             = reverb.mDiffusion;
   prop.Density               = reverb.mDensity;
   prop.Flags                 = reverb.mFlags;
   
   // Here we only want to affect 3D sounds.  While not quite obvious from the docs,
   // SetReverbProperties sets the global reverb environment for 2D sounds whereas
   // SetAmbientReverbProperties sets the global reverb environment for 3D sounds.
   
   FMOD_RESULT result = smFunc->FMOD_System_SetReverbAmbientProperties( smSystem, &prop );
   if( result != FMOD_OK )
      Con::errorf( "SFXFMODDevice::setReverb - Failed to set reverb (%s)", FMODResultToString( result ).c_str() );
}

//-----------------------------------------------------------------------------

void SFXFMODDevice::resetReverb()
{
   FMOD_REVERB_PROPERTIES prop = FMOD_PRESET_OFF;
   smFunc->FMOD_System_SetReverbProperties( smSystem, &prop );
}

//-----------------------------------------------------------------------------

void SFXFMODDevice::updateMemUsageStats()
{
   smFunc->FMOD_System_GetMemoryInfo( smSystem, ( unsigned int ) FMOD_MEMBITS_ALL,
      ( unsigned int ) 0, ( unsigned int* ) &smStatMemUsageCore, ( unsigned int* ) 0 );
      
   if( smEventSystem )
      smFunc->FMOD_EventSystem_GetMemoryInfo( smEventSystem, ( unsigned int ) 0,
         ( unsigned int ) FMOD_EVENT_MEMBITS_ALL, ( unsigned int* ) &smStatMemUsageEvents, ( unsigned int* ) 0 );
}

//=============================================================================
//    Console Functions.
//=============================================================================
// MARK: ---- Console Functions ----

//------------------------------------------------------------------------------

ConsoleFunction( fmodDumpDSPInfo, void, 1, 1, "()"
				"@brief Dump information about the standard DSP effects.\n\n"
				"@ingroup SFXFMOD")
{
   if( !SFXFMODDevice::smFunc )
      return;
      
   const U32 firstDSPType = FMOD_DSP_TYPE_MIXER;
   const U32 lastDSPType = FMOD_DSP_TYPE_TREMOLO;
   
   for( U32 i = firstDSPType; i <= lastDSPType; ++ i )
   {
      FMOD_DSP* dsp;
      if( SFXFMODDevice::smFunc->FMOD_System_CreateDSPByType( SFXFMODDevice::smSystem, ( FMOD_DSP_TYPE ) i, &dsp ) == FMOD_OK )
      {
         // Print general info.
         
         char name[ 33 ];
         unsigned int version;
         int channels;
         int numParameters;
         
         dMemset( name, 0, sizeof( name ) );
         SFXFMODDevice::smFunc->FMOD_DSP_GetInfo( dsp, name, &version, &channels, ( int* ) NULL, ( int* ) NULL );
         SFXFMODDevice::smFunc->FMOD_DSP_GetNumParameters( dsp, &numParameters );
         
         Con::printf( "----------------------------------------------------------------" );
         Con::printf( "DSP: %s", name );
         Con::printf( "Version: %i.%i", ( version & 0xffff0000 ) >> 16, version & 0xffff );
         Con::printf( "Channels: %i", channels );
         Con::printf( "Parameters: %i", numParameters );
         Con::printf( "" );
         
         // Print parameter info.
         
         for( U32 n = 0; n < numParameters; ++ n )
         {
            char name[ 17 ];
            char label[ 17 ];
            char description[ 1024 ];
            float minValue, maxValue;
            float value;
            char valueString[ 256 ];
            
            dMemset( name, 0, sizeof( name ) );
            dMemset( label, 0, sizeof( label ) );
            dMemset( description, 0, sizeof( description ) );
            dMemset( valueString, 0, sizeof( valueString ) );
            
            SFXFMODDevice::smFunc->FMOD_DSP_GetParameterInfo( dsp, n, name, label, description, sizeof( description ) - 1, &minValue, &maxValue );
            SFXFMODDevice::smFunc->FMOD_DSP_GetParameter( dsp, n, &value, valueString, sizeof( valueString ) - 1 );
            
            Con::printf( "* Parameter %i", n );
            Con::printf( "Name: %s", name );
            Con::printf( "Label: %s", label );
            Con::printf( "Description: %s", description );
            Con::printf( "Min: %f", minValue );
            Con::printf( "Max: %f", maxValue );
            Con::printf( "Value: %f (%s)", value, valueString );
            Con::printf( "" );
         }
         
         // Release the DSP.
         
         SFXFMODDevice::smFunc->FMOD_DSP_Release( dsp );
      }
   }
}

//-----------------------------------------------------------------------------

ConsoleFunction( fmodDumpMemoryStats, void, 1, 1, "()"
				"@return Prints the current memory consumption of the FMOD module\n\n"
				"@ingroup SFXFMOD")
{
   int current = 0;
   int max = 0;

   if (SFXFMODDevice::smFunc && SFXFMODDevice::smFunc->FMOD_Memory_GetStats.fn)
         SFXFMODDevice::smFunc->FMOD_Memory_GetStats(&current, &max);
   Con::printf("Fmod current: %d, max: %d", current, max);
}
