/**
*
* \file Asura_Camera.h
*
* Created by Kevin Lea - 16/01/2002 16:02:51
*
* Copyright (C) 2001 Rebellion, All Rights Reserved.
*
* \brief
*
**/
#ifdef ASURA_USE_PRAGMA_ONCE
    #pragma once
#endif

#ifndef __ASURA_CAMERA_H__
#define __ASURA_CAMERA_H__

#include "Core/Asura_Maths.h"
#include "Debugging/Asura_Assert.h"
#include "Camera/Asura_Frustum_Planes.h"
#include "Network/Asura_Guid.h"

#define ASURA_MAX_NO_OF_CAMERAS 4

#if !defined(ASURA_CAMERA_DEFAULT_FAR_CLIP)
#define	ASURA_CAMERA_DEFAULT_FAR_CLIP	1000.0f
#endif	// !defined(ASURA_CAMERA_DEFAULT_FAR_CLIP)

#if !defined(ASURA_CAMERA_DEFAULT_NEAR_CLIP)
#define	ASURA_CAMERA_DEFAULT_NEAR_CLIP	0.250f
#endif	// !defined(ASURA_CAMERA_DEFAULT_NEAR_CLIP)


class Asura_ViewingArea
{
	public:
		/// Kevin Lea - 02/01/2002 17:36:07 - viewing area stuff
		/// In units of screen width or height as appropriate
		/// e.g. bottom quarter of screen would be all values set to 0.5f
		float m_fWidth;
		float m_fHeight;
		float m_fXOffset;
		float m_fYOffset;

		Asura_ViewingArea():m_fWidth(1.0f), m_fHeight(1.0f), m_fXOffset(0.0f), m_fYOffset(0.0f) {}

		Asura_ViewingArea(float _XOffset, float _YOffset, float _Width, float _Height)
		: m_fWidth  (_Width)
		, m_fHeight (_Height)
		, m_fXOffset(_XOffset)
		, m_fYOffset(_YOffset)
		{}

		float GetAspectRatio(void)    const 
		{ 
			Asura_Assert((m_fHeight > 0.0001f), "Height of a viewport is 0.0f");
				
			return m_fWidth / m_fHeight; 
		}

		float GetInvAspectRatio(void) const 
		{ 
			Asura_Assert((m_fWidth > 0.0001f), "Width of a viewport is 0.0f");
		
			return m_fHeight / m_fWidth; 
		}
};

class Asura_CameraData
{
	public:
		
		Asura_CameraData();
		void SetPosition(const Asura_Vector_3& xNewPos);
		void UpdateFrustumPlanes(void);

		Asura_Vector_3		m_xPosition;
		Asura_Matrix_3x3	m_xOrientation;
        Asura_Euler_3       m_xOriEuler;

		Asura_ViewingArea	m_xViewingArea;
		Asura_Frustum_Planes m_xFrustum;

		float				m_fFarClip;
		float				m_fNearClip;

		float				m_fXOverYAspectRatio;
		float				m_fYOverXAspectRatio;

		Asura_Angle			m_xHalfFieldOfViewX;
		float				m_fOneOverCosHalfFovX;
		float				m_fTanHalfFovX;
		float				m_fOneOverTanHalfFovX;

		Asura_Angle			m_xHalfFieldOfViewY;
		float				m_fOneOverCosHalfFovY;
		float				m_fTanHalfFovY;
		float				m_fOneOverTanHalfFovY;

		float               m_fLODMetric;

		Asura_Guid			m_uEntityToIgnore;
		bool				m_bIsActive;

#if defined ASURA_USE_HDR

		class HDRSettings
		{
		public:

			// No constructor here; would be a struct but SetToDefaults() is handy
			void SetToDefaults();

			// Defines which region on the screen is used for measuring current luminance
			Asura_Bounding_Rectangle m_xRegion;

			// Key value controls scene exposure
			float m_fKeyValue;
			float m_fAdaptationRate;

			// Min and max luminance allowed when adapting to light levels
			float m_fMinLuminance;
			float m_fMaxLuminance;

			// Source values with luminance above the white point cause white out
			float m_fWhitePoint;

			// Final result is scaled so that values below black point go black
			float m_fBlackPoint;

