// Copyright (C) 2002-2012 Nikolaus Gebhardt
// This file is part of the "Irrlicht Engine".
// For conditions of distribution and use, see copyright notice in irrlicht.h

#ifndef __S_VIEW_FRUSTUM_H_INCLUDED__
#define __S_VIEW_FRUSTUM_H_INCLUDED__

#include "plane3d.h"
#include "vector3d.h"
#include "line3d.h"
#include "aabbox3d.h"
#include "matrix4.h"
#include "IVideoDriver.h"

namespace irr
{
namespace scene
{

	//! Defines the view frustum. That's the space visible by the camera.
	/** The view frustum is enclosed by 6 planes. These six planes share
	eight points. A bounding box around these eight points is also stored in
	this structure.
	*/
	struct SViewFrustum
	{
		enum VFPLANES
		{
			//! Far plane of the frustum. That is the plane furthest away from the eye.
			VF_FAR_PLANE = 0,
			//! Near plane of the frustum. That is the plane nearest to the eye.
			VF_NEAR_PLANE,
			//! Left plane of the frustum.
			VF_LEFT_PLANE,
			//! Right plane of the frustum.
			VF_RIGHT_PLANE,
			//! Bottom plane of the frustum.
			VF_BOTTOM_PLANE,
			//! Top plane of the frustum.
			VF_TOP_PLANE,

			//! Amount of planes enclosing the view frustum. Should be 6.
			VF_PLANE_COUNT
		};


		//! Default Constructor
		SViewFrustum() : BoundingRadius(0.f), FarNearDistance(0.f) {}

		//! Copy Constructor
		SViewFrustum(const SViewFrustum& other);

		//! This constructor creates a view frustum based on a projection and/or view matrix.
		//\param zClipFromZero: Clipping of z can be projected from 0 to w when true (D3D style) and from -w to w when false (OGL style).
		SViewFrustum(const core::matrix4& mat, bool zClipFromZero);

		//! This constructor creates a view frustum based on a projection and/or view matrix.
		//\param zClipFromZero: Clipping of z can be projected from 0 to w when true (D3D style) and from -w to w when false (OGL style).
		inline void setFrom(const core::matrix4& mat, bool zClipFromZero);

		//! transforms the frustum by the matrix
		/** \param mat: Matrix by which the view frustum is transformed.*/
		void transform(const core::matrix4& mat);

		//! returns the point which is on the far left upper corner inside the the view frustum.
		core::vector3df getFarLeftUp() const;

		//! returns the point which is on the far left bottom corner inside the the view frustum.
		core::vector3df getFarLeftDown() const;

		//! returns the point which is on the far right top corner inside the the view frustum.
		core::vector3df getFarRightUp() const;

		//! returns the point which is on the far right bottom corner inside the the view frustum.
		core::vector3df getFarRightDown() const;

		//! returns the point which is on the near left upper corner inside the the view frustum.
		core::vector3df getNearLeftUp() const;

		//! returns the point which is on the near left bottom corner inside the the view frustum.
		core::vector3df getNearLeftDown() const;

		//! returns the point which is on the near right top corner inside the the view frustum.
		core::vector3df getNearRightUp() const;

		//! returns the point which is on the near right bottom corner inside the the view frustum.
		core::vector3df getNearRightDown() const;

		//! returns a bounding box enclosing the whole view frustum
		const core::aabbox3d<f32> &getBoundingBox() const;

		//! recalculates the bounding box and sphere based on the planes
		inline void recalculateBoundingBox();

		//! get the bounding sphere's radius (of an optimized sphere, not the AABB's)
		float getBoundingRadius() const;

		//! get the bounding sphere's radius (of an optimized sphere, not the AABB's)
		core::vector3df getBoundingCenter() const;

		//! the cam should tell the frustum the distance between far and near
		void setFarNearDistance(float distance);

		//! get the given state's matrix based on frustum E_TRANSFORMATION_STATE
		core::matrix4& getTransform( video::E_TRANSFORMATION_STATE state);

		//! get the given state's matrix based on frustum E_TRANSFORMATION_STATE
		const core::matrix4& getTransform( video::E_TRANSFORMATION_STATE state) const;

		//! clips a line to the view frustum.
		/** \return True if the line was clipped, false if not */
		bool clipLine(core::line3d<f32>& line) const;

