/*
**	Command & Conquer Renegade(tm)
**	Copyright 2025 Electronic Arts Inc.
**
**	This program 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.
**
**	This program is distributed in the hope that it will be useful,
**	but WITHOUT ANY WARRANTY; without even the implied warranty of
**	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
**	GNU General Public License for more details.
**
**	You should have received a copy of the GNU General Public License
**	along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

/***********************************************************************************************
 ***              C O N F I D E N T I A L  ---  W E S T W O O D  S T U D I O S               ***
 ***********************************************************************************************
 *                                                                                             *
 *                 Project Name : WWAudio                                                      *
 *                                                                                             *
 *                     $Archive:: /Commando/Code/WWAudio/Sound3D.cpp                          $*
 *                                                                                             *
 *                       Author:: Patrick Smith                                                *
 *                                                                                             *
 *                     $Modtime:: 12/10/01 12:40p                                             $*
 *                                                                                             *
 *                    $Revision:: 20                                                          $*
 *                                                                                             *
 *---------------------------------------------------------------------------------------------*
 * Functions:                                                                                  *
 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */


#include "sound3d.h"
#include "soundbuffer.h"
#include "wwaudio.h"
#include "soundscene.h"
#include "utils.h"
#include "soundchunkids.h"
#include "persistfactory.h"
#include "chunkio.h"
#include "sound3dhandle.h"
#include "systimer.h"


//////////////////////////////////////////////////////////////////////////////////
//
//	Static factories
//
//////////////////////////////////////////////////////////////////////////////////
SimplePersistFactoryClass<Sound3DClass, CHUNKID_SOUND3D> _Sound3DPersistFactory;


enum
{
	CHUNKID_VARIABLES			= 0x11090955,
	CHUNKID_BASE_CLASS
};

enum
{
	VARID_AUTO_CALC_VEL		= 0x01,
	VARID_CURR_VEL,
	VARID_XXX1,
	VARID_XXX2,
	VARID_MAX_VOL_RADIUS,
	VARID_IS_STATIC,
};



////////////////////////////////////////////////////////////////////////////////////////////////
//
//	Sound3DClass
//
////////////////////////////////////////////////////////////////////////////////////////////////
Sound3DClass::Sound3DClass (void)
	: m_bAutoCalcVel (true),
	  m_CurrentVelocity (0, 0, 0),
	  m_MaxVolRadius (0),
	  m_LastUpdate (0),
	  m_IsStatic (false),
	  m_IsTransformInitted (false)
{
	return ;
}


////////////////////////////////////////////////////////////////////////////////////////////////
//
//	Sound3DClass
//
////////////////////////////////////////////////////////////////////////////////////////////////
Sound3DClass::Sound3DClass (const Sound3DClass &src)
	: m_bAutoCalcVel (true),
	  m_CurrentVelocity (0, 0, 0),
	  m_MaxVolRadius (0),
	  m_LastUpdate (0),
	  m_IsStatic (false),
	  m_IsTransformInitted (false),
	  AudibleSoundClass (src)
{
	(*this) = src;
	return ;
}


////////////////////////////////////////////////////////////////////////////////////////////////
//
//	~Sound3DClass
//
////////////////////////////////////////////////////////////////////////////////////////////////
Sound3DClass::~Sound3DClass (void)
{
 	Free_Miles_Handle ();
	return ;
}


////////////////////////////////////////////////////////////////////////////////////////////////
//
//	operator=
//
////////////////////////////////////////////////////////////////////////////////////////////////
const Sound3DClass &
Sound3DClass::operator= (const Sound3DClass &src)
{
	m_bAutoCalcVel			= src.m_bAutoCalcVel;
	m_CurrentVelocity		= src.m_CurrentVelocity;
	m_MaxVolRadius			= src.m_MaxVolRadius;
	m_IsStatic				= src.m_IsStatic;
	m_LastUpdate			= src.m_LastUpdate;

	// Call the base class
	AudibleSoundClass::operator= (src);
	return (*this);
}


////////////////////////////////////////////////////////////////////////////////////////////////
//
//	Play
//
////////////////////////////////////////////////////////////////////////////////////////////////
bool
Sound3DClass::Play (bool alloc_handle)
{
	// Record our first 'tick' if we just started playing
	if (m_State != STATE_PLAYING) {
		m_LastUpdate = TIMEGETTIME ();
	}

	// Allow the base class to process this call
	return AudibleSoundClass::Play (m_IsCulled == false);
}