			// Fixed luminance used when dynamic exposure disabled
			float m_fFixedLuminance;
			bool m_bDynamicExposure;
		} m_xHDRSettings;

#endif // defined ASURA_USE_HDR

		void ResetNearFarClip( void )				{ Platform_InitialiseNearFarClip(); }

	private:
		// Platform-specific function to provide appropriate default values
		// for the near/far clip-planes wrt the hardware...
		void Platform_InitialiseNearFarClip (void);
};

class Asura_Camera
{
	public:
	
		static inline void SetCameraIndex( u_int uCamIdx = 0 )		
		{	
            Asura_Assert( uCamIdx < static_cast<u_int>(s_iNumberOfCameras), "" );

            if( uCamIdx == s_uCameraIndex ) { return; }

			s_pxCurrentCamera = &s_axCameraStorage[uCamIdx];
			s_uCameraIndex = uCamIdx;
			s_ucCameraIndexBitFlag = static_cast<u_char>(1 << uCamIdx);
			s_bCameraHasChanged = true;
		}

		static void SetToDefaults(void);
		
		static void SetPosition(const Asura_Vector_3& newPosition);
		static void SetOrientation(const Asura_Matrix_3x3& newOrientation);
        static void SetOrientation( const Asura_Euler_3& xNewOri );
		static void SetOrientation( const Asura_Quat& xNewOri );

		static void SetFarClip	(const float fDistance);
		static void SetNearClip	(const float fDistance);

		static void ResetNearFarClip( void )					{ s_pxCurrentCamera->ResetNearFarClip(); }
		
		// (MH - 30.05.2003) note that this used to require the physical screen
		// dimensions in the ratio parameter, but now the aspect ratio is normalised. 
		// So the old default parameter value of 4.0f/3.0f is equivalent to the new 
		// one of 1.0f for ordinary TVs / monitors. Cutscenes rendered in widescreen
		// should be able to set this to 16/9 or whatever, and viewports which are
		// non-square can similarly change it so that the Y-culling is right.
		static void SetHorizontalFieldOfView(Asura_Angle fHorizontalFOV, float fNormalisedXOverYAspectRatio = 1.0f);
		static void SetVerticalFieldOfView(Asura_Angle fVerticalFOV, float fNormalisedXOverYAspectRatio = 1.0f);

		static void SetFieldOfView(Asura_Angle fHorizontalFOV, float fNormalisedXOverYAspectRatio = 1.0f)	{ SetHorizontalFieldOfView (fHorizontalFOV, fNormalisedXOverYAspectRatio); }
		// (MP - 01.07.2004) Use this to let the camera system know about
        // your project's standard FOV, specified in radians. This will
        // ensure the LOD metric returned below is correct. The default is
        // 75º.
        static void SetStandardFOV( float fStandardFOV );
		
		// Viewing area restricts rendering to a section of the current render target.
		// (BW - 03.03.09) Note that this does not set Asura_Viewport (which represents the whole render target),
		// but it does call platform specific SetViewport functions (representing current renderable area).
		// This can cause confusion.
		static void SetViewingArea(float width=1.0f, float height=1.0f, float xoffset=0.0f, float yoffset=0.0f);
		static void SetViewingArea(const Asura_ViewingArea& area);
		static void SetEntityToIgnore( Asura_Guid uGUID = ASURA_GUID_SERVER );
		static void SetNumberOfCameras(int number);	
		static void SetCameraActiveStatus(bool isActive);	

        static inline const Asura_Vector_3* GetPosition( void ) { return &s_pxCurrentCamera->m_xPosition; }
        static inline const Asura_Matrix_3x3* GetOrientation( void ) { return &s_pxCurrentCamera->m_xOrientation; }
        static inline const Asura_Euler_3* GetOriEuler( void ) { return &s_pxCurrentCamera->m_xOriEuler; }
        static inline const Asura_ViewingArea* GetViewingArea(void) { return &s_pxCurrentCamera->m_xViewingArea; }

		static inline float	GetFarClip	(void)	{ return(s_pxCurrentCamera->m_fFarClip); }
		static inline float	GetNearClip	(void)	{ return(s_pxCurrentCamera->m_fNearClip); }