		//! the position of the camera
		core::vector3df cameraPosition;

		//! all planes enclosing the view frustum.
		core::plane3d<f32> planes[VF_PLANE_COUNT];

		//! bounding box around the view frustum
		core::aabbox3d<f32> boundingBox;

	private:
		//! Hold a copy of important transform matrices
		enum E_TRANSFORMATION_STATE_FRUSTUM
		{
			ETS_VIEW = 0,
			ETS_PROJECTION = 1,
			ETS_COUNT_FRUSTUM
		};

		//! recalculates the bounding sphere based on the planes
		inline void recalculateBoundingSphere();

		//! Hold a copy of important transform matrices
		core::matrix4 Matrices[ETS_COUNT_FRUSTUM];

		float BoundingRadius;
		float FarNearDistance;
		core::vector3df BoundingCenter;
	};


	/*!
		Copy constructor ViewFrustum
	*/
	inline SViewFrustum::SViewFrustum(const SViewFrustum& other)
	{
		cameraPosition=other.cameraPosition;
		boundingBox=other.boundingBox;

		u32 i;
		for (i=0; i<VF_PLANE_COUNT; ++i)
			planes[i]=other.planes[i];

		for (i=0; i<ETS_COUNT_FRUSTUM; ++i)
			Matrices[i]=other.Matrices[i];

		BoundingRadius = other.BoundingRadius;
		FarNearDistance = other.FarNearDistance;
		BoundingCenter = other.BoundingCenter;
	}

	inline SViewFrustum::SViewFrustum(const core::matrix4& mat, bool zClipFromZero)
	{
		setFrom(mat, zClipFromZero);
	}


	inline void SViewFrustum::transform(const core::matrix4& mat)
	{
		for (u32 i=0; i<VF_PLANE_COUNT; ++i)
			mat.transformPlane(planes[i]);

		mat.transformVect(cameraPosition);
		recalculateBoundingBox();
	}


	inline core::vector3df SViewFrustum::getFarLeftUp() const
	{
		core::vector3df p;
		planes[scene::SViewFrustum::VF_FAR_PLANE].getIntersectionWithPlanes(
			planes[scene::SViewFrustum::VF_TOP_PLANE],
			planes[scene::SViewFrustum::VF_LEFT_PLANE], p);

		return p;
	}

	inline core::vector3df SViewFrustum::getFarLeftDown() const
	{
		core::vector3df p;
		planes[scene::SViewFrustum::VF_FAR_PLANE].getIntersectionWithPlanes(
			planes[scene::SViewFrustum::VF_BOTTOM_PLANE],
			planes[scene::SViewFrustum::VF_LEFT_PLANE], p);

		return p;
	}

	inline core::vector3df SViewFrustum::getFarRightUp() const
	{
		core::vector3df p;
		planes[scene::SViewFrustum::VF_FAR_PLANE].getIntersectionWithPlanes(
			planes[scene::SViewFrustum::VF_TOP_PLANE],
			planes[scene::SViewFrustum::VF_RIGHT_PLANE], p);

		return p;
	}

	inline core::vector3df SViewFrustum::getFarRightDown() const
	{
		core::vector3df p;
		planes[scene::SViewFrustum::VF_FAR_PLANE].getIntersectionWithPlanes(
			planes[scene::SViewFrustum::VF_BOTTOM_PLANE],
			planes[scene::SViewFrustum::VF_RIGHT_PLANE], p);

		return p;
	}

	inline core::vector3df SViewFrustum::getNearLeftUp() const
	{
		core::vector3df p;
		planes[scene::SViewFrustum::VF_NEAR_PLANE].getIntersectionWithPlanes(
			planes[scene::SViewFrustum::VF_TOP_PLANE],
			planes[scene::SViewFrustum::VF_LEFT_PLANE], p);

		return p;
	}

	inline core::vector3df SViewFrustum::getNearLeftDown() const
	{
		core::vector3df p;
		planes[scene::SViewFrustum::VF_NEAR_PLANE].getIntersectionWithPlanes(
			planes[scene::SViewFrustum::VF_BOTTOM_PLANE],
			planes[scene::SViewFrustum::VF_LEFT_PLANE], p);

		return p;
	}