////////////////////////////////////////////////////////////////////////////////////////////////
//
//	On_Frame_Update
//
////////////////////////////////////////////////////////////////////////////////////////////////
bool
Sound3DClass::On_Frame_Update (unsigned int milliseconds)
{
	Matrix3D prev_tm = m_PrevTransform;

	if (m_bDirty && (m_PhysWrapper != NULL)) {
		m_Scene->Update_Sound (m_PhysWrapper);
		m_bDirty = false;
	}

	//
	// Update the sound's position if its linked to a render object
	//
	Apply_Auto_Position ();

	//
	//	Make sure the transform is initialized
	//
	if (m_IsTransformInitted == false) {
		prev_tm = m_Transform;
	}

// Disabling auto-velocity because hardware doppler is poor...
#if 0

	//
	// Update the current velocity if we are 'auto-calcing'.
	//
	if (m_bAutoCalcVel && Get_Class_ID () != CLASSID_LISTENER) {
		Vector3 last_pos = prev_tm.Get_Translation ();
		Vector3 curr_pos = m_Transform.Get_Translation ();

		//
		//	Don't update the velocity if we haven't moved (optimization -- Miles calls
		// can be really slow)
		//
		if (last_pos != curr_pos) {
			Vector3 curr_vel;

			//
			//	Extrapolate our current velocity given the last time slice and the distance
			// we moved.
			//
			float secs_since_last_update = (TIMEGETTIME () - m_LastUpdate);
			if (secs_since_last_update > 0) {
				curr_vel = ((curr_pos - last_pos) / secs_since_last_update);
			} else {
				curr_vel.Set (0, 0, 0);
			}

			Set_Velocity (curr_vel);
		}
	}

#endif // doppler disable

	//
	// Remember when the last time we updated our 'auto-calc'
	// variables.
	//
	m_LastUpdate = TIMEGETTIME ();
	m_PrevTransform = m_Transform;

	//
	// If necessary, update the volume based on the distance
	// from the edge of the dropoff radius.
	//
	if (m_SoundHandle != NULL) {
		Update_Edge_Volume ();
	}

	return AudibleSoundClass::On_Frame_Update ();
}


////////////////////////////////////////////////////////////////////////////////////////////////
//
//	Update_Edge_Volume
//
////////////////////////////////////////////////////////////////////////////////////////////////
void
Sound3DClass::Update_Edge_Volume (void)
{
	if (m_DropOffRadius == 0.0F || As_SoundPseudo3DClass () != NULL) {
		return ;
	}

	//
	// Find the difference in the sound position and its listener's position
	//
	Vector3 sound_pos	= m_ListenerTransform.Get_Translation () - m_Transform.Get_Translation ();
	float distance		= sound_pos.Quick_Length ();

	const float FALLOFF_RANGE = 0.85F;

	//
	//	Are we close to the edge of the dropoff radius?
	//
	float falloff_start = (m_DropOffRadius * FALLOFF_RANGE);
	if (distance >= falloff_start && distance <= m_DropOffRadius) {

		//
		//	Normalize our distance from the edge
		//
		float percent	= (distance - falloff_start) / (m_DropOffRadius - falloff_start);
		percent			= WWMath::Clamp (1.0F - percent, 0.0F, 1.0F);

		//
		//	Update the sound volume
		//
		Internal_Set_Volume (m_RealVolume * percent);
	}

	return ;
}


////////////////////////////////////////////////////////////////////////////////////////////////
//
//	Set_Transform
//
////////////////////////////////////////////////////////////////////////////////////////////////
void
Sound3DClass::Set_Transform (const Matrix3D &transform)
{
	if (transform == m_Transform) {
		return ;
	}


	// Update our internal transform
	m_PrevTransform	= m_Transform;
	m_Transform			= transform;
	Set_Dirty ();

	if (m_IsTransformInitted == false) {
		m_PrevTransform = transform;
		m_IsTransformInitted = true;
	}

	Update_Miles_Transform ();
	return ;
}


////////////////////////////////////////////////////////////////////////////////////////////////
//
//	Set_Listener_Transform
//
////////////////////////////////////////////////////////////////////////////////////////////////
void
Sound3DClass::Set_Listener_Transform (const Matrix3D &tm)
{
	//
	//	If the transform has changed, then cache the new transform
	// and update the sound's position in the "Mile's" world
	//
	if (m_ListenerTransform != tm) {
		m_ListenerTransform = tm;

		Update_Miles_Transform ();
	}

	return ;
}


