//=============================================================================
// FILE: spatialaudio_common.cpp
//
//                  Copyright (c) 2017 QUALCOMM Technologies Inc.
//                              All Rights Reserved.
//
//=============================================================================
#include "spatialaudio_common.h"

#include "glm/gtx/transform.hpp"
#include "spatialaudio_utility.h"
#include "svrConfig.h"
#include "svrGeometry.h"
#include "svrKtxLoader.h"
#include "svrProfile.h"
#include "svrUtil.h"

using namespace Svr;
using namespace glm;

#define SPATIAL_AUDIO_GAZE_BOX_DEFAULT_COLOR (vec3(.5f, 1.f, 1.f))

VAR(bool, gLogFramesPerSecond, false, kVariableNonpersistent);
VAR(int, gRenderFieldOfView, 90, kVariableNonpersistent);
VAR(bool, gLooping, true, kVariableNonpersistent);

// MSAA samples: 1 = Off; 2/4 = Number of MSAA samples
VAR(int, gMsaaSamples, 4, kVariableNonpersistent);
VAR(bool, gForceOrientationTexture, false, kVariableNonpersistent);
VAR(int, gForcedVSyncTime, 0, kVariableNonpersistent);
VAR(bool, gLogSensorData, false, kVariableNonpersistent);


VAR(int, gReverb_Setting, 0, kVariableNonpersistent);

VAR(bool, gReverbA_Enabled, true, kVariableNonpersistent);
VAR(float, gReverbA_RoomX, 10.f, kVariableNonpersistent);
VAR(float, gReverbA_RoomY, 25.f, kVariableNonpersistent);
VAR(float, gReverbA_RoomZ, 15.f, kVariableNonpersistent);
VAR(float, gReverbA_Gain, 1.f, kVariableNonpersistent);
VAR(float, gReverbA_TimeAdjust, 1.f, kVariableNonpersistent);
VAR(float, gReverbA_BrightnessAdjust, 1.f, kVariableNonpersistent);
VAR(int, gReverbA_RoomWallMaterial, 0, kVariableNonpersistent);
VAR(int, gReverbA_RoomCeilingMaterial, 0, kVariableNonpersistent);
VAR(int, gReverbA_RoomFloorMaterial, 0, kVariableNonpersistent);

VAR(bool, gReverbB_Enabled, false, kVariableNonpersistent);
VAR(float, gReverbB_RoomX, 10.f, kVariableNonpersistent);
VAR(float, gReverbB_RoomY, 25.f, kVariableNonpersistent);
VAR(float, gReverbB_RoomZ, 15.f, kVariableNonpersistent);
VAR(float, gReverbB_Gain, 1.f, kVariableNonpersistent);
VAR(float, gReverbB_TimeAdjust, 1.f, kVariableNonpersistent);
VAR(float, gReverbB_BrightnessAdjust, 1.f, kVariableNonpersistent);
VAR(int, gReverbB_RoomWallMaterial, 0, kVariableNonpersistent);
VAR(int, gReverbB_RoomCeilingMaterial, 0, kVariableNonpersistent);
VAR(int, gReverbB_RoomFloorMaterial, 0, kVariableNonpersistent);

VAR(bool, gReverbC_Enabled, false, kVariableNonpersistent);
VAR(float, gReverbC_RoomX, 10.f, kVariableNonpersistent);
VAR(float, gReverbC_RoomY, 25.f, kVariableNonpersistent);
VAR(float, gReverbC_RoomZ, 15.f, kVariableNonpersistent);
VAR(float, gReverbC_Gain, 1.f, kVariableNonpersistent);
VAR(float, gReverbC_TimeAdjust, 1.f, kVariableNonpersistent);
VAR(float, gReverbC_BrightnessAdjust, 1.f, kVariableNonpersistent);
VAR(int, gReverbC_RoomWallMaterial, 0, kVariableNonpersistent);
VAR(int, gReverbC_RoomCeilingMaterial, 0, kVariableNonpersistent);
VAR(int, gReverbC_RoomFloorMaterial, 0, kVariableNonpersistent);


VAR(int, gRolloff_Setting, 0, kVariableNonpersistent);

VAR(int, gRolloffA_Type, 0, kVariableNonpersistent);
VAR(float, gRolloffA_MinDistance, 1.f, kVariableNonpersistent);
VAR(float, gRolloffA_MaxDistance, 8.f, kVariableNonpersistent);

VAR(int, gRolloffB_Type, 0, kVariableNonpersistent);
VAR(float, gRolloffB_MinDistance, 1.f, kVariableNonpersistent);
VAR(float, gRolloffB_MaxDistance, 8.f, kVariableNonpersistent);

VAR(int, gRolloffC_Type, 0, kVariableNonpersistent);
VAR(float, gRolloffC_MinDistance, 1.f, kVariableNonpersistent);
VAR(float, gRolloffC_MaxDistance, 8.f, kVariableNonpersistent);


VAR(bool, gLogAudioUpdate, false, kVariableNonpersistent);
VAR(bool, gLogHeadPoseUpdate, false, kVariableNonpersistent);
VAR(bool, gLogSoundVolume, false, kVariableNonpersistent);
VAR(bool, gLogObjectTransform, false, kVariableNonpersistent);
VAR(bool, gLogSoundfieldTransform, false, kVariableNonpersistent);
VAR(bool, gLogIsSoundPlaying, false, kVariableNonpersistent);

VAR(bool, gRolloffBugWorkaround, true, kVariableNonpersistent);


// Sometimes we need to test application running slowly
// To force a desired FPS, set gForcedVSyncTime to desired frame time (milliseconds)
unsigned int gLastRenderTime = 0;

static void L_CreateLayout(float centerX, float centerY, float radiusX, float radiusY, svrLayoutCoords *pLayout)
{
	// This is always in screen space so we want Z = 0 and W = 1
	float lowerLeftPos[4] = { centerX - radiusX, centerY - radiusY, 0.0f, 1.0f };
	float lowerRightPos[4] = { centerX + radiusX, centerY - radiusY, 0.0f, 1.0f };
	float upperLeftPos[4] = { centerX - radiusX, centerY + radiusY, 0.0f, 1.0f };
	float upperRightPos[4] = { centerX + radiusX, centerY + radiusY, 0.0f, 1.0f };

	float lowerUVs[4] = { 0.0f, 0.0f, 1.0f, 0.0f };
	float upperUVs[4] = { 0.0f, 1.0f, 1.0f, 1.0f };

	memcpy(pLayout->LowerLeftPos, lowerLeftPos, sizeof(lowerLeftPos));
	memcpy(pLayout->LowerRightPos, lowerRightPos, sizeof(lowerRightPos));
	memcpy(pLayout->UpperLeftPos, upperLeftPos, sizeof(upperLeftPos));
	memcpy(pLayout->UpperRightPos, upperRightPos, sizeof(upperRightPos));

	memcpy(pLayout->LowerUVs, lowerUVs, sizeof(lowerUVs));
	memcpy(pLayout->UpperUVs, upperUVs, sizeof(upperUVs));
}
static void LayersMaskAndLayout(svrRenderLayer*const renderLayers)
{
	assert(renderLayers);

	L_CreateLayout(0.0f, 0.0f, 1.0f, 1.0f, &renderLayers[0].imageCoords);
	renderLayers[0].eyeMask = kEyeMaskLeft;

	L_CreateLayout(0.0f, 0.0f, 1.0f, 1.0f, &renderLayers[1].imageCoords);
	renderLayers[1].eyeMask = kEyeMaskRight;
}
static bool ResolveMsaaTarget(Svr::SvrRenderTarget &sourceTarget, Svr::SvrRenderTarget &destTarget)
{
	GL(glBindFramebuffer(GL_READ_FRAMEBUFFER, sourceTarget.GetFrameBufferId()));
	GL(glReadBuffer(GL_COLOR_ATTACHMENT0));

	GL(glBindFramebuffer(GL_DRAW_FRAMEBUFFER, destTarget.GetFrameBufferId()));
	GLenum nDrawBuffer[] = { GL_COLOR_ATTACHMENT0 };
	GL(glDrawBuffers(1, nDrawBuffer));

	int srcWidth = sourceTarget.GetWidth();
	int srcHeight = sourceTarget.GetHeight();
	int destWidth = destTarget.GetWidth();
	int destHeight = destTarget.GetHeight();

	// LOGI("Resolving render target %d (%d sample[s]) to %d (%d sample[s])", sourceTarget.GetFrameBufferId(), sourceTarget.GetSamples(), destTarget.GetFrameBufferId(), destTarget.GetSamples());

	// This can be GL_LINEAR if the sizes are not the same.  Probably a performance hit
	GL(glBlitFramebuffer(0, 0, srcWidth, srcHeight, 0, 0, destWidth, destHeight, GL_COLOR_BUFFER_BIT, GL_NEAREST));

	return true;
}
static void RenderLayersSetStereo(svrRenderLayer*const renderLayers, Svr::SvrRenderTarget*const renderTarget)
{
	assert(renderLayers && renderTarget);
	renderLayers[0].imageHandle = renderTarget[Svr::kLeft].GetColorAttachment();
	renderLayers[1].imageHandle = renderTarget[Svr::kRight].GetColorAttachment();
}
static void ResolveMsaaTargetStereo(Svr::SvrRenderTarget*const multiSampledSrc, Svr::SvrRenderTarget*const singleSampledDst)
{
	assert(multiSampledSrc && singleSampledDst);
	ResolveMsaaTarget(multiSampledSrc[Svr::kLeft], singleSampledDst[Svr::kLeft]);
	ResolveMsaaTarget(multiSampledSrc[Svr::kRight], singleSampledDst[Svr::kRight]);
}
static void PrepareEyeBuffer(const int width, const int height)
{
	GL(glViewport(0, 0, width, height));
	GL(glScissor(0, 0, width, height));
}
static void SvrRenderTargetStereoInitialize(SvrRenderTarget*const renderTarget, const int width, const int height, const int msaaSamples)
{
	assert(renderTarget);
	renderTarget[kLeft].Initialize(width, height, msaaSamples, GL_RGBA8, true);
	renderTarget[kRight].Initialize(width, height, msaaSamples, GL_RGBA8, true);
}
static bool InRange(const float v, const float lowInclusive, const float highExclusive)
{
	assert(lowInclusive <= highExclusive);
	assert(lowInclusive >= 0);
	assert(highExclusive >= 0);
	return v >= lowInclusive && v < highExclusive;
}
static bool InNormalizedRange(const float v)
{
	return InRange(v, 0.f, 1.f + .001f);
}

static bool RaySphereIntersect(
	float *tVal,
	const vec3& rayOrigin,
	const vec3& rayDir,
	const vec3& sphereCenter,
	const float sphereRadius)
{
	assert(tVal);
	const float a = dot(rayDir, rayDir);
	const vec3 rayOriginMinusSphereCenter = rayOrigin - sphereCenter;
	const float b = 2.f*dot(rayDir, rayOriginMinusSphereCenter);
	const float c = dot(rayOriginMinusSphereCenter, rayOriginMinusSphereCenter) - sphereRadius*sphereRadius;

	const float discriminant = b*b - 4.f*a*c;
	if (discriminant < 0.f)
	{
		return false;//no intersection
	}
	else
	{
		const float denominator = 2.f*a;
		const float sqrtDiscriminant = sqrtf(discriminant);
		const float tLower = (-b - sqrtDiscriminant) / denominator;
		const float tHigher = (-b + sqrtDiscriminant) / denominator;

		*tVal = tLower >= 0.f ? tLower : tHigher;
		return *tVal >= 0.f;
	}
}


bool Q3DAudioObjectCreate(q3d_audio_client* const pSpatialAudio, Q3DSound*const pSound)
{
	assert(pSpatialAudio && pSound);
	assert(pSound->mFilename && pSound->mFilename[0]);

	pSound->mId = Q3DAudioObjectCreate(pSpatialAudio, pSound->mFilename);
	if (!gRolloffBugWorkaround)
	{
		//can't do this here or rolloff won't take because of an obscure q3d bug.  This afflicts object_dynamic
		const q3d_audio_result distanceRolloffResult = Q3DAudioObjectSetRolloff(
			pSpatialAudio,
			pSound->mId,
			pSound->mRolloffType,
			pSound->mRolloffMinDistance,
			pSound->mRolloffMaxDistance);
		return Q3D_AUDIO_HANDLE_VALID(pSound->mId) && Q3D_AUDIO_RESULT_SUCCESS(distanceRolloffResult);
	}
	else
	{
		return Q3D_AUDIO_HANDLE_VALID(pSound->mId);
	}
}

void Q3DAudioObjectSetRolloffDatafields(
	Q3DSound* pSoundObject, 
	const q3d_audio_distance_rolloff_type rolloffType,
	const float	rolloffMinDistance,
	const float	rolloffMaxDistance)
{
	assert(pSoundObject);
	pSoundObject->mRolloffType = rolloffType;
	pSoundObject->mRolloffMinDistance = rolloffMinDistance;
	pSoundObject->mRolloffMaxDistance = rolloffMaxDistance;
}

void Q3DAudioObjectCreateArray(
	q3d_audio_client*const pSpatialAudio, 
	Q3DSound* pSoundObject, 
	const int soundObjectNum,
	const q3d_audio_distance_rolloff_type rolloffType,
	const float	rolloffMinDistance,
	const float	rolloffMaxDistance)
{
	assert(pSpatialAudio);
	assert(pSoundObject && soundObjectNum > 0);

	for (int i = 0; i < soundObjectNum; ++i)
	{
		Q3DSound& sound = pSoundObject[i];

		//have to cache the rolloff parameters for when any nonlooping sound is recreated for multiple plays
		Q3DAudioObjectSetRolloffDatafields(&sound, rolloffType, rolloffMinDistance, rolloffMaxDistance);
		Q3DAudioObjectCreate(pSpatialAudio, &sound);
	}
}

bool Q3DAudioIsSoundPlaying(q3d_audio_client*const pSpatialAudio, const q3d_audio_sound_id soundId)
{
	assert(pSpatialAudio);

	const bool isSoundPlaying = q3dclient_audio_is_sound_playing(pSpatialAudio, soundId);
	if (gLogIsSoundPlaying)
	{
		LOGI("q3d_audio_is_sound_playing(%i)=%i", soundId, isSoundPlaying);
	}

	return isSoundPlaying;
}

q3d_audio_result Q3DAudioResumeSound(q3d_audio_client* const pSpatialAudio, const q3d_audio_sound_id soundId)
{
	assert(pSpatialAudio);
	Q3D_AUDIO_HANDLE_VALID(soundId);

	q3d_audio_result resumeResult = Q3D_AUDIO_ERROR_UNKNOWN;
	resumeResult = q3dclient_audio_resume_sound(pSpatialAudio, soundId);
	LOGI("q3d_audio_resume_sound(%i)=%i", soundId, resumeResult);
	//Q3D_AUDIO_ASSERT_SUCCESS(resumeResult);//can't guarantee this will ever succeed with non-looping sounds, since non-looping sounds invalidate their own handles upon playing to completion in a separate thread

	return resumeResult;
}

q3d_audio_result Q3DAudioPauseSound(q3d_audio_client* const pSpatialAudio, const q3d_audio_sound_id soundId)
{
	assert(pSpatialAudio);
	Q3D_AUDIO_HANDLE_VALID(soundId);

	q3d_audio_result pauseResult = Q3D_AUDIO_ERROR_UNKNOWN;
	if (Q3DAudioIsSoundPlaying(pSpatialAudio, soundId))
	{
		pauseResult = q3dclient_audio_pause_sound(pSpatialAudio, soundId);
		LOGI("q3d_audio_pause_sound(%i)=%i", soundId, pauseResult);
		//Q3D_AUDIO_ASSERT_SUCCESS(pauseResult);//can't guarantee this will ever succeed with non-looping sounds, since non-looping sounds invalidate their own handles upon playing to completion in a separate thread
	}
	return pauseResult;
}

bool Q3DAudioPreloadSoundFiles(
	q3d_audio_client*const pSpatialAudio,
	const Q3DSound*const pSoundFileNames,
	const int soundsNum)
{
	assert(pSpatialAudio);
	assert(pSoundFileNames);
	assert(soundsNum > 0);

	bool success = true;
	for (int i = 0; i < soundsNum; ++i)
	{
		success &= Q3DAudioPreloadSoundFile(pSpatialAudio, pSoundFileNames[i].mFilename);
	}

	return success;
}

bool Q3DAudioUnloadSoundFiles(
	q3d_audio_client*const pSpatialAudio,
	const Q3DSound*const pSound,
	const int soundsNum)
{
	assert(pSpatialAudio);
	assert(pSound);
	Q3D_AUDIO_ASSERT_HANDLE_VALID(pSound->mId);
	assert(soundsNum > 0);

	bool success = true;
	for (int i = 0; i < soundsNum; ++i)
	{
		success &= Q3D_AUDIO_RESULT_SUCCESS(Q3DAudioUnloadSoundFile(pSpatialAudio, pSound[i].mFilename));
	}

    assert(success);
	return success;
}

float ClampNormalizedRange(const float v)
{
	return clamp(v, 0.f, 1.f);
}

bool Q3DAudioSoundStop(q3d_audio_client* const pSpatialAudio, Q3DSound*const pSound)
{
	assert(pSpatialAudio && pSound);
	Q3D_AUDIO_HANDLE_VALID(pSound->mId);

	q3d_audio_vec3f position;
	const q3d_audio_result getPositionResult = Q3DAudioGetObjectPosition(pSpatialAudio, &position, pSound->mId, gLogObjectTransform);
	const q3d_audio_result stopSoundResult = Q3DAudioSoundStopAndInvalidateHandle(pSpatialAudio, pSound->mId);

	const bool soundObjectCreateResult = Q3DAudioObjectCreate(pSpatialAudio, pSound);

	const q3d_audio_result setPositionResult =
		q3dclient_audio_set_sound_object_position_in_vec3f(pSpatialAudio, pSound->mId, position);
	if(gLogObjectTransform)
	{
		LOGI("q3d_audio_set_sound_object_position_in_vec3f(%f,%f,%f)=%i", position.x, position.y, position.z, setPositionResult);
	}
	//Q3D_AUDIO_ASSERT_SUCCESS(setPositionResult);//can't guarantee this will ever succeed with non-looping sounds, since non-looping sounds invalidate their own handles upon playing to completion in a separate thread

	return	Q3D_AUDIO_RESULT_SUCCESS(getPositionResult) && 
			Q3D_AUDIO_RESULT_SUCCESS(stopSoundResult) && 
			soundObjectCreateResult && 
			Q3D_AUDIO_RESULT_SUCCESS(setPositionResult);
}


void Q3DSoundCrossFaderCommon::Play(
	q3d_audio_client*const pSpatialAudio,
	Q3DSound* pSounds,
	Q3DSoundCrossFaderCommon::StopSoundFunctionPtr pStopSound,
	const float soundsNum,
	const float timeToPlay,
	const float timeToCrossfade)
{
	assert(pSpatialAudio);
	mpSpatialAudio = pSpatialAudio;
	assert(soundsNum > 0);

	assert(pSounds);
	mpSound = pSounds;

	assert(soundsNum >= 2);
	mSoundNum = soundsNum;

	assert(pStopSound != 0);
	mpStopSound = pStopSound;

	const float highestSupportedFramerate = 1 / 120.f;
	assert(timeToPlay >= highestSupportedFramerate);
	mTimeToPlay = timeToPlay;

	assert(timeToCrossfade >= 0.f);
	mTimeToCrossfade = timeToCrossfade;

	mSoundCurrentIndex = 0;
	mTimeInCurrentState = 0.f;

	Q3DAudioSoundPlay(mpSpatialAudio, mpSound[mSoundCurrentIndex].mId, gLooping);
	mState = kPlaying;
	mSoundLastPlayedIndex = mSoundCurrentIndex;

	assert(Q3DAudioIsSoundPlaying(mpSpatialAudio, mpSound[mSoundCurrentIndex].mId));
}

Q3DSoundCrossFaderCommon::Q3DSoundCrossFaderCommon() :mState(kStopped)
{
}
int Q3DSoundCrossFaderCommon::GetNextSoundIndex()
{
	return (mSoundCurrentIndex + 1) % mSoundNum;
}

void Q3DSoundCrossFaderCommon::Stop()
{
	for(int i=0; i < mSoundNum; ++i)
	{
		Q3DAudioSoundStopAndInvalidateHandle(mpSpatialAudio, mpSound[i].mId);//means *this instance is now "dead" and can't play a second time -- would need to call Q3DAudioSoundStop() instead to be able to play again
	}
	mState = kStopped;
}

void Q3DSoundCrossFader::Play(
	q3d_audio_client*const pSpatialAudio,
	Q3DSound* pSounds,
	Q3DSoundCrossFaderCommon::StopSoundFunctionPtr pStopSound,
	const float soundsNum,
	const float timeToPlay,
	const float timeToCrossfade)
{
	mCommon.Play(pSpatialAudio, pSounds, pStopSound, soundsNum, timeToPlay, timeToCrossfade);
}

void Q3DSoundCrossFader::Update(const float elapsedTime)
{
	assert(mCommon.mState != Q3DSoundCrossFaderCommon::kStopped);
	if (mCommon.mState == Q3DSoundCrossFaderCommon::kStopped)
	{
		return;
	}

	mCommon.mTimeInCurrentState += elapsedTime;

	//state switch
	const float timeMax = (mCommon.mState == Q3DSoundCrossFaderCommon::kPlaying) ? mCommon.mTimeToPlay : mCommon.mTimeToCrossfade;
	if (mCommon.mTimeInCurrentState >= timeMax)
	{
		//switch states
		mCommon.mTimeInCurrentState -= timeMax;
        if (mCommon.mTimeInCurrentState > 0.f)
        {
            //if a timestep larger than mCommon.mTimeInCurrentState was passed in, still only change state once
            mCommon.mTimeInCurrentState = 0.f;
        }
		mCommon.mState = (mCommon.mState == Q3DSoundCrossFaderCommon::kPlaying) ? Q3DSoundCrossFaderCommon::kCrossFading : Q3DSoundCrossFaderCommon::kPlaying;
		switch (mCommon.mState)
		{
			case Q3DSoundCrossFaderCommon::kPlaying:
			{
				//just finished crossfading out the current sound
				//stop current sound
				mCommon.mpStopSound(mCommon.mpSpatialAudio, &mCommon.mpSound[mCommon.mSoundCurrentIndex]);
				mCommon.mSoundCurrentIndex = mCommon.GetNextSoundIndex();//the new sound is the current sound
				break;
			}
			case Q3DSoundCrossFaderCommon::kCrossFading:
			{
				//start crossfade
				const int newSoundIndex = mCommon.GetNextSoundIndex();
				const Q3DSound& sound = mCommon.mpSound[newSoundIndex];
				const q3d_audio_sound_id newSoundId = sound.mId;
				Q3DAudioSoundPlay(mCommon.mpSpatialAudio, newSoundId, gLooping);
				mCommon.mSoundLastPlayedIndex = newSoundIndex;
				Q3DAudioSetSoundVolume(mCommon.mpSpatialAudio, newSoundId, 0.f, gLogSoundVolume);
				
				if (gRolloffBugWorkaround)
				{
					//need to do this here since rolloff can't be consistently set immediately after creation
					Q3DAudioObjectSetRolloff(
						mCommon.mpSpatialAudio,
						sound.mId,
						sound.mRolloffType,
						sound.mRolloffMinDistance,
						sound.mRolloffMaxDistance);
				}
				break;
			}
			default:
			{
				assert(false);
				break;
			}
		}
	}

	//state update
	switch (mCommon.mState)
	{
		case Q3DSoundCrossFaderCommon::kPlaying:
		{
			break;//do nothing
		}
		case Q3DSoundCrossFaderCommon::kCrossFading:
		{
			const q3d_audio_sound_id newSoundFieldId = mCommon.mpSound[mCommon.GetNextSoundIndex()].mId;
			const q3d_audio_sound_id oldSoundFieldId = mCommon.mpSound[mCommon.mSoundCurrentIndex].mId;

			float oldSoundFieldVolume;
			Q3DAudioGetSoundVolume(mCommon.mpSpatialAudio, oldSoundFieldVolume, oldSoundFieldId, gLogSoundVolume);

			float newSoundFieldVolume;
			Q3DAudioGetSoundVolume(mCommon.mpSpatialAudio, newSoundFieldVolume, newSoundFieldId, gLogSoundVolume);

			const float volumeIncrement = (elapsedTime / mCommon.mTimeToCrossfade);
			Q3DAudioSetSoundVolume(
				mCommon.mpSpatialAudio,
				oldSoundFieldId,
				ClampNormalizedRange(oldSoundFieldVolume - volumeIncrement),
				gLogSoundVolume);

			Q3DAudioSetSoundVolume(
				mCommon.mpSpatialAudio,
				newSoundFieldId,
				ClampNormalizedRange(newSoundFieldVolume + volumeIncrement),
				gLogSoundVolume);
			break;
		}
		default:
		{
			assert(false);
			break;
		}
	}
}

void Q3DSoundCrossFader::Stop()
{
	mCommon.Stop();
}

void GazeSphere::Initialize(const float radius, const vec3& center)
{
	mRadius = radius;
	mCenter = center;
	mCooldownTimer = mChargeTimer = mChargeHysteresisTimer = 0.f;
}

GazeSphereManager::GazeSphereManager()
{
	Reset();
}

void GazeSphereManager::Reset()
{
	mGazeSpheresNum = 0;
	mGazeSphereLastActivatedIndex = mGazeSphereActivatedThisUpdateIndex = kGazeSphereActivatedThisUpdateIndexNone;
}

bool GazeSphereManager::AddGazeSpheres(GazeSphere*const gazeSpheres, const int gazeSpheresNum)
{
	const int totalGazeSpheres = gazeSpheresNum + mGazeSpheresNum;
	const bool valid = gazeSpheres && gazeSpheresNum > 0 && totalGazeSpheres <= kGazeSpheresMax;
	assert(valid);
	if (!valid)
	{
		return false;
	}

	memcpy(&mGazeSpheres[mGazeSpheresNum], gazeSpheres, sizeof(*gazeSpheres)*gazeSpheresNum);
	mGazeSpheresNum = totalGazeSpheres;
	return true;
}

void GazeSphereManager::Update(
	const glm::vec3& playerGazeRayOrigin,
	const glm::vec3& playerGazeRayDir,
	const float elapsedTime)
{
	mGazeSphereActivatedThisUpdateIndex = kGazeSphereActivatedThisUpdateIndexNone;

	//find closest sphere intersecting player gaze
	float tValSmallest = FLT_MAX;
	int sphereIndexClosestIntersection = kGazeSphereActivatedThisUpdateIndexNone;
	for (int sphereIndex = 0; sphereIndex < mGazeSpheresNum; ++sphereIndex)
	{
		float tVal;
		const GazeSphere& sphere = mGazeSpheres[sphereIndex];
		if (RaySphereIntersect(&tVal, playerGazeRayOrigin, playerGazeRayDir, sphere.mCenter, sphere.mRadius))
		{
			if (tVal < tValSmallest)
			{
				tVal = tValSmallest;
				sphereIndexClosestIntersection = sphereIndex;
			}
		}
	}

	for (int sphereIndex = 0; sphereIndex < mGazeSpheresNum; ++sphereIndex)
	{
		GazeSphere& gazeSphere = mGazeSpheres[sphereIndex];
		if (sphereIndex == sphereIndexClosestIntersection &&
			gazeSphere.mCooldownTimer <= 0.f &&
			sphereIndex != mGazeSphereLastActivatedIndex/*to activate a sphere a second time, player must look away first*/)
		{
			gazeSphere.mChargeHysteresisTimer = GAZE_SPHERE_CHARGE_HYSTERESIS_SECONDS;//being charged, so reset hysteresis
			if (gazeSphere.mChargeTimer <= 0.f)
			{
				gazeSphere.mChargeTimer = GAZE_SPHERE_CHARGE_SECONDS;//just started being charged this frame
			}
			else
			{
				gazeSphere.mChargeTimer -= elapsedTime;
				if (gazeSphere.mChargeTimer <= 0.f)
				{
					mGazeSphereLastActivatedIndex = mGazeSphereActivatedThisUpdateIndex = sphereIndex;			//activate sphere this frame
					gazeSphere.mCooldownTimer = GAZE_SPHERE_COOLDOWN_SECONDS;	//start cooldown to prevent repeated activations
				}
			}
		}
		else
		{
			if (gazeSphere.mCooldownTimer > 0.f)
			{
				gazeSphere.mCooldownTimer -= elapsedTime;
			}
			if (gazeSphere.mChargeHysteresisTimer > 0.f)
			{
				gazeSphere.mChargeHysteresisTimer -= elapsedTime;
				if (gazeSphere.mChargeHysteresisTimer <= 0.f)
				{
					gazeSphere.mChargeTimer = 0.f;//player looked away for too long and lost this gazeSphere's charge
				}
			}
		}
	}

	if (sphereIndexClosestIntersection != mGazeSphereLastActivatedIndex)
	{
		//player looked away from last activated sphere
		mGazeSphereLastActivatedIndex = kGazeSphereActivatedThisUpdateIndexNone;
	}
}
bool GazeSphereManager::GetGazeSphereActivatedThisUpdateIndex(int*const gazeSphereActivatedThisUpdateIndex) const
{
	const bool ret = mGazeSphereActivatedThisUpdateIndex != kGazeSphereActivatedThisUpdateIndexNone;
	if (ret)
	{
		*gazeSphereActivatedThisUpdateIndex = mGazeSphereActivatedThisUpdateIndex;
	}
	return ret;
}

const GazeSphere*const GazeSphereManager::GetGazeSpheres() const
{
	return mGazeSpheres;
}

int GazeSphereManager::GetGazeSpheresNum() const
{
	return mGazeSpheresNum;
}


//-----------------------------------------------------------------------------
unsigned int GetTimeMS()
//-----------------------------------------------------------------------------
{
	struct timeval t;
	t.tv_sec = t.tv_usec = 0;

	if (gettimeofday(&t, NULL) == -1)
	{
		return 0;
	}

	return (unsigned int)(t.tv_sec * 1000LL + t.tv_usec / 1000LL);
}

SpatialAudioCommon::SpatialAudioCommon()
	: mFrameTimer(64)
{
}

bool LoadTextureCommon(GLuint *pTexture, const char*const pFileName, const char*const pAppExternalPath)
{
	GLenum TexTarget;
	KtxTexture TexHelper;

	LOGI("Loading %s...", pFileName);

	// Open File
	int TexBuffSize;
	char *pTexBuffer = (char *)GetFileBuffer(&TexBuffSize, pAppExternalPath, pFileName);
	if (pTexBuffer == NULL)
	{
		LOGE("Unable to open file: %s", pFileName);
		return false;
	}

	// Use the helper to create the texture
	TKTXHeader* pOutHeader = NULL;
	TKTXErrorCode ResultCode = TexHelper.LoadKtxFromBuffer(pTexBuffer, TexBuffSize, pTexture, &TexTarget, pOutHeader);
	if (ResultCode != KTX_SUCCESS)
	{
		LOGE("Unable to load texture: %s", pFileName);
	}


	// No longer need buffers
	free(pTexBuffer);

	return true;
}

void InitializeModelAndTexture(
	SvrGeometry**const pModelGeometry,
	SvrShader*const pShader,
	GLuint*const pTextureHandle,
	const char*const pAppExternalPath,
	const char*const pObjFileLocalPath,
	const char*const pVertexShaderFileRelativePath,
	const char*const pFragmentShaderFileRelativePath,
	const char*const pVertexShaderDebugName,
	const char*const pFragmentShaderDebugName,
	const char*const pTextureFileRelativePath)
{
	assert(pModelGeometry && pShader && pTextureHandle && pAppExternalPath && pObjFileLocalPath && 
		pVertexShaderFileRelativePath && pVertexShaderDebugName && pFragmentShaderFileRelativePath && 
		pFragmentShaderDebugName && pTextureFileRelativePath);

	InitializeModel(
		pModelGeometry, 
		pShader, 
		pAppExternalPath, 
		pObjFileLocalPath, 
		pVertexShaderFileRelativePath, 
		pFragmentShaderFileRelativePath, 
		pVertexShaderDebugName,
		pFragmentShaderDebugName);

	LoadTextureCommon(pTextureHandle, pTextureFileRelativePath, pAppExternalPath);
}

void SpatialAudioCommon::Initialize()
{
	// Load the config file
	static const char* gAppConfigFilePath = "app_config.txt";

	const int kFilePathMaxLen = 512;
	char filePath[kFilePathMaxLen];
	snprintf(filePath, kFilePathMaxLen, "%s/%s", mAppContext.externalPath, gAppConfigFilePath);
	LoadVariableFile(filePath);

	mPaused = false;
	mTouchDownLastFrame = false;

	const q3d_audio_result createResult = 
        Q3DAudioCreate(&mpSpatialAudio, mAppContext.activity->vm, mAppContext.activity->clazz);
    assert(mpSpatialAudio != 0);
    Q3D_AUDIO_ASSERT_SUCCESS(createResult);

	const q3d_audio_version audioVersion = q3dclient_audio_get_version(GetSpatialAudio());
	LOGI("q3d_audio_get_version()=%i.%i", audioVersion.major, audioVersion.minor);

	const char* versionString = q3dclient_audio_get_version_string(GetSpatialAudio());
	LOGI("q3d_audio_get_version_string()=%s", versionString);

	uint32_t periodMs;
	const q3d_audio_result getRenderingDelayResult = q3dclient_audio_get_rendering_delay(mpSpatialAudio, &periodMs);
	LOGI("q3d_audio_get_rendering_delay()=%i and reports %ims", getRenderingDelayResult, periodMs);
	Q3D_AUDIO_ASSERT_SUCCESS(getRenderingDelayResult);

	SetReverb(GetUserConfigReverbConfigurationIndex());
	mCurrentEyeBuffer = 0;

	if (gRenderFieldOfView < mAppContext.targetEyeFovYDeg)
	{
		LOGI("gRenderFieldOfView set to targetEyeFovYDeg");
		gRenderFieldOfView = mAppContext.targetEyeFovYDeg;
	}

	if (gMsaaSamples == 1)
	{
		LOGI("Rendering to non-MSAA target");
	}
	else if (gMsaaSamples == 2 || gMsaaSamples == 4)
	{
		LOGI("Rendering to %dX-MSAA target", gMsaaSamples);
	}
	else
	{
		LOGI("Unsupported MSAA samples: %d.  Rendering to non-MSAA target for both eyes", gMsaaSamples);
		gMsaaSamples = 1;
	}

	SvrApplication::Initialize();

	mAppContext.cpuPerfLevel = kPerfMedium;
	mAppContext.gpuPerfLevel = kPerfMedium;

	mOrientationTexture = 0;
	if (gForceOrientationTexture)
	{
		const char *pOrientationTexFile = "textures/bbb_title.ktx";
		LoadTextureCommon(&mOrientationTexture, pOrientationTexFile, mAppContext.externalPath);
	}

	float aspect = (float)mAppContext.targetEyeWidth / (float)mAppContext.targetEyeHeight;
	float fieldOfView = gRenderFieldOfView * DEG_TO_RAD;

	mProjMtx = glm::perspective(fieldOfView, aspect, 0.1f, 3000.0f);
	mHeadPosition = glm::vec3(0.0f);

	mLastFPSTime = GetTimeMS();
	mCurrentFPS = 0.0f;

	InitializeModel(
		&mpReticleModel,
		&mpReticleShader,
		mAppContext.externalPath,
		"models/cone.obj",
		"shaders/model_v.glsl",
		"shaders/model_f.glsl",
		"modelVs",
		"modelFs");
}

void SpatialAudioCommon::Shutdown()
{
    if (mpSpatialAudio)
    {
        const q3d_audio_result destroyResult = Q3DAudioDestroy(&mpSpatialAudio);
        Q3D_AUDIO_ASSERT_SUCCESS(destroyResult);
    }

    if (mOrientationTexture)
    {
        DestroyTexture(&mOrientationTexture);
    }
	
    if(mpReticleModel)
    {
        DestroyGeometry(&mpReticleModel);
    }

	mpReticleShader.Destroy();
	mGazeSphereManager.Reset();

	SvrApplication::Shutdown();
}

void SpatialAudioCommon::AllocateEyeBuffers()
{
	LOGI("ObjectStatic - Allocating Separate Single Eye Buffers");

	LOGI("**************************************************");
	LOGI("Creating %dx%d (%d sample[s]) framebuffer...", mAppContext.targetEyeWidth, mAppContext.targetEyeHeight, gMsaaSamples);
	LOGI("**************************************************");

	for (int whichBuffer = 0; whichBuffer < SVR_NUM_EYE_BUFFERS; whichBuffer++)
	{
		SvrRenderTargetStereoInitialize(mAppContext.eyeBuffers[whichBuffer].eyeTarget, mAppContext.targetEyeWidth, mAppContext.targetEyeHeight, gMsaaSamples);
		SvrRenderTargetStereoInitialize(mOverlayBuffers[whichBuffer], mAppContext.targetEyeWidth, mAppContext.targetEyeHeight, gMsaaSamples);

		if (gMsaaSamples == 2 || gMsaaSamples == 4)
		{
			const int noMsaaSamples = 1;
			SvrRenderTargetStereoInitialize(mSingleSampleBuffers[whichBuffer], mAppContext.targetEyeWidth, mAppContext.targetEyeHeight, noMsaaSamples);
			SvrRenderTargetStereoInitialize(mSingleSampleOverlayBuffers[whichBuffer], mAppContext.targetEyeWidth, mAppContext.targetEyeHeight, noMsaaSamples);
		}
	}
}

void SpatialAudioCommon::Update()
{
	PROFILE_ENTER(GROUP_WORLDRENDER, 0, "Update");

	mFrameTimer.Start();
	SvrApplication::Update();

	// Update the time
	unsigned int TimeNow = GetTimeMS();

	unsigned int ReportTimeFPS = 1000;
	mFrameCount++;
	if (TimeNow - mLastFPSTime > ReportTimeFPS)
	{
		float ElapsedSeconds = (float)(TimeNow - mLastFPSTime) / 1000.0f;
		mCurrentFPS = (float)mFrameCount / ElapsedSeconds;
		if (gLogFramesPerSecond)
		{
			LOGI("FPS: %0.2f", mCurrentFPS);
		}

		mFrameCount = 0;
		mLastFPSTime = GetTimeMS();;
	}

	if (mInput.IsPointerDown(0))
	{
		float xPos;
		float yPos;
		if (mInput.GetPointerLocation(0, xPos, yPos))
		{
			// LOGI("Touch Position: (%0.2f, %0.2f)", xPos, yPos);
		}

		// Recenter the pose
		svrRecenterPose();

		if (!mTouchDownLastFrame)
		{
			OnTouchScreen();//derived class opportunity to react to screen touch

			mTouchDownLastFrame = true;
		}
	}
	else
	{
		mTouchDownLastFrame = false;
	}

	PROFILE_EXIT(GROUP_WORLDRENDER);
}

glm::vec3 SpatialAudioCommon::GetPlayerHeadPositionWorldSpace()
{
	return mPlayerHeadPositionWorldSpace;
}
glm::quat SpatialAudioCommon::GetPlayerHeadOrientationWorldSpace()
{
	return mPlayerHeadOrientationWorldSpace;
}

void SpatialAudioCommon::Render()
{
	unsigned int TimeNow = GetTimeMS();
	if (gLastRenderTime == 0)
	{
		gLastRenderTime = TimeNow;
	}

	if (gForcedVSyncTime != 0)
	{
		// Forcing VSync
		if (TimeNow - gLastRenderTime < gForcedVSyncTime)
		{
			return;
		}
	}

	float elapsedTime = (float)(TimeNow - gLastRenderTime) / 1000.0f;

	const float epsilon = 1 / 900.f;
	if (elapsedTime < epsilon)
	{
		return;
	}

	gLastRenderTime = TimeNow;

	PROFILE_ENTER(GROUP_WORLDRENDER, 0, "Render");

	GL(glEnable(GL_SCISSOR_TEST));
	GL(glEnable(GL_DEPTH_TEST));
	GL(glDepthFunc(GL_LESS));
	GL(glDepthMask(GL_TRUE));

	float predictedTimeMs = svrGetPredictedDisplayTime();
	svrHeadPoseState poseState = svrGetPredictedHeadPose(predictedTimeMs);

	const vec3 playerHeadPositionCameraSpace = vec3(poseState.pose.position.x, poseState.pose.position.y, poseState.pose.position.z);
	mPlayerHeadPositionWorldSpace = SvrPosePositionCameraSpaceToWorldSpace(playerHeadPositionCameraSpace);
	Q3DAudioSetHeadPosition(mpSpatialAudio, mPlayerHeadPositionWorldSpace, gLogHeadPoseUpdate);

	const quat playerHeadOrientationCameraSpace =
		quat(	poseState.pose.rotation.w,
				poseState.pose.rotation.x,
				poseState.pose.rotation.y,
				poseState.pose.rotation.z);
	mPlayerHeadOrientationWorldSpace = SvrPoseRotationCameraSpaceToWorldSpace(playerHeadOrientationCameraSpace);
	Q3DAudioSetHeadRotation(mpSpatialAudio, mPlayerHeadOrientationWorldSpace, gLogHeadPoseUpdate);

	//gaze manager update
	const vec3 worldForwardSvrBasis(0.f, 0.f, -1.f);
	const vec3 forwardWorldSpace = GetPlayerHeadOrientationWorldSpace()*worldForwardSvrBasis;
	mGazeSphereManager.Update(GetPlayerHeadPositionWorldSpace(), forwardWorldSpace, elapsedTime);


	RenderCallback(elapsedTime);

	if (gLogSensorData)
	{
		LOGE("Position: (%0.2f, %0.2f, %0.2f); Orientation: (%0.2f, %0.2f, %0.2f, %0.2f)", 
			poseState.pose.position.x,
			poseState.pose.position.y,
			poseState.pose.position.z,
			poseState.pose.rotation.x,
			poseState.pose.rotation.y,
			poseState.pose.rotation.z,
			poseState.pose.rotation.w);
	}

	if (!(poseState.poseStatus & kTrackingPosition))
	{
		poseState.pose.position.x = mHeadPosition.x;
		poseState.pose.position.y = mHeadPosition.y;
		poseState.pose.position.z = mHeadPosition.z;
	}

	float currentIpd = DEFAULT_IPD;
	float headHeight = DEFAULT_HEAD_HEIGHT;
	float headDepth = DEFAULT_HEAD_DEPTH;
	SvrGetEyeViewMatrices(poseState, false,
		currentIpd, headHeight, headDepth,
		mViewMtx[kLeft], mViewMtx[kRight]);

	GL(glClearColor(0.2f, 0.2f, 0.2f, 1.0));

	int numRenderCalls = SVR_NUM_EYES;

	GLenum invalidateList[] = { GL_DEPTH_ATTACHMENT, GL_STENCIL_ATTACHMENT };
	GLsizei invalidateListCount = 2;

	for (int whichEye = 0; whichEye < numRenderCalls; whichEye++)
	{
		// These get changed when rendering the GUI so put them back now
		GL(glEnable(GL_SCISSOR_TEST));
		GL(glEnable(GL_DEPTH_TEST));
		GL(glDepthFunc(GL_LESS));
		GL(glDepthMask(GL_TRUE));

		//main rendering pass
		{
			//Bind the eye buffer
			SvrRenderTarget& renderTarget = mAppContext.eyeBuffers[mAppContext.eyeBufferIndex].eyeTarget[whichEye];
			renderTarget.Bind();
			PrepareEyeBuffer(mAppContext.targetEyeWidth, mAppContext.targetEyeHeight);
			GL(glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT));

			svrBeginEye(kEyeBufferStereoSeparate, (svrWhichEye)whichEye);

			RenderModelCallback(whichEye, elapsedTime);//derived class opportunity to render objects
		
			// Eye is done being rendered
			svrEndEye(kEyeBufferStereoSeparate, (svrWhichEye)whichEye);

			//ensure depth buffer doesn't get copied from rendering memory to main memory, since it has no more use
			GL(glInvalidateFramebuffer(GL_FRAMEBUFFER, invalidateListCount, invalidateList));

			//Unbind the eye buffer (call so attachments are invalidated correctly)
			renderTarget.Unbind();
		}

		//render overlay -- anything rendered here is affixed to the player's "face/HUD" with no jitter; timewarp won't violate this
		{
			SvrRenderTarget& renderTarget = mOverlayBuffers[mAppContext.eyeBufferIndex][whichEye];
			renderTarget.Bind();
			PrepareEyeBuffer(mAppContext.targetEyeWidth, mAppContext.targetEyeHeight);
			GL(glClearColor(0.0f, 0.0f, 0.0f, 0.0f));
			GL(glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT));

			//gaze reticle render
			{
				mpReticleShader.Bind();
				mpReticleShader.SetUniformMat4("projMtx", GetProjMtx());
				mpReticleShader.SetUniformMat4("viewMtx", GetViewMtx()[whichEye]);

				const mat4 coneLocalRotationToLookDownForwardAxis = rotate(radians(-90.f), vec3(1.f, 0.f, 0.f));
				const mat4 coneLocalOffset = translate(mat4(1.f), vec3(0.f, 0.f, -5.f));
				const mat4 coneLocalToWorldRotation = mat4_cast(GetPlayerHeadOrientationWorldSpace());
				const mat4 coneLocalToWorldTranslation = translate(mat4(1.f), GetPlayerHeadPositionWorldSpace());

				mat4 coneLocalToWorld = coneLocalToWorldTranslation*coneLocalToWorldRotation*coneLocalOffset*coneLocalRotationToLookDownForwardAxis;
				mpReticleShader.SetUniformMat4("mdlMtx", coneLocalToWorld);

				vec4 coneColor(1.f, 1.f, 1.f, 1.f);
				mpReticleShader.SetUniformVec4("modelColor", coneColor);
				mpReticleModel->Submit();
			}

			//ensure depth buffer doesn't get copied from rendering memory to main memory, since it has no more use
			GL(glInvalidateFramebuffer(GL_FRAMEBUFFER, invalidateListCount, invalidateList));

			//Unbind the eye buffer (call so attachments are invalidated correctly)
			renderTarget.Unbind();
		}
	}

	//call update after this class and derived classes have performed their updates
	Q3DAudioUpdate(mpSpatialAudio, gLogAudioUpdate);


	svrFrameParams frameParams;
	memset(&frameParams, 0, sizeof(frameParams));
	frameParams.frameIndex = mAppContext.frameCount;
	frameParams.eyeBufferType = kEyeBufferStereoSeparate;
	frameParams.eyeLayers[0].imageType = kTypeTexture;
	frameParams.eyeLayers[1].imageType = kTypeTexture;
	frameParams.overlayLayers[0].imageType = kTypeTexture;
	frameParams.overlayLayers[1].imageType = kTypeTexture;

	if (gMsaaSamples == 2 || gMsaaSamples == 4)
	{
		ResolveMsaaTargetStereo(mAppContext.eyeBuffers[mAppContext.eyeBufferIndex].eyeTarget, mSingleSampleBuffers[mAppContext.eyeBufferIndex]);
		ResolveMsaaTargetStereo(mOverlayBuffers[mAppContext.eyeBufferIndex], mSingleSampleOverlayBuffers[mAppContext.eyeBufferIndex]);

		RenderLayersSetStereo(frameParams.eyeLayers, mSingleSampleBuffers[mAppContext.eyeBufferIndex]);
		RenderLayersSetStereo(frameParams.overlayLayers, mSingleSampleOverlayBuffers[mAppContext.eyeBufferIndex]);
	}
	else
	{
		RenderLayersSetStereo(frameParams.eyeLayers, mAppContext.eyeBuffers[mAppContext.eyeBufferIndex].eyeTarget);
		RenderLayersSetStereo(frameParams.overlayLayers, mOverlayBuffers[mAppContext.eyeBufferIndex]);
	}

	// Fill in the rest of the eye layer data that is common to MSAA or not
	LayersMaskAndLayout(frameParams.eyeLayers);
	LayersMaskAndLayout(frameParams.overlayLayers);

	frameParams.headPoseState = poseState;
	frameParams.fieldOfView = gRenderFieldOfView * DEG_TO_RAD;
	frameParams.minVsyncs = 1;
	PROFILE_EXIT(GROUP_WORLDRENDER);

	if (gForceOrientationTexture)
	{
		frameParams.eyeLayers[0].imageHandle = mOrientationTexture;
		frameParams.eyeLayers[1].imageHandle = mOrientationTexture;
	}

	// LOGI("Submitting Eye Buffers: Left = %d; Right = %d", frameParams.eyeBufferArray[0], frameParams.eyeBufferArray[1]);

	svrSubmitFrame(&frameParams);

	mAppContext.eyeBufferIndex = (mAppContext.eyeBufferIndex + 1) % SVR_NUM_EYE_BUFFERS;
	mCurrentEyeBuffer = (mCurrentEyeBuffer + 1) % SVR_NUM_EYE_BUFFERS;
	mFrameTimer.Stop();
}

q3d_audio_client* SpatialAudioCommon::GetSpatialAudio()
{
	return mpSpatialAudio;
}
const q3d_audio_client* SpatialAudioCommon::GetSpatialAudio() const
{
	return mpSpatialAudio;
}

glm::mat4*const SpatialAudioCommon::GetViewMtx()
{
	return mViewMtx;
}
const glm::mat4*const SpatialAudioCommon::GetViewMtx() const
{
	return mViewMtx;
}

glm::mat4& SpatialAudioCommon::GetProjMtx()
{
	return mProjMtx;
}
const glm::mat4& SpatialAudioCommon::GetProjMtx() const
{
	return mProjMtx;
}

bool SpatialAudioCommon::IsPaused() const
{
	return mPaused;
}
void SpatialAudioCommon::PausedInvertAndReflectInGazeBox(bool*const gazeBoxActive)
{
	assert(gazeBoxActive);

	const bool pausedNow = !mPaused;
	mPaused = pausedNow;
	gazeBoxActive[kGazeBoxPause] = pausedNow;
}

bool SpatialAudioCommon::SetReverb(const int configFileSettingIndex)
{
	bool enabled;
	vec3 roomDimensions;
	float gain;
	float timeAdjust;
	float brightnessAdjust;
	int wallMaterial;
	int ceilingMaterial;
	int floorMaterial;

	switch (configFileSettingIndex)
	{
		case kGazeBoxReverbA:
		{
			enabled = gReverbA_Enabled;
			roomDimensions = vec3(gReverbA_RoomX, gReverbA_RoomY, gReverbA_RoomZ);
			gain = gReverbA_Gain;
			timeAdjust = gReverbA_TimeAdjust;
			brightnessAdjust = gReverbA_BrightnessAdjust;
			wallMaterial = gReverbA_RoomWallMaterial;
			ceilingMaterial = gReverbA_RoomCeilingMaterial;
			floorMaterial = gReverbA_RoomFloorMaterial;
			break;
		}
		case kGazeBoxReverbB:
		{
			enabled = gReverbB_Enabled;
			roomDimensions = vec3(gReverbB_RoomX, gReverbB_RoomY, gReverbB_RoomZ);
			gain = gReverbB_Gain;
			timeAdjust = gReverbB_TimeAdjust;
			brightnessAdjust = gReverbB_BrightnessAdjust;
			wallMaterial = gReverbB_RoomWallMaterial;
			ceilingMaterial = gReverbB_RoomCeilingMaterial;
			floorMaterial = gReverbB_RoomFloorMaterial;
			break;
		}
		case kGazeBoxReverbC:
		{
			enabled = gReverbC_Enabled;
			roomDimensions = vec3(gReverbC_RoomX, gReverbC_RoomY, gReverbC_RoomZ);
			gain = gReverbC_Gain;
			timeAdjust = gReverbC_TimeAdjust;
			brightnessAdjust = gReverbC_BrightnessAdjust;
			wallMaterial = gReverbC_RoomWallMaterial;
			ceilingMaterial = gReverbC_RoomCeilingMaterial;
			floorMaterial = gReverbC_RoomFloorMaterial;
			break;
		}
		default:
		{
			assert(false);
			break;
		}
	}

	Q3DAudioSetReverb(
		mpSpatialAudio, 
		enabled, 
		gain, 
		timeAdjust, 
		brightnessAdjust, 
		roomDimensions, 
		(q3d_audio_material_type)wallMaterial,
		(q3d_audio_material_type)ceilingMaterial,
		(q3d_audio_material_type)floorMaterial);
}

bool SpatialAudioCommon::SetReverbAndReflectInBoolArray(bool*const gazeBoxActive, const int gazeSphereActivatedIndex)
{
	assert(gazeBoxActive);
	assert(ConfigurationIndexIsReverb(gazeSphereActivatedIndex));

	SetReverb(gazeSphereActivatedIndex);
	SetBoolsToFalse(&gazeBoxActive[kGazeBoxReverbA], kGazeBoxReverbNum);
	gazeBoxActive[gazeSphereActivatedIndex] = true;
}


GazeSphereManager* SpatialAudioCommon::GetGazeSphereManager()
{
	return &mGazeSphereManager;
}

bool GetRolloffSettings(
	q3d_audio_distance_rolloff_type*const type, 
	float*const minDistance, 
	float*const maxDistance, 
	const int rollOffConfigurationIndex)
{
	assert(type && minDistance && maxDistance);
	assert(ConfigurationIndexIsRolloff(rollOffConfigurationIndex));
	switch (rollOffConfigurationIndex)
	{
		case SpatialAudioCommon::kGazeBoxRolloffA:
		{
			*type = (q3d_audio_distance_rolloff_type)gRolloffA_Type;
			*minDistance = gRolloffA_MinDistance;
			*maxDistance = gRolloffA_MaxDistance;
			break;
		}
		case SpatialAudioCommon::kGazeBoxRolloffB:
		{
			*type = (q3d_audio_distance_rolloff_type)gRolloffB_Type;
			*minDistance = gRolloffB_MinDistance;
			*maxDistance = gRolloffB_MaxDistance;
			break;
		}
		case SpatialAudioCommon::kGazeBoxRolloffC:
		{
			*type = (q3d_audio_distance_rolloff_type)gRolloffC_Type;
			*minDistance = gRolloffC_MinDistance;
			*maxDistance = gRolloffC_MaxDistance;
			break;
		}
		default:
		{
			assert(false);
			return false;
		}
	}

	return true;
}
int GetUserConfigRolloffConfigurationIndex()
{
	return gRolloff_Setting + SpatialAudioCommon::kGazeBoxRolloffA;
}
int GetUserConfigReverbConfigurationIndex()
{
	return gReverb_Setting + SpatialAudioCommon::kGazeBoxReverbA;
}

bool ConfigurationIndexIsReverb(const int index)
{
	return index >= SpatialAudioCommon::kGazeBoxReverbA && index <= SpatialAudioCommon::kGazeBoxReverbC;
}
bool ConfigurationIndexIsRolloff(const int index)
{
	return index >= SpatialAudioCommon::kGazeBoxRolloffA && index <= SpatialAudioCommon::kGazeBoxRolloffC;
}

void InitializeGazeBoxes(GazeSphereManager*const pGazeSphereManager, const int boxNum)
{
	assert(pGazeSphereManager && boxNum > 0);
	assert(boxNum < GazeSphereManager::kGazeSpheresMax);
	GazeSphere gazeSpheresToAdd[GazeSphereManager::kGazeSpheresMax];

	const float boxOriginOffsetX = 2.f;
	const vec3 boxOriginOffset(boxOriginOffsetX, 0.f, 0.f);
	vec3 boxOriginNext = vec3(-.5f*((float)boxNum - 1)*boxOriginOffsetX, 3.f, -8.f);
	for (int i = 0; i < boxNum; ++i)
	{
		gazeSpheresToAdd[i].Initialize(1.f, boxOriginNext);
		boxOriginNext += boxOriginOffset;
	}
	pGazeSphereManager->AddGazeSpheres(gazeSpheresToAdd, boxNum);
}

void GazeBoxRender(
	SvrGeometry*const pModel, 
	SvrShader*const pShader, 
	const GLuint*const pTextures, 
    const GLuint textureSamplerId,
	const GazeSphere*const pGazeSpheres, 
	const int gazeSpheresNum,
	const bool*const gazeBoxesActive,
	float*const gazeBoxLastHsvSaturation,
	float*const gazeBoxLastHsvValue,
	const float elapsedTime,
	const int whichEye,
	mat4& projMtx,/*conceptually const*/
	mat4*const viewMtx/*conceptually const*/)
{
	assert(	pModel && pShader && pTextures && pGazeSpheres && gazeBoxesActive && gazeBoxLastHsvSaturation && 
			gazeBoxLastHsvValue && viewMtx);
	assert(gazeSpheresNum > 0);
	assert(elapsedTime > 0.f);

	//gaze box render
	pShader->Bind();
	for (int gazeBoxIndex = 0; gazeBoxIndex < gazeSpheresNum; ++gazeBoxIndex)
	{
		const mat4 boxModelRotateRight = rotate(mat4(1.f), radians(90.f), vec3(1.f, 0.f, 0.f));
		const mat4 boxModelRotateUp = rotate(mat4(1.f), radians(-90.f), vec3(0.f, 1.f, 0.f));
		const mat4 boxModelLocalRotation = boxModelRotateUp*boxModelRotateRight;

		const GazeSphere& gazeSphere = pGazeSpheres[gazeBoxIndex];
		const mat4 boxModelTranslation = translate(mat4(1.f), gazeSphere.mCenter);
		mat4 boxModelLocalToWorld = boxModelTranslation*boxModelLocalRotation;

		pShader->SetUniformMat4("projMtx", projMtx);
		pShader->SetUniformMat4("viewMtx", viewMtx[whichEye]);
		pShader->SetUniformMat4("mdlMtx", boxModelLocalToWorld);
		pShader->SetUniformSampler("srcTex", pTextures[gazeBoxIndex], GL_TEXTURE_2D, textureSamplerId);

		const vec3 rgbNew = GazeSphereColorProcess(
			&gazeBoxLastHsvSaturation[gazeBoxIndex],
			&gazeBoxLastHsvValue[gazeBoxIndex],
			gazeSphere,
			SPATIAL_AUDIO_GAZE_BOX_DEFAULT_COLOR,
			gazeBoxesActive[gazeBoxIndex],
			elapsedTime);
		vec4 rgbNew4(rgbNew.x, rgbNew.y, rgbNew.z, 1.f);
		pShader->SetUniformVec4("modelColor", rgbNew4);

		pModel->Submit();
	}
}


void InitializeGazeBoxRenderResources(
	SvrGeometry**const pModel,
	SvrShader*const pShader,
	GLuint*const pTextures,
    GLuint*const gazeBoxTextureSampler,
	float*const lastSaturation, 
	float*const lastValue, 
	const char(*const texturePaths)[SPATIAL_AUDIO_TEXTURE_PATH_MAX_LEN],
	const char*const pAppContextExternalPath,
	const int gazeBoxNum)
{
	assert(pModel && pShader && pTextures && gazeBoxTextureSampler && lastSaturation && lastValue && pAppContextExternalPath);
	assert(gazeBoxNum > 0);

	//gaze manager
	InitializeModel(
		pModel,
		pShader,
		pAppContextExternalPath,
		"models/texturedCube.obj",
		"shaders/gazeBox_v.glsl",
		"shaders/gazeBox_f.glsl",
		"boxVs",
		"boxFs");

	for (int gazeBoxIndex = 0; gazeBoxIndex < gazeBoxNum; ++gazeBoxIndex)
	{
		LoadTextureCommon(&pTextures[gazeBoxIndex], texturePaths[gazeBoxIndex], pAppContextExternalPath);

		InitializeLastSaturationAndValue(
			&lastSaturation[gazeBoxIndex],
			&lastValue[gazeBoxIndex],
			SPATIAL_AUDIO_GAZE_BOX_DEFAULT_COLOR);
	}

    *gazeBoxTextureSampler = GazeBoxSamplerCreate();
}
void DestroyGazeBoxRenderResources(
    SvrGeometry**const pModel,
    SvrShader*const pShader,
    GLuint*const pTextures,
    GLuint*const gazeBoxTextureSampler,
    const int gazeBoxNum)
{
    assert(pModel && pShader && pTextures && gazeBoxTextureSampler);
    assert(gazeBoxNum > 0);

    if (*pModel)
    {
        DestroyGeometry(pModel);
    }
    pShader->Destroy();
    for (int i = 0; i < gazeBoxNum; ++i)
    {
        GLuint& texture = pTextures[i];
        if (texture)
        {
            DestroyTexture(&texture);
        }
    }
    if (*gazeBoxTextureSampler)
    {
        GazeBoxSamplerDestroy(gazeBoxTextureSampler);
    }
}

void GetDefaultGazeBoxTexturePaths(char(*texturePathsDst)[SPATIAL_AUDIO_TEXTURE_PATH_MAX_LEN], const int texturesNum)
{
	const int texturesMax = 8;
	assert(texturesNum > 0 && texturesNum <= texturesMax);

	const char texturePathsSrc[texturesMax][SPATIAL_AUDIO_TEXTURE_PATH_MAX_LEN] =
	{
		//keep synchronized with corresponding unnamed enum in SpatialAudioCommon
		"textures/pause.ktx",
		"textures/loop.ktx",
		"textures/reverb_a.ktx",
		"textures/reverb_b.ktx",
		"textures/reverb_none.ktx",
		"textures/rolloff_a.ktx",
		"textures/rolloff_b.ktx",
		"textures/rolloff_c.ktx"
	};

	for (int i = 0; i < texturesNum; ++i)
	{
		strncpy(&texturePathsDst[i][0], &texturePathsSrc[i][0], SPATIAL_AUDIO_TEXTURE_PATH_MAX_LEN);
	}
}

vec3 GazeSphereColorProcess(
	float*const gazeSphereLastHsvSaturation,
	float*const gazeSphereLastHsvValue,
	const GazeSphere& gazeSphere,
	const vec3& defaultRgb,
	const bool gazeSphereActive,
	const float elapsedTime)
{
	assert(gazeSphereLastHsvSaturation && gazeSphereLastHsvValue);
	assert(elapsedTime > 0.f);
	assert(InNormalizedRange(defaultRgb.x) && InNormalizedRange(defaultRgb.y) && InNormalizedRange(defaultRgb.z));

	float hueDegrees, saturationTarget, valueTarget;
	RgbToHsv(&hueDegrees, &saturationTarget, &valueTarget, defaultRgb);

	if (gazeSphere.mCooldownTimer > 0.f)
	{
		saturationTarget *= (GAZE_SPHERE_COOLDOWN_SECONDS - gazeSphere.mCooldownTimer) / GAZE_SPHERE_COOLDOWN_SECONDS;
	}

	if (gazeSphere.mChargeTimer > 0.f)
	{
		float start, end;
		if (!gazeSphereActive)
		{
			start = SPATIAL_AUDIO_HSV_VALUE_DEACTIVATED;
			end = SPATIAL_AUDIO_HSV_VALUE_ACTIVATED;
		}
		else
		{
			start = SPATIAL_AUDIO_HSV_VALUE_ACTIVATED;
			end = SPATIAL_AUDIO_HSV_VALUE_DEACTIVATED;
		}
		valueTarget = Lerp(start, end, gazeSphere.mChargeTimer, GAZE_SPHERE_CHARGE_SECONDS);
	}
	else if (gazeSphereActive)
	{
		valueTarget = SPATIAL_AUDIO_HSV_VALUE_ACTIVATED;
	}
	else
	{
		valueTarget = SPATIAL_AUDIO_HSV_VALUE_DEACTIVATED;
	}

	float& lastHsvSaturation = *gazeSphereLastHsvSaturation;
	lastHsvSaturation = InfiniteImpulseResponseCalc(
		lastHsvSaturation,
		saturationTarget,
		InfiniteImpulseResponseFactorCalc(8.f, elapsedTime));

	float& lastHsvValue = *gazeSphereLastHsvValue;
	lastHsvValue = InfiniteImpulseResponseCalc(
		lastHsvValue,
		valueTarget,
		InfiniteImpulseResponseFactorCalc(2.f, elapsedTime));

	return HsvToRgb(hueDegrees, lastHsvSaturation, lastHsvValue);
}

void SetGazeBoxActiveLooping(bool*const gazeBoxActive, const bool looping)
{
	assert(gazeBoxActive);
	gazeBoxActive[SpatialAudioCommon::kGazeBoxLoop] = looping;
}
void SetGazeBoxActiveDefaultsReverbRolloff(bool*const gazeBoxActive, const bool looping)
{
	assert(gazeBoxActive);

	SetBoolsToFalse(&gazeBoxActive[0], SpatialAudioCommon::kGazeBoxNum);
	gazeBoxActive[GetUserConfigReverbConfigurationIndex()] = true;
	gazeBoxActive[GetUserConfigRolloffConfigurationIndex()] = true;
	SetGazeBoxActiveLooping(gazeBoxActive, looping);
}

GLuint GazeBoxSamplerCreate()
{
    GLuint gazeBoxTextureSampler;
    glGenSamplers(1, &gazeBoxTextureSampler);
    glSamplerParameteri(gazeBoxTextureSampler, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glSamplerParameteri(gazeBoxTextureSampler, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

    return gazeBoxTextureSampler;
}
void GazeBoxSamplerDestroy(GLuint*const gazeBoxTextureSampler)
{
    assert(gazeBoxTextureSampler);
    glDeleteSamplers(1, gazeBoxTextureSampler);
    *gazeBoxTextureSampler = 0;
}

void DestroyTexture(GLuint*const texture)
{
	assert(texture);
	if (*texture != 0)
	{
		GL(glDeleteTextures(1, texture));
	}
	*texture = 0;
}


void RgbToHsv(float*const hueDegrees, float*const saturation, float*const value, const vec3& rgb)
{
	assert(hueDegrees && saturation && value);

	const float r = rgb.x;
	const float g = rgb.y;
	const float b = rgb.z;
	const float cmax = max(max(r, g), b);
	const float cmin = min(min(r, g), b);

	const float delta = cmax - cmin;
	const float tolerance = .001f;
	if (delta < tolerance)
	{
		*hueDegrees = 0.f;
	}
	else if (r == cmax)
	{
		*hueDegrees = fmodf((g - b) / delta + 6.f, 6.f);
	}
	else if (g == cmax)
	{
		*hueDegrees = (b - r) / delta + 2.f;
	}
	else
	{
		assert(b == cmax);
		*hueDegrees = (r - g) / delta + 4.f;
	}
	*hueDegrees *= 60.f;
	*saturation = cmax < tolerance ? 0.f : delta / cmax;
	*value = cmax;

	assert(InRange(*hueDegrees, 0.f, 360.f + tolerance));
	assert(InRange(*saturation, 0.f, 1.f + tolerance));
	assert(InRange(*value, 0.f, 1.f + tolerance));
}
vec3 HsvToRgb(const float hueDegrees, const float saturation, const float value)
{
	const float c = value*saturation;
	const float x = c*(1.f - fabs(fmodf(hueDegrees / 60.f, 2.f) - 1.f));
	const float m = value - c;
	vec3 rgbNewInterim;
	if (InRange(hueDegrees, 0.f, 60.f))
	{
		rgbNewInterim = vec3(c, x, 0.f);
	}
	else if (InRange(hueDegrees, 60.f, 120.f))
	{
		rgbNewInterim = vec3(x, c, 0.f);
	}
	else if (InRange(hueDegrees, 120.f, 180.f))
	{
		rgbNewInterim = vec3(0.f, c, x);
	}
	else if (InRange(hueDegrees, 180.f, 240.f))
	{
		rgbNewInterim = vec3(0.f, x, c);
	}
	else if (InRange(hueDegrees, 240.f, 300.f))
	{
		rgbNewInterim = vec3(x, 0.f, c);
	}
	else if (InRange(hueDegrees, 300.f, 360.f))
	{
		rgbNewInterim = vec3(c, 0.f, x);
	}
	else
	{
		assert(false);
	}
	const vec3 ret(rgbNewInterim.x + m,
		rgbNewInterim.y + m,
		rgbNewInterim.z + m);

	const float tolerance = .001f;
	assert(InRange(ret.x, 0.f, 1.f + tolerance));
	assert(InRange(ret.y, 0.f, 1.f + tolerance));
	assert(InRange(ret.z, 0.f, 1.f + tolerance));
	return ret;
}

float InfiniteImpulseResponseCalc(const float lastVal, const float target, const float responseFactor)
{
	assert(responseFactor >= 0.f && responseFactor <= 1.f);
	return lastVal + responseFactor*(target - lastVal);
}
float InfiniteImpulseResponseFactorCalc(const float factor, const float elapsedTime)
{
	return min(factor*elapsedTime, 1.f);
}
float Lerp(const float start, const float end, const float timeCurrentCountdownFromMax, const float timeMax)
{
	assert(timeCurrentCountdownFromMax >= 0.f && timeCurrentCountdownFromMax <= timeMax);
	return start + (end - start)*(timeMax - timeCurrentCountdownFromMax) / timeMax;
}

void InitializeLastSaturationAndValue(float*const lastSaturation, float*const lastValue, const vec3& defaultColor)
{
	float hueIgnored;
	RgbToHsv(&hueIgnored, lastSaturation, lastValue, defaultColor);
	*lastValue = SPATIAL_AUDIO_HSV_VALUE_DEACTIVATED;
}

void SetBoolsToFalse(bool*const firstElement, const int elementsNum)
{
	assert(elementsNum > 0);
	memset(firstElement, 0, sizeof(*firstElement)*elementsNum);
}