	inline core::vector3df SViewFrustum::getNearRightUp() const
	{
		core::vector3df p;
		planes[scene::SViewFrustum::VF_NEAR_PLANE].getIntersectionWithPlanes(
			planes[scene::SViewFrustum::VF_TOP_PLANE],
			planes[scene::SViewFrustum::VF_RIGHT_PLANE], p);

		return p;
	}

	inline core::vector3df SViewFrustum::getNearRightDown() const
	{
		core::vector3df p;
		planes[scene::SViewFrustum::VF_NEAR_PLANE].getIntersectionWithPlanes(
			planes[scene::SViewFrustum::VF_BOTTOM_PLANE],
			planes[scene::SViewFrustum::VF_RIGHT_PLANE], p);

		return p;
	}

	inline const core::aabbox3d<f32> &SViewFrustum::getBoundingBox() const
	{
		return boundingBox;
	}

	inline void SViewFrustum::recalculateBoundingBox()
	{
		boundingBox.reset(getNearLeftUp());
		boundingBox.addInternalPoint(getNearRightUp());
		boundingBox.addInternalPoint(getNearLeftDown());
		boundingBox.addInternalPoint(getNearRightDown());
		boundingBox.addInternalPoint(getFarRightUp());
		boundingBox.addInternalPoint(getFarLeftDown());
		boundingBox.addInternalPoint(getFarRightDown());
		boundingBox.addInternalPoint(getFarLeftUp());

		// Also recalculate the bounding sphere when the bbox changes
		recalculateBoundingSphere();
	}

	inline float SViewFrustum::getBoundingRadius() const
	{
		return BoundingRadius;
	}

	inline core::vector3df SViewFrustum::getBoundingCenter() const
	{
		return BoundingCenter;
	}

	inline void SViewFrustum::setFarNearDistance(float distance)
	{
		FarNearDistance = distance;
	}

	//! This constructor creates a view frustum based on a projection
	//! and/or view matrix.
	inline void SViewFrustum::setFrom(const core::matrix4& mat, bool zClipFromZero)
	{
		// left clipping plane
		planes[VF_LEFT_PLANE].Normal.X = mat[3 ] + mat[0];
		planes[VF_LEFT_PLANE].Normal.Y = mat[7 ] + mat[4];
		planes[VF_LEFT_PLANE].Normal.Z = mat[11] + mat[8];
		planes[VF_LEFT_PLANE].D =        mat[15] + mat[12];

		// right clipping plane
		planes[VF_RIGHT_PLANE].Normal.X = mat[3 ] - mat[0];
		planes[VF_RIGHT_PLANE].Normal.Y = mat[7 ] - mat[4];
		planes[VF_RIGHT_PLANE].Normal.Z = mat[11] - mat[8];
		planes[VF_RIGHT_PLANE].D =        mat[15] - mat[12];

		// top clipping plane
		planes[VF_TOP_PLANE].Normal.X = mat[3 ] - mat[1];
		planes[VF_TOP_PLANE].Normal.Y = mat[7 ] - mat[5];
		planes[VF_TOP_PLANE].Normal.Z = mat[11] - mat[9];
		planes[VF_TOP_PLANE].D =        mat[15] - mat[13];

		// bottom clipping plane
		planes[VF_BOTTOM_PLANE].Normal.X = mat[3 ] + mat[1];
		planes[VF_BOTTOM_PLANE].Normal.Y = mat[7 ] + mat[5];
		planes[VF_BOTTOM_PLANE].Normal.Z = mat[11] + mat[9];
		planes[VF_BOTTOM_PLANE].D =        mat[15] + mat[13];

		// far clipping plane
		planes[VF_FAR_PLANE].Normal.X = mat[3 ] - mat[2];
		planes[VF_FAR_PLANE].Normal.Y = mat[7 ] - mat[6];
		planes[VF_FAR_PLANE].Normal.Z = mat[11] - mat[10];
		planes[VF_FAR_PLANE].D =        mat[15] - mat[14];

		// near clipping plane
		if ( zClipFromZero )
		{
			planes[VF_NEAR_PLANE].Normal.X = mat[2];
			planes[VF_NEAR_PLANE].Normal.Y = mat[6];
			planes[VF_NEAR_PLANE].Normal.Z = mat[10];
			planes[VF_NEAR_PLANE].D =        mat[14];
		}
		else
		{
			// near clipping plane
			planes[VF_NEAR_PLANE].Normal.X = mat[3 ] + mat[2];
			planes[VF_NEAR_PLANE].Normal.Y = mat[7 ] + mat[6];
			planes[VF_NEAR_PLANE].Normal.Z = mat[11] + mat[10];
			planes[VF_NEAR_PLANE].D =        mat[15] + mat[14];
		}

		// normalize normals
		u32 i;
		for ( i=0; i != VF_PLANE_COUNT; ++i)
		{
			const f32 len = -core::reciprocal_squareroot(
					planes[i].Normal.getLengthSQ());
			planes[i].Normal *= len;
			planes[i].D *= len;
		}

		// make bounding box
		recalculateBoundingBox();
	}