////////////////////////////////////////////////////////////////////////////////////////////////
//
//	Update_Miles_Transform
//
////////////////////////////////////////////////////////////////////////////////////////////////
void
Sound3DClass::Update_Miles_Transform (void)
{
	//
	// Do we have a valid miles handle?
	//
	if (m_SoundHandle != NULL) {

		//
		//	Build a matrix to transform coordinates from world-space to listener-space
		//
		Matrix3D world_to_listener_tm;
		m_ListenerTransform.Get_Orthogonal_Inverse (world_to_listener_tm);

		//
		//	Transform the object's TM into "listener-space"
		//
		Matrix3D listener_space_tm = world_to_listener_tm * m_Transform;

		//
		// Pass the sound's position onto miles
		//
		Vector3 position = listener_space_tm.Get_Translation ();
		::AIL_set_3D_position (m_SoundHandle->Get_H3DSAMPLE (), -position.Y, position.Z, position.X);

		//
		// Pass the sound's orientation (facing) onto miles
		//
		Vector3 facing	= listener_space_tm.Get_X_Vector ();
		Vector3 up		= listener_space_tm.Get_Z_Vector ();

		::AIL_set_3D_orientation (m_SoundHandle->Get_H3DSAMPLE (),
										  -facing.Y,
										  facing.Z,
										  facing.X,
										  -up.Y,
										  up.Z,
										  up.X);
	}

	return ;
}


////////////////////////////////////////////////////////////////////////////////////////////////
//
//	Set_Position
//
////////////////////////////////////////////////////////////////////////////////////////////////
void
Sound3DClass::Set_Position (const Vector3 &position)
{
	//
	// Pass the sound's position onto miles
	//
	if (m_Transform.Get_Translation () != position)  {
		// Update our internal transform
		//
		// SKB: 4/13/01 - Confirmed to be OK by Pat Smith.
		//  Took Set_Transform() outside of condition because even if sound is
		//  not playing I need to be able to change it's position.
		//  I had a problem that sounds would never be added to the scene because
		//  their positions stayed at 0,0,0 even after this Set_Postion() call.
		m_PrevTransform = m_Transform;
		m_Transform.Set_Translation (position);
		Set_Dirty ();

		if (m_IsTransformInitted == false) {
			m_PrevTransform = m_Transform;
			m_IsTransformInitted = true;
		}

		if (m_SoundHandle != NULL) {

			//
			//	Transform the sound's position into 'listener-space'
			//
			Vector3 sound_pos	= position;
			Vector3 listener_space_pos;
			Matrix3D::Inverse_Transform_Vector (m_ListenerTransform, sound_pos, &listener_space_pos);

			//
			//	Update the object's position inside of Miles
			//
			::AIL_set_3D_position (m_SoundHandle->Get_H3DSAMPLE (), -listener_space_pos.Y,
					listener_space_pos.Z, listener_space_pos.X);
		}
	}

	return ;
}


////////////////////////////////////////////////////////////////////////////////////////////////
//
//	Set_Velocity
//
////////////////////////////////////////////////////////////////////////////////////////////////
void
Sound3DClass::Set_Velocity (const Vector3 &velocity)
{
	MMSLockClass lock;

	m_CurrentVelocity = velocity;
	Set_Dirty ();

	//
	// Pass the sound's velocity onto miles
	//
	if (m_SoundHandle != NULL) {

		//WWDEBUG_SAY (("Current Velocity: %.2f %.2f %.2f\n", m_CurrentVelocity.X, m_CurrentVelocity.Y, m_CurrentVelocity.Z));
		::AIL_set_3D_velocity_vector (m_SoundHandle->Get_H3DSAMPLE (),
												-m_CurrentVelocity.Y,
												m_CurrentVelocity.Z,
												m_CurrentVelocity.X);
	}

	return ;
}


////////////////////////////////////////////////////////////////////////////////////////////////
//
//	Set_DropOff_Radius
//
////////////////////////////////////////////////////////////////////////////////////////////////
void
Sound3DClass::Set_DropOff_Radius (float radius)
{
	//MMSLockClass lock;

	m_DropOffRadius = radius;
	Set_Dirty ();

	// Pass attenuation settings onto miles
	if (m_SoundHandle != NULL) {
		::AIL_set_3D_sample_distances (	m_SoundHandle->Get_H3DSAMPLE (),
													m_DropOffRadius,
													(m_MaxVolRadius > 1.0F) ? m_MaxVolRadius : 1.0F);
	}

	return ;
}