        static inline Asura_Angle GetHorizontalFieldOfView( void ) { return s_pxCurrentCamera->m_xHalfFieldOfViewX * 2.0f; }
		static inline Asura_Angle GetVerticalFieldOfView( void ) { return s_pxCurrentCamera->m_xHalfFieldOfViewY * 2.0f; }

        static inline float GetXOverYAspectRatio( void ) { return s_pxCurrentCamera->m_fXOverYAspectRatio; }
        static inline float GetYOverXAspectRatio( void ) { return s_pxCurrentCamera->m_fYOverXAspectRatio; }
		
        static inline float GetOneOverTanHalfFovX( void ) { return s_pxCurrentCamera->m_fOneOverTanHalfFovX; }
		static inline float GetOneOverTanHalfFovY( void ) { return s_pxCurrentCamera->m_fOneOverTanHalfFovY; }

        static inline float GetLODMetric( u_int uCameraIndex = s_uCameraIndex ) { return s_axCameraStorage[uCameraIndex].m_fLODMetric; }

        static inline const Asura_CameraData& GetCameraData( u_int uCamIdx ) { return s_axCameraStorage[uCamIdx]; }

		static inline Asura_Guid GetEntityToIgnore(void)
		{
			return s_pxCurrentCamera->m_uEntityToIgnore;
		}
		static inline int GetNumberOfCameras(void)
		{	
			return s_iNumberOfCameras;
		}
		
		static inline bool IsCameraActive(void)
		{
			return s_pxCurrentCamera->m_bIsActive;
		}

		static inline bool IsCameraActive(u_int uCameraIndex) { return s_axCameraStorage[uCameraIndex].m_bIsActive; }

		static inline unsigned char GetCameraIndexBitFlag(void)
		{
			return s_ucCameraIndexBitFlag;
		}

		// (MH - 16.04.2002) 
		static inline unsigned int GetCameraIndex(void)
		{
			return s_uCameraIndex;
		}

		static inline const Asura_Frustum_Planes* GetFrustumPlanes(void);

		static inline bool IsSphereInFrustum( const Asura_Vector_3& xCentre, const float fRadius );

		static inline bool IsAABBInFrustum( const Asura_Bounding_Box& xAABB );

		// KJFL 8 Jan 2008 - Hierarchical AABB-frustum test
		//
		// You pass in a bounding box and a bitfield indicating which planes to use (as we have 5 frustum planes (no far plane)
		// this defaults to 31 or binary 11111). Like the standard function it returns false if the AABB is completely outside
		// the frustum, and true otherwise. However, it also sets the uOutClipMask bitfield to indicate which planes (if any)
		// the AABB straddled. 
		//
		// So, if the AABB is completely inside the frustum, the output clip mask would be zero. If we were doing a hierarchical 
		// test at this point we'd know any AABBs inside the one we've just tested are also guaranteed to be inside the frustum,
		// and so no more checks are needed.
		//
		// Similiarly, if we test an AABB and it's inside the frustum but straddles the left plane, only one of the uOutClipMask
		// bits will be set. The cunning part is that for any future calls we can use the output bitfield as the input for uInClipMask, 
		// and the routine will *only* check those planes. Logically if a parent AABB only straddles the left plane, any children
		// boxes which are inside it can only be inside/outside/straddle the left plane - so there's no reason to check other planes.
		static inline bool IsAABBInFrustum( const Asura_Bounding_Box& xAABB, u_int& uOutClipMask, u_int uInClipMask = 31 );

		// (RMS 7.8.2002) Access to the forward, right and up vectors of the camera
		// with optional "Ignore" flag which forces the UP vector to be Vertical
		static inline void GetForwardVector( Asura_Vector_3& xFwdVec, bool bIgnoreUp = false )
		{
			if( bIgnoreUp )
			{
				xFwdVec.x = s_pxCurrentCamera->m_xOrientation.e13;
				xFwdVec.y = 0.0f;
				xFwdVec.z = s_pxCurrentCamera->m_xOrientation.e33;

				xFwdVec.Normalise();

				Asura_Assert( false == xFwdVec.ApproximatelyEquals( Asura_Vector_3( 0.0f, 0.0f, 0.0f ) ), "(RMS) Error - Camera was looking straight up!" );
			}
			else
			{
				xFwdVec.x = s_pxCurrentCamera->m_xOrientation.e13;
				xFwdVec.y = s_pxCurrentCamera->m_xOrientation.e23;
				xFwdVec.z = s_pxCurrentCamera->m_xOrientation.e33;
			}
		}


		static inline void GetRightVector( Asura_Vector_3& xRightVec, bool bIgnoreUp = false )
		{
			if( bIgnoreUp )
			{
				xRightVec.x = s_pxCurrentCamera->m_xOrientation.e11;
				xRightVec.y = 0.0f;
				xRightVec.z = s_pxCurrentCamera->m_xOrientation.e31;

				xRightVec.Normalise();

				Asura_Assert( false == xRightVec.ApproximatelyEquals( Asura_Vector_3( 0.0f, 0.0f, 0.0f ) ), "(RMS) Error - Camera right vector was straight up!" );
			}
			else
			{
				xRightVec.x = s_pxCurrentCamera->m_xOrientation.e11;
				xRightVec.y = s_pxCurrentCamera->m_xOrientation.e21;
				xRightVec.z = s_pxCurrentCamera->m_xOrientation.e31;
			}
		}

		static inline void GetUpVector( Asura_Vector_3& xUpVec )
		{
			xUpVec.x = s_pxCurrentCamera->m_xOrientation.e12;
			xUpVec.y = s_pxCurrentCamera->m_xOrientation.e22;
			xUpVec.z = s_pxCurrentCamera->m_xOrientation.e32;
		}

		/// Kevin Lea - 07/11/2002 16:16:13 - World space to screen space
		//
		//	Basically you pass it a world space position in the xPosition param, which on return is set
		//  to screen-space coords. If bInPixels is false (the default) then the returned values are
		//  relative to the current camera's viewport:
		//
		//	xPosition.x is nominally set to 0.0f to 1.0f, relative to the camera's viewport
		//	xPosition.y is nominally set to 0.0f to 1.0f, relative to the camera's viewport
		//	xPosition.z is the distance (in m) from the camera. +ve indicates in front of the camera, -ve behind.
		//	If xPosition.z is 0, both x & y are set to 0.5f.
		//	NOTE: x & y can be outside the range 0.0f to 1.0f, if the position passed is not on-screen.
		//	I've purposely not clamped the result.
		//	
		//	If bInPixels is set to true, the x & y coords returned are in pixels, relative to the whole
		//  display. So, in four player splitscreen, something directly ahead of the bottom-right camera
		//  will return relative coords of (0.5f,0.5f) and pixel values of (0.75f*ScreenWidth,0.75f*ScreenHeight).
        //
        // (MP - 17.02.2006) Now returns true iff project position falls
        // within the screen bounds.
		static bool ProjectWorldCoordsToScreenSpace(Asura_Vector_3& xPosition, const bool bInPixels = false);

		// (MH - 30.05.2003) physical screen aspect ratio - to allow 4:3 or 16:9
		static void SetPhysicalScreenAspectRatio(float fXOverYAspectRatio);
		static float GetPhysicalScreenAspectRatio() { return s_fPhysicalScreenXOverYAspectRatio; }
		static bool IsWidescreen() { return s_bIsWidescreen; }
		static bool HasCameraChanged() { return s_bCameraHasChanged; }
		static void ViewingAreaSet() { s_bCameraHasChanged = false; }

        static float FindAdjustedMinDistSqdFromCam( const Asura_Vector_3& xPos, u_int uCamVisFlags = (1 << ASURA_MAX_NO_OF_CAMERAS) - 1 );

		static void GetViewMatrix( Asura_AMatrix4x4& xViewMatrix ); //Catalin Arsenescu 30 August 2007

#if defined ASURA_USE_HDR

		static void GetHDRSettings( Asura_CameraData::HDRSettings& xSettings ) { xSettings = s_pxCurrentCamera->m_xHDRSettings; }
		static void SetHDRSettings( const Asura_CameraData::HDRSettings& xSettings ) { s_pxCurrentCamera->m_xHDRSettings = xSettings; }

#endif // defined ASURA_USE_HDR

	private:
		static Asura_CameraData		s_axCameraStorage[ASURA_MAX_NO_OF_CAMERAS];
		static Asura_CameraData*	s_pxCurrentCamera;
		static unsigned char		s_ucCameraIndexBitFlag;
		static unsigned int			s_uCameraIndex;	// (MH - 16.04.2002) 
		static int					s_iNumberOfCameras;
		static float				s_fPhysicalScreenXOverYAspectRatio;	// e.g. 4:3, 16:9
		static bool					s_bIsWidescreen;
		static bool					s_bCameraHasChanged;
        static float                s_fOneOverTanHalfStandardFOV;
};

/*
	'Asura_Camera' bigger inlines.
*/

inline const Asura_Frustum_Planes* Asura_Camera::GetFrustumPlanes(void)
{
	return &(s_pxCurrentCamera->m_xFrustum);
}


bool Asura_Camera::IsSphereInFrustum( const Asura_Vector_3& xCentre, const float fRadius )
{	
	const Asura_Vector_3 xViewSpacePos = GetOrientation()->ApplyTranspose( xCentre-(*GetPosition()) );

	// is the sphere intersecting or in front of the view frustum?
	if ( xViewSpacePos.z + fRadius >= 0.0f )
	{
		const float fR = fRadius * s_pxCurrentCamera->m_fOneOverCosHalfFovX;
		const float fZ = xViewSpacePos.z * s_pxCurrentCamera->m_fTanHalfFovX;

		// what about the left and right planes?
		if ( Asura_Maths::Abs(xViewSpacePos.x) <= fR + fZ )
		{
			const float fZ2 = fZ * s_pxCurrentCamera->m_fYOverXAspectRatio;

			const float fR2 = fRadius * s_pxCurrentCamera->m_fOneOverCosHalfFovY;

			// and finally the top and bottom planes?
			if ( Asura_Maths::Abs(xViewSpacePos.y) <= fR2 + fZ2 )
			{
				return true;
			}
		}
	}

	return false;
}

bool Asura_Camera::IsAABBInFrustum( const Asura_Bounding_Box& xAABB, u_int& uOutClipMask, u_int uInClipMask )
{
	const Asura_Frustum_Planes* pxPlanes = GetFrustumPlanes();
	const Asura_Vector_3 xCentre = xAABB.CalculateCentre();
	const Asura_Vector_3 xHalfDiagonal = xAABB.GetMax() - xCentre;

    uOutClipMask = 0;

	// Make sure out clip mask is valid
	uInClipMask &= 31;

	u_int uPlaneBit = 1;
	u_int uPlaneIndex = 0;

	while ( uPlaneBit <= uInClipMask )
	{
		if ( uPlaneBit & uInClipMask )
		{	
			const float fCentreProjection = pxPlanes->GetPlaneConstant(uPlaneIndex) - xCentre * pxPlanes->GetPlaneNormal(uPlaneIndex);

			const float fDiagonalProjection = xHalfDiagonal * pxPlanes->GetAbsPlaneNormal(uPlaneIndex);

			if ( fCentreProjection + fDiagonalProjection < 0.0f )
			{
				// We're definitely outside the frustum. Reset the clip mask so we have
				// similar behaviour to when we're completely inside.
				uOutClipMask = 0;
				return false;
			}

			// do we straddle this plane?
			if ( fCentreProjection - fDiagonalProjection < 0.0f ) uOutClipMask |= uPlaneBit;
		}

		uPlaneBit += uPlaneBit; // TODO - is a shift faster than an add?
		uPlaneIndex++;
	}

	return true;
}


bool Asura_Camera::IsAABBInFrustum( const Asura_Bounding_Box& xAABB )
{
	const Asura_Frustum_Planes* pxPlanes = GetFrustumPlanes();
	const Asura_Vector_3 xCentre = xAABB.CalculateCentre();
	const Asura_Vector_3 xHalfDiagonal = xAABB.GetMax() - xCentre;

	const u_int uNumberOfPlanes = 5;
	for( u_int uPlaneIndex = 0; uPlaneIndex < uNumberOfPlanes; ++uPlaneIndex )
	{
		const float fCentreProjection = pxPlanes->GetPlaneConstant(uPlaneIndex) - xCentre * pxPlanes->GetPlaneNormal(uPlaneIndex);

		const float fDiagonalProjection = xHalfDiagonal * pxPlanes->GetAbsPlaneNormal(uPlaneIndex);

		if ( fCentreProjection + fDiagonalProjection < 0.0f ) return false;
	}

	return true;
}



#endif//__ASURA_CAMERA_H__