	/*!
		View Frustum depends on Projection & View Matrix
	*/
	inline core::matrix4& SViewFrustum::getTransform(video::E_TRANSFORMATION_STATE state)
	{
		u32 index = 0;
		switch ( state )
		{
			case video::ETS_PROJECTION:
				index = SViewFrustum::ETS_PROJECTION; break;
			case video::ETS_VIEW:
				index = SViewFrustum::ETS_VIEW; break;
			default:
				break;
		}
		return Matrices [ index ];
	}

	/*!
		View Frustum depends on Projection & View Matrix
	*/
	inline const core::matrix4& SViewFrustum::getTransform(video::E_TRANSFORMATION_STATE state) const
	{
		u32 index = 0;
		switch ( state )
		{
			case video::ETS_PROJECTION:
				index = SViewFrustum::ETS_PROJECTION; break;
			case video::ETS_VIEW:
				index = SViewFrustum::ETS_VIEW; break;
			default:
				break;
		}
		return Matrices [ index ];
	}

	//! Clips a line to the frustum
	inline bool SViewFrustum::clipLine(core::line3d<f32>& line) const
	{
		bool wasClipped = false;
		for (u32 i=0; i < VF_PLANE_COUNT; ++i)
		{
			if (planes[i].classifyPointRelation(line.start) == core::ISREL3D_FRONT)
			{
				line.start = line.start.getInterpolated(line.end,
						1.f-planes[i].getKnownIntersectionWithLine(line.start, line.end));
				wasClipped = true;
			}
			if (planes[i].classifyPointRelation(line.end) == core::ISREL3D_FRONT)
			{
				line.end = line.start.getInterpolated(line.end,
						1.f-planes[i].getKnownIntersectionWithLine(line.start, line.end));
				wasClipped = true;
			}
		}
		return wasClipped;
	}

	inline void SViewFrustum::recalculateBoundingSphere()
	{
		// Find the center
		const float shortlen = (getNearLeftUp() - getNearRightUp()).getLength();
		const float longlen = (getFarLeftUp() - getFarRightUp()).getLength();

		const float farlen = FarNearDistance;
		const float fartocenter = (farlen + (shortlen - longlen) * (shortlen + longlen)/(4*farlen)) / 2;
		const float neartocenter = farlen - fartocenter;

		BoundingCenter = cameraPosition + -planes[VF_NEAR_PLANE].Normal * neartocenter;

		// Find the radius
		core::vector3df dir[8];
		dir[0] = getFarLeftUp() - BoundingCenter;
		dir[1] = getFarRightUp() - BoundingCenter;
		dir[2] = getFarLeftDown() - BoundingCenter;
		dir[3] = getFarRightDown() - BoundingCenter;
		dir[4] = getNearRightDown() - BoundingCenter;
		dir[5] = getNearLeftDown() - BoundingCenter;
		dir[6] = getNearRightUp() - BoundingCenter;
		dir[7] = getNearLeftUp() - BoundingCenter;

		u32 i = 0;
		float diam[8] = { 0.f };

		for (i = 0; i < 8; ++i)
			diam[i] = dir[i].getLengthSQ();

		float longest = 0;

		for (i = 0; i < 8; ++i)
		{
			if (diam[i] > longest)
				longest = diam[i];
		}

		BoundingRadius = sqrtf(longest);
	}

} // end namespace scene
} // end namespace irr

#endif