//
// Asura_Camera.cpp
//
// Created by Kevin Lea - 05/10/2001 15:27:14
// 
// Copyright (C) 2001 Rebellion, All Rights Reserved.
//

#include "Core/Asura.h"

#include "Camera/Asura_Camera.h"

#include "Debugging/Asura_Assert.h"
#include "Rendering/Asura_Viewport.h"
#include "Rendering/Asura_Render.h"


#include "Debugging/Asura_CPUProfile.h"


DEBUGVAR float dbg_fDefaultFOV = 0.375f * Asura_Maths::Pi;

//////////////////////////////////////////////
//           ASURA_CAMERADATA               //
//////////////////////////////////////////////

Asura_CameraData::Asura_CameraData()
{
			m_uEntityToIgnore = ASURA_GUID_SERVER;
			m_bIsActive = true;

			m_xPosition.Zero();
			m_xOrientation.Identity();
			m_xOriEuler.Zero();

			Platform_InitialiseNearFarClip();
			
			// (AMM 20 Nov 08)
			//
			// PS2 Platform initialise needs to have been called before this function in order to load the VU0
			// maths library that UpdateFrustumPlanes requires. This is because there are static objects that 
			// use this ctor, which resulted in the function getting called before the VU0 maths lib was loaded
			// causing a hang. This function does not seem to be required at this stage anyway as cameras are
			// reset on platform and state initialise.
			 
			//UpdateFrustumPlanes();
}

void Asura_CameraData::SetPosition(const Asura_Vector_3& xNewPos)
{
	m_xPosition = xNewPos;
	m_xFrustum.PositionChanged(xNewPos);
}


void Asura_CameraData::UpdateFrustumPlanes()
{
	Asura_Vector_3 xCameraUp(m_xOrientation.e12,m_xOrientation.e22,m_xOrientation.e32);
	Asura_Vector_3 xCameraRight(m_xOrientation.e11,m_xOrientation.e21,m_xOrientation.e31);
	Asura_Vector_3 xCameraForward(m_xOrientation.e13,m_xOrientation.e23,m_xOrientation.e33);
	Asura_Quat xRotation;
	Asura_Vector_3 xPlane;

	// Near plane of frustum
	m_xFrustum.SetPlaneNormal(0,-xCameraForward);

	// Left plane of frustum 
	{	
		xRotation.FromRotation( -m_xHalfFieldOfViewX, xCameraUp );
		xPlane = xRotation.RotateVector( xCameraForward);
		m_xFrustum.SetPlaneNormal(1, xPlane ^ xCameraUp);
	}

	// Right plane of frustum 
	{
		xRotation.FromRotation( m_xHalfFieldOfViewX, xCameraUp );
		xPlane = xRotation.RotateVector( xCameraForward);
		m_xFrustum.SetPlaneNormal(2, xPlane ^ -xCameraUp);
	}

	// Bottom plane of frustum 
	{
		xRotation.FromRotation( -m_xHalfFieldOfViewY, xCameraRight );
		xPlane = xRotation.RotateVector( xCameraForward);
		m_xFrustum.SetPlaneNormal(3, xPlane ^ xCameraRight);
	}

	// Top plane of frustum 
	{
		xRotation.FromRotation( m_xHalfFieldOfViewY, xCameraRight );
		xPlane = xRotation.RotateVector( xCameraForward);
		m_xFrustum.SetPlaneNormal(4, xPlane ^ -xCameraRight);
	}

	m_xFrustum.PositionChanged(m_xPosition);
	// now generate the abs versions of the normals
	for (u_int u=0; u<5; ++u)
	{
		m_xFrustum.DoAbsPlaneNormal(u);
	}
}

//////////////////////////////////////////////
//               ASURA_CAMERA               //
//////////////////////////////////////////////

Asura_CameraData	Asura_Camera::s_axCameraStorage[ASURA_MAX_NO_OF_CAMERAS];
Asura_CameraData*	Asura_Camera::s_pxCurrentCamera = Asura_Camera::s_axCameraStorage;
int					Asura_Camera::s_iNumberOfCameras = 1;
unsigned int 		Asura_Camera::s_uCameraIndex = 0xffff;
unsigned char		Asura_Camera::s_ucCameraIndexBitFlag = 0;
float				Asura_Camera::s_fPhysicalScreenXOverYAspectRatio = 4.0f / 3.0f;
bool				Asura_Camera::s_bIsWidescreen = false;
bool				Asura_Camera::s_bCameraHasChanged = true;
float               Asura_Camera::s_fOneOverTanHalfStandardFOV = 1.30322537284f; // Default standard FOV is 75º.

#if defined ASURA_USE_HDR

void Asura_CameraData::HDRSettings::SetToDefaults()
{
	// Initialise the measurement region to a large square in the centre of the screen
	m_xRegion.Set( -0.9f, -0.9f, 0.9f, 0.9f );

	m_fKeyValue = 0.18f;
	m_fAdaptationRate = 0.5f;
	m_fMinLuminance = 0.2f;
	m_fMaxLuminance = 10.0f;
	m_fWhitePoint = 3.0f;
	m_fBlackPoint = 0.0f;

	m_fFixedLuminance = 0.18f;
	m_bDynamicExposure = true;
}

#endif // defined ASURA_USE_HDR

void Asura_Camera::SetPhysicalScreenAspectRatio(float fXOverYAspectRatio)
{ 
	s_fPhysicalScreenXOverYAspectRatio = fXOverYAspectRatio;
	s_bIsWidescreen = (16.0f/9.0f == fXOverYAspectRatio);
}

void Asura_Camera::SetPosition(const Asura_Vector_3& newPosition)
{
	s_bCameraHasChanged = true;
	s_pxCurrentCamera->SetPosition(newPosition);
}

void Asura_Camera::SetOrientation(const Asura_Matrix_3x3& newOrientation)
{
	s_bCameraHasChanged = true;
	s_pxCurrentCamera->m_xOrientation = newOrientation;
	s_pxCurrentCamera->m_xOriEuler.FromMatrix( s_pxCurrentCamera->m_xOrientation );
		//Update Frustum planes
	s_pxCurrentCamera->UpdateFrustumPlanes();
}

void Asura_Camera::SetOrientation(const Asura_Quat& xNewOri)
{
	s_bCameraHasChanged = true;
	xNewOri.ToMatrix(s_pxCurrentCamera->m_xOrientation);
	xNewOri.ToEuler(s_pxCurrentCamera->m_xOriEuler);
		//Update Frustum planes
	s_pxCurrentCamera->UpdateFrustumPlanes();
}

void Asura_Camera::SetOrientation( const Asura_Euler_3& xNewOri )
{
	s_bCameraHasChanged = true;
	s_pxCurrentCamera->m_xOriEuler = xNewOri;
	s_pxCurrentCamera->m_xOriEuler.ToMatrix( s_pxCurrentCamera->m_xOrientation );
		//Update Frustum planes
	s_pxCurrentCamera->UpdateFrustumPlanes();
}

void Asura_Camera::SetFarClip (const float fDistance)
{
	s_bCameraHasChanged	= true;
	s_pxCurrentCamera->m_fFarClip	= fDistance;
}

void Asura_Camera::SetNearClip (const float fDistance)
{
	s_bCameraHasChanged	= true;
	s_pxCurrentCamera->m_fNearClip	= fDistance;
}

void Asura_Camera::SetHorizontalFieldOfView(Asura_Angle fHorizontalFOV, float fNormalisedXOverYAspectRatio)
{
	Asura_Assert(fHorizontalFOV > 0.f && fNormalisedXOverYAspectRatio > 0.f, "Bad values for field of view" );

	const float	fXOverYAspectRatio	= fNormalisedXOverYAspectRatio * s_fPhysicalScreenXOverYAspectRatio;

	// Mark the camera as changed...
	s_bCameraHasChanged	= true;

	// Calculate the values needed...
	s_pxCurrentCamera->m_fXOverYAspectRatio	= fXOverYAspectRatio;
	s_pxCurrentCamera->m_fYOverXAspectRatio	= 1.0f / fXOverYAspectRatio;

	// Calculate the horizontal values...
	s_pxCurrentCamera->m_xHalfFieldOfViewX		= fHorizontalFOV * 0.5f;
	s_pxCurrentCamera->m_fTanHalfFovX			= Asura_Maths::Tangent(s_pxCurrentCamera->m_xHalfFieldOfViewX);
	s_pxCurrentCamera->m_fOneOverTanHalfFovX	= 1.0f / s_pxCurrentCamera->m_fTanHalfFovX;
	s_pxCurrentCamera->m_fOneOverCosHalfFovX	= 1.0f / Asura_Maths::Cosine(s_pxCurrentCamera->m_xHalfFieldOfViewX);

	// Calculate the vertical values...
	s_pxCurrentCamera->m_xHalfFieldOfViewY		= Asura_Maths::ArcTangent(s_pxCurrentCamera->m_fTanHalfFovX, s_pxCurrentCamera->m_fXOverYAspectRatio);
	s_pxCurrentCamera->m_fTanHalfFovY			= Asura_Maths::Tangent(s_pxCurrentCamera->m_xHalfFieldOfViewY);
	s_pxCurrentCamera->m_fOneOverTanHalfFovY	= 1.0f / s_pxCurrentCamera->m_fTanHalfFovY;
	s_pxCurrentCamera->m_fOneOverCosHalfFovY	= 1.0f / Asura_Maths::Cosine(s_pxCurrentCamera->m_xHalfFieldOfViewY);

	// Calculate the LOD value...
	s_pxCurrentCamera->m_fLODMetric			= s_fOneOverTanHalfStandardFOV / s_pxCurrentCamera->m_fOneOverTanHalfFovX;

	//Update Frustum planes
	s_pxCurrentCamera->UpdateFrustumPlanes();
}

void Asura_Camera::SetVerticalFieldOfView(Asura_Angle fVerticalFOV, float fNormalisedXOverYAspectRatio)
{
	const float	fXOverYAspectRatio	= fNormalisedXOverYAspectRatio * s_fPhysicalScreenXOverYAspectRatio;

	// Mark the camera as changed...
	s_bCameraHasChanged	= true;

	// Calculate the values needed...
	s_pxCurrentCamera->m_fXOverYAspectRatio	= fXOverYAspectRatio;
	s_pxCurrentCamera->m_fYOverXAspectRatio	= 1.0f / fXOverYAspectRatio;

	// Calculate the vertical values...
	s_pxCurrentCamera->m_xHalfFieldOfViewY		= fVerticalFOV * 0.5f;
	s_pxCurrentCamera->m_fTanHalfFovY			= Asura_Maths::Tangent(s_pxCurrentCamera->m_xHalfFieldOfViewY);
	s_pxCurrentCamera->m_fOneOverTanHalfFovY	= 1.0f / s_pxCurrentCamera->m_fTanHalfFovY;
	s_pxCurrentCamera->m_fOneOverCosHalfFovY	= 1.0f / Asura_Maths::Cosine(s_pxCurrentCamera->m_xHalfFieldOfViewY);

	// Calculate the horizontal values...
	s_pxCurrentCamera->m_xHalfFieldOfViewX		= Asura_Maths::ArcTangent(s_pxCurrentCamera->m_fTanHalfFovY, s_pxCurrentCamera->m_fYOverXAspectRatio);
	s_pxCurrentCamera->m_fTanHalfFovX			= Asura_Maths::Tangent(s_pxCurrentCamera->m_xHalfFieldOfViewX);
	s_pxCurrentCamera->m_fOneOverTanHalfFovX	= 1.0f / s_pxCurrentCamera->m_fTanHalfFovX;
	s_pxCurrentCamera->m_fOneOverCosHalfFovX	= 1.0f / Asura_Maths::Cosine(s_pxCurrentCamera->m_xHalfFieldOfViewX);

	// Calculate the LOD value...
	s_pxCurrentCamera->m_fLODMetric			= s_fOneOverTanHalfStandardFOV / s_pxCurrentCamera->m_fOneOverTanHalfFovX;

	//Update Frustum planes
	s_pxCurrentCamera->UpdateFrustumPlanes();
}

void Asura_Camera::SetStandardFOV( float fStdFOV )
{
	s_fOneOverTanHalfStandardFOV = 1.0f / Asura_Maths::Tangent(0.5f * fStdFOV);
}

void Asura_Camera::SetViewingArea(float width, float height, float xoffset, float yoffset)
{
	s_bCameraHasChanged = true;
	s_pxCurrentCamera->m_xViewingArea.m_fWidth = width;
	s_pxCurrentCamera->m_xViewingArea.m_fHeight = height;
	s_pxCurrentCamera->m_xViewingArea.m_fXOffset = xoffset;
	s_pxCurrentCamera->m_xViewingArea.m_fYOffset = yoffset;
}
void Asura_Camera::SetViewingArea(const Asura_ViewingArea& area)
{
	s_bCameraHasChanged = true;
	s_pxCurrentCamera->m_xViewingArea = area;
}
void Asura_Camera::SetToDefaults(void)
{
	s_bCameraHasChanged = true;
	s_pxCurrentCamera->m_xViewingArea.m_fWidth = s_pxCurrentCamera->m_xViewingArea.m_fHeight = 1.0f;
	s_pxCurrentCamera->m_xViewingArea.m_fXOffset = s_pxCurrentCamera->m_xViewingArea.m_fYOffset = 0.0f;

	SetVerticalFieldOfView( dbg_fDefaultFOV );

	/// Kevin Lea - 14/03/2002 14:25:52 - set to an 'impossible' entity
	/// i.e. don't use ASURA_GUID_UNREGISTERED, since that may result
	/// it cutscenes etc not being drawn
	s_pxCurrentCamera->m_uEntityToIgnore = ASURA_GUID_SERVER;

	#if defined ASURA_USE_HDR
		s_pxCurrentCamera->m_xHDRSettings.SetToDefaults();
	#endif

	//Update Frustum planes
	s_pxCurrentCamera->UpdateFrustumPlanes();
}
void Asura_Camera::SetEntityToIgnore(Asura_Guid guid)
{
	Asura_Assert(guid != ASURA_GUID_UNREGISTERED, "By setting the entity to ignore to ASURA_GUID_UNREGISTERED, you will be preventing lots of guid-free things from drawing, e.g. Cutscene actors. Was this your intention?");
	s_pxCurrentCamera->m_uEntityToIgnore = guid;
}
void Asura_Camera::SetNumberOfCameras(int number)
{
	Asura_Assert(number>0 && number<=ASURA_MAX_NO_OF_CAMERAS,"camera count out of range");
	s_iNumberOfCameras = number;
}
void Asura_Camera::SetCameraActiveStatus(bool isActive)
{
	s_pxCurrentCamera->m_bIsActive = isActive;
}

bool Asura_Camera::ProjectWorldCoordsToScreenSpace(Asura_Vector_3& xPosition, const bool bInPixels)
{
	xPosition = s_pxCurrentCamera->m_xOrientation.ApplyTranspose(xPosition - s_pxCurrentCamera->m_xPosition);

	if (xPosition.z != 0.0f)
	{
		float fOneOverZ = 0.5f/xPosition.z;

		if (Asura_Render::ShouldRenderMirrored())
		{
			xPosition.x = -xPosition.x;
		}

		xPosition.x = 0.5f + (xPosition.x * s_pxCurrentCamera->m_fOneOverTanHalfFovX) * fOneOverZ;
		xPosition.y = 0.5f + (xPosition.y * s_pxCurrentCamera->m_fOneOverTanHalfFovX*s_pxCurrentCamera->m_fXOverYAspectRatio) * fOneOverZ;
	}
	else
	{
		xPosition.x = 0.5f;
		xPosition.y = 0.5f;
	}

	const bool bOnScreen = ( xPosition.x >= 0.0f && xPosition.x <= 1.0f
		&&
		xPosition.y >= 0.0f && xPosition.y <= 1.0f );

	if (bInPixels)
	{
		const Asura_Vector_2&	xSubPixelOffset	= Asura_Render::GetSubPixelOffset ();

		xPosition.x	 = ((xPosition.x*s_pxCurrentCamera->m_xViewingArea.m_fWidth)+s_pxCurrentCamera->m_xViewingArea.m_fXOffset);
		xPosition.x	*= Asura_Viewport::GetWidth();
		xPosition.x	+= xSubPixelOffset.x;

		xPosition.y	 = ((xPosition.y*s_pxCurrentCamera->m_xViewingArea.m_fHeight)+s_pxCurrentCamera->m_xViewingArea.m_fYOffset);
		xPosition.y	*= Asura_Viewport::GetHeight();
		xPosition.y	+= xSubPixelOffset.y;
	}

	return bOnScreen;
}

float Asura_Camera::FindAdjustedMinDistSqdFromCam( const Asura_Vector_3& xPos, u_int uCamVisFlags )
{
	float fMinSqdDist = Asura_Maths::LargeFloat;

	u_int uCamMask = 1 << 0;

	const u_int uNumCams = s_iNumberOfCameras;
	for( u_int uCam = 0; uCam < uNumCams; ++uCam, uCamMask <<= 1 )
	{
		if( !(uCamVisFlags & uCamMask) ) { continue; }

		const Asura_CameraData& xCamData = s_axCameraStorage[uCam];
		Asura_Assert( xCamData.m_bIsActive, "" );

		float fDistSqd = (xPos - xCamData.m_xPosition).MagnitudeSquared();

		// Account for FOV.
		fDistSqd *= xCamData.m_fLODMetric * xCamData.m_fLODMetric;

		fMinSqdDist = Asura_Maths::Min( fDistSqd, fMinSqdDist );
	}

	return fMinSqdDist;
}

void Asura_Camera::GetViewMatrix( Asura_AMatrix4x4& xViewMatrix )
{
	const Asura_Matrix_3x3& xOrient	= s_pxCurrentCamera->m_xOrientation;
	const Asura_Vector_3& xPos		= s_pxCurrentCamera->m_xPosition;

	xViewMatrix.SetColumn0(Asura_AVector4(  xOrient.Element[ 0 ][ 0 ],  xOrient.Element[ 1 ][ 0 ],  xOrient.Element[ 2 ][ 0 ], 0.0f ) );
	xViewMatrix.SetColumn1(Asura_AVector4(  xOrient.Element[ 0 ][ 1 ],  xOrient.Element[ 1 ][ 1 ],  xOrient.Element[ 2 ][ 1 ], 0.0f ) );
	xViewMatrix.SetColumn2(Asura_AVector4(  xOrient.Element[ 0 ][ 2 ],  xOrient.Element[ 1 ][ 2 ],  xOrient.Element[ 2 ][ 2 ], 0.0f ) );
	xViewMatrix.SetColumn3(Asura_AVector4(  xPos.GetX(),				   xPos.GetY(),				   xPos.GetZ(),				  1.0f ) );
}

//#if PG_DEBUG_CULLING
//
//#if 0
//	/* Add these bits to 'Asura_Camera.h' */
//	#if PG_DEBUG_AABB
//		static bool IsSphereInFrustumReal
//			(const Asura_Vector_3& xCentre,const float fRadius);
//		static bool IsSphereInFrustumExt
//			(const Asura_Vector_3& xCentre,const float fRadius);
//		static bool IsAABBInFrustumExt(const Asura_Bounding_Box& xBox);
//	#endif
//
//	#if PG_DEBUG_AABB
//	bool Asura_Camera::IsSphereInFrustum
//		(const Asura_Vector_3& xCentre, const float fRadius)
//	{
//		return Asura_Camera::IsSphereInFrustumExt(xCentre,fRadius);
//	}
//	#else
//	#endif
//
//	#if PG_DEBUG_AABB
//	bool Asura_Camera::IsAABBInFrustum(register const Asura_Bounding_Box& xB)
//	{
//		return Asura_Camera::IsAABBInFrustumExt(xB);
//	}
//	#else
//	#endif
//#endif
//
//bool Asura_Camera::IsSphereInFrustumReal(
//	const Asura_Vector_3& xCentre,const float fRadius
//)
//{			
//	const Asura_Matrix_3x3& xOrientation = *GetOrientation();
//	const Asura_Vector_3 xViewSpacePos
//		= xOrientation.ApplyTranspose(xCentre-(*GetPosition()));
//
//	// is the sphere intersecting or in front of the view frustum?
//	if (xViewSpacePos.z+fRadius>=0)
//	{
//		float fR = fRadius * s_pxCurrentCamera->m_fOneOverCosHalfFovX;
//		float fZ = xViewSpacePos.z * s_pxCurrentCamera->m_fTanHalfFovX;
//
//		// what about the left and right planes?
//		if ((xViewSpacePos.x-fZ)<=fR && (-xViewSpacePos.x-fZ)<=fR)
//		{
//			fZ *= s_pxCurrentCamera->m_fYOverXAspectRatio;
//			fR = fRadius * s_pxCurrentCamera->m_fOneOverCosHalfFovY;
//
//			// and finally the top and bottom planes?
//			if ((xViewSpacePos.y-fZ)<=fR && (-xViewSpacePos.y-fZ)<=fR)
//			{
//				return true;
//			}
//		}
//	}
//
//	return false;
//}
//
//bool Asura_Camera::IsSphereInFrustumExt(
//	const Asura_Vector_3& xCentre,const float fRadius
//)
//{			
//	Asura_Bounding_Box xB;
//	xB.MinX = xCentre.x - fRadius;
//	xB.MinY = xCentre.y - fRadius;
//	xB.MinZ = xCentre.z - fRadius;
//
//	xB.MaxX = xCentre.x + fRadius;
//	xB.MaxY = xCentre.y + fRadius;
//	xB.MaxZ = xCentre.z + fRadius;
//
//#if 1
//	/* slightly deform the AABB into a noncubical shape */
//	xB.MaxX += fRadius*0.05f;
//	xB.MinY -= fRadius*0.05f;
//	xB.MinZ += fRadius*0.05f;
//	xB.MaxZ -= fRadius*0.05f;
//#endif
//
//	return Asura_Camera::IsAABBInFrustumExt(xB);
//}
//
//bool Asura_Camera::IsAABBInFrustumExt(
//	register const Asura_Bounding_Box& xB
//)
//{
//	register const Asura_Vector_3 xCentre = xB.CalculateCentre();
//
//	/*
//		Should we test the inner or outer sphere first? The inner test
//		is cheaper, but we expect most AABBs to be well outside the
//		frustum (as in to the sides or behind the camera), and the outer
//		test rejects them quicker.
//	*/
//
//	register const float fOuterRadius = xB.CalculateRadius();
//	register const float fInnerRadius = 0.5f
//		* Asura_Maths::Min(xB.MaxX-xB.MinX,
//			Asura_Maths::Min(xB.MaxY-xB.MinY,xB.MaxZ-xB.MinZ));
//	const bool bInnerOverlaps
//		= Asura_Camera::IsSphereInFrustumReal(xCentre,fInnerRadius);
//	const bool bOuterOverlaps
//		= Asura_Camera::IsSphereInFrustumReal(xCentre,fOuterRadius);
//	const bool bAABBOutside
//		= Asura_Camera::GetFrustumPlanes()->OutsideAABB(xB);
//
//	if (bInnerOverlaps)
//	{
//		Asura_Assert(bOuterOverlaps,
//			"if inner sphere overlaps, outer must too");
//      Asura_Assert(!bAABBOutside,
//			"if inner sphere overlaps, AABB cannot be outside");
//	}
//	if (bAABBOutside)
//	{
//		Asura_Assert(!bInnerOverlaps,
//			"if AABB outside, inner sphere cannot overlap");
//	}
//
//#if 0
//	/*
//		These two tests are not right because the AABB test does return
//		false positives (not outside when the AABB is outside) in some
//		cases.  as reported at the end of section 2.3 of:
//			http://WWW.CESCG.org/CESCG-2002/DSykoraJJelinek/
//	*/
//	if (!bAABBOutside)
//	{
//		Asura_Assert(bOuterOverlaps,
//			"if AABB overlaps, outer sphere must too");
//	}
//	if (!bOuterOverlaps)
//	{
//		Asura_Assert(bAABBOutside,
//			"if outer sphere does not overlap, AABB must be outside");
//	}
//#endif
//
//	static bool bTest = true;
//
//	if (bTest)
//	{
//		return !bAABBOutside;
//	}
//
//	return bInnerOverlaps || bOuterOverlaps;
//}
//#endif