////////////////////////////////////////////////////////////////////////////////////////////////
//
//	Set_Max_Vol_Radius
//
////////////////////////////////////////////////////////////////////////////////////////////////
void
Sound3DClass::Set_Max_Vol_Radius (float radius)
{
	m_MaxVolRadius = radius;
	Set_Dirty ();

	// Pass attenuation settings onto miles
	if (m_SoundHandle != NULL) {
		::AIL_set_3D_sample_distances (	m_SoundHandle->Get_H3DSAMPLE (),
													m_DropOffRadius,
													(m_MaxVolRadius > 1.0F) ? m_MaxVolRadius : 1.0F);
	}

	return ;
}


////////////////////////////////////////////////////////////////////////////////////////////////
//
//	Initialize_Miles_Handle
//
////////////////////////////////////////////////////////////////////////////////////////////////
void
Sound3DClass::Initialize_Miles_Handle (void)
{
	MMSLockClass lock;

	// If this sound is already playing, then update its
	// playing position to make sure we really should
	// be playing it... (it will free the miles handle if not)
	if (m_State == STATE_PLAYING) {
		Update_Play_Position ();
	}

	// Do we have a valid sample handle from miles?
	if (m_SoundHandle != NULL) {

		//
		// Pass the actual sound data onto the sample
		//
		m_SoundHandle->Initialize (m_Buffer);

		//
		// Record the total length of the sample in milliseconds...
		//
		m_SoundHandle->Get_Sample_MS_Position ((S32 *)&m_Length, NULL);

		//
		// Pass our cached settings onto miles
		//
		float real_volume = Determine_Real_Volume ();
		m_SoundHandle->Set_Sample_Volume (int(real_volume * 127.0F));
		m_SoundHandle->Set_Sample_Pan (int(m_Pan * 127.0F));
		m_SoundHandle->Set_Sample_Loop_Count (m_LoopCount);

		//
		// Pass attenuation settings onto miles
		//
		::AIL_set_3D_sample_distances (	m_SoundHandle->Get_H3DSAMPLE (),
													m_DropOffRadius,
													(m_MaxVolRadius > 1.0F) ? m_MaxVolRadius : 1.0F);


		//
		//	Assign the 3D effects level accordingly (for reverb, etc)
		//
		::AIL_set_3D_sample_effects_level (m_SoundHandle->Get_H3DSAMPLE (),
				WWAudioClass::Get_Instance ()->Get_Effects_Level ());

		//
		//	Pass the sound's position and orientation onto Miles
		//
		Update_Miles_Transform ();

		//
		//	Apply the pitch factor (if necessary)
		//
		if (m_PitchFactor != 1.0F) {
			Set_Pitch_Factor (m_PitchFactor);
		}

		// If this sound is already playing (and just now got a handle)
		// then make sure we start it.
		if (m_State == STATE_PLAYING) {
			m_SoundHandle->Start_Sample ();

			// Update the loop count based on the number of loops left
			m_SoundHandle->Set_Sample_Loop_Count (m_LoopsLeft);
		}

		// Seek to the position of the sound where we last left off.
		// For example, this sound could have gotten bumped due to a low priority,
		// but is now back and ready to resume at the position it would have been
		// at if it was never bumped.
		Seek (m_CurrentPosition);

		// Associate this object instance with the handle
		m_SoundHandle->Set_Sample_User_Data (INFO_OBJECT_PTR, (S32)this);
	}

	return ;
}


////////////////////////////////////////////////////////////////////////////////////////////////
//
//	Allocate_Miles_Handle
//
////////////////////////////////////////////////////////////////////////////////////////////////
void
Sound3DClass::Allocate_Miles_Handle (void)
{
	//MMSLockClass lock;

	//
	// If we need to, get a play-handle from the audio system
	//
	if (m_SoundHandle == NULL) {
		Set_Miles_Handle ((MILES_HANDLE)WWAudioClass::Get_Instance ()->Get_3D_Sample (*this));
	}

	return ;
}


////////////////////////////////////////////////////////////////////////////////////////////////
//
//	Add_To_Scene
//
////////////////////////////////////////////////////////////////////////////////////////////////
void
Sound3DClass::Add_To_Scene (bool start_playing)
{
	SoundSceneClass *scene = WWAudioClass::Get_Instance ()->Get_Sound_Scene ();
	if ((scene != NULL) && (m_Scene == NULL)) {

		// Determine what culling system this sound belongs to
		if (m_IsStatic) {
			scene->Add_Static_Sound (this, start_playing);
		} else {
			scene->Add_Sound (this, start_playing);
		}
		m_Scene = scene;
	}

	return ;
}


////////////////////////////////////////////////////////////////////////////////////////////////
//
//	Remove_From_Scene
//
////////////////////////////////////////////////////////////////////////////////////////////////
void
Sound3DClass::Remove_From_Scene (void)
{
	if (m_Scene != NULL) {

		// Determine what culling system this sound belongs to
		if (m_IsStatic) {
			m_Scene->Remove_Static_Sound (this);
		} else {
			m_Scene->Remove_Sound (this);
		}

		m_Scene = NULL;
		m_PhysWrapper = NULL;
	}

	return ;
}


////////////////////////////////////////////////////////////////////////////////////////////////
//
//	On_Loop_End
//
////////////////////////////////////////////////////////////////////////////////////////////////
void
Sound3DClass::On_Loop_End (void)
{
	// Allow the base class to process this message
	AudibleSoundClass::On_Loop_End ();
	return ;
}

/////////////////////////////////////////////////////////////////////////////////
//
//	Get_Factory
//
/////////////////////////////////////////////////////////////////////////////////
const PersistFactoryClass &
Sound3DClass::Get_Factory (void) const
{
	return _Sound3DPersistFactory;
}


//////////////////////////////////////////////////////////////////////////////////
//
//	Save
//
//////////////////////////////////////////////////////////////////////////////////
bool
Sound3DClass::Save (ChunkSaveClass &csave)
{
	csave.Begin_Chunk (CHUNKID_BASE_CLASS);
		AudibleSoundClass::Save (csave);
	csave.End_Chunk ();

	csave.Begin_Chunk (CHUNKID_VARIABLES);

		WRITE_MICRO_CHUNK (csave, VARID_AUTO_CALC_VEL, m_bAutoCalcVel);
		WRITE_MICRO_CHUNK (csave, VARID_CURR_VEL, m_CurrentVelocity);

		WRITE_MICRO_CHUNK (csave, VARID_MAX_VOL_RADIUS, m_MaxVolRadius);
		WRITE_MICRO_CHUNK (csave, VARID_IS_STATIC, m_IsStatic);

	csave.End_Chunk ();

	return true;
}


//////////////////////////////////////////////////////////////////////////////////
//
//	Load
//
//////////////////////////////////////////////////////////////////////////////////
bool
Sound3DClass::Load (ChunkLoadClass &cload)
{
	while (cload.Open_Chunk ()) {
		switch (cload.Cur_Chunk_ID ()) {

			case CHUNKID_BASE_CLASS:
				AudibleSoundClass::Load (cload);
				break;

			case CHUNKID_VARIABLES:
			{
				//
				//	Read all the variables from their micro-chunks
				//
				while (cload.Open_Micro_Chunk ()) {
					switch (cload.Cur_Micro_Chunk_ID ()) {

						READ_MICRO_CHUNK (cload, VARID_AUTO_CALC_VEL, m_bAutoCalcVel);
						READ_MICRO_CHUNK (cload, VARID_CURR_VEL, m_CurrentVelocity);
						READ_MICRO_CHUNK (cload, VARID_MAX_VOL_RADIUS, m_MaxVolRadius);
						READ_MICRO_CHUNK (cload, VARID_IS_STATIC, m_IsStatic);
					}

					cload.Close_Micro_Chunk ();
				}
			}
			break;
		}

		cload.Close_Chunk ();
	}

	return true;
}


////////////////////////////////////////////////////////////////////////////////////////////////
//
//	Set_Miles_Handle
//
////////////////////////////////////////////////////////////////////////////////////////////////
void
Sound3DClass::Set_Miles_Handle (MILES_HANDLE handle)
{
	//
	// Start fresh
	//
	Free_Miles_Handle ();

	//
	//	Is our data valid?
	//
	if (handle != INVALID_MILES_HANDLE && m_Buffer != NULL) {

		//
		//	Configure the sound handle
		//
		m_SoundHandle = new Sound3DHandleClass;
		m_SoundHandle->Set_Miles_Handle (handle);

		//
		//	Use this new handle
		//
		Initialize_Miles_Handle ();
	}

	return ;
}