//-----------------------------------------------------------------------------
// Copyright (c) 2012 GarageGames, LLC
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
//-----------------------------------------------------------------------------

#include "platform/platform.h"
#include "math/util/frustum.h"

#include "math/mMathFn.h"
#include "math/mathUtils.h"
#include "math/mSphere.h"
#include "platform/profiler.h"


//TODO: For OBB/frustum intersections and ortho frustums, we can resort to a much quicker AABB/OBB test


// Must be CW ordered for face[0] of each edge!  Keep in mind that normals
// are pointing *inwards* and thus what appears CCW outside is CW inside.
FrustumData::EdgeListType FrustumData::smEdges
(
   PolyhedronData::Edge( PlaneNear, PlaneTop, NearTopRight, NearTopLeft ),
   PolyhedronData::Edge( PlaneNear, PlaneBottom, NearBottomLeft, NearBottomRight ),
   PolyhedronData::Edge( PlaneNear, PlaneLeft, NearTopLeft, NearBottomLeft ),
   PolyhedronData::Edge( PlaneNear, PlaneRight, NearTopRight, NearBottomRight ),
   PolyhedronData::Edge( PlaneFar, PlaneTop, FarTopLeft, FarTopRight ),
   PolyhedronData::Edge( PlaneFar, PlaneBottom, FarBottomRight, FarBottomLeft ),
   PolyhedronData::Edge( PlaneFar, PlaneLeft, FarBottomLeft, FarTopLeft ),
   PolyhedronData::Edge( PlaneFar, PlaneRight, FarTopRight, FarBottomRight ),
   PolyhedronData::Edge( PlaneTop, PlaneLeft, FarTopLeft, NearTopLeft ),
   PolyhedronData::Edge( PlaneTop, PlaneRight, NearTopRight, FarTopRight ),
   PolyhedronData::Edge( PlaneBottom, PlaneLeft, NearBottomLeft, FarBottomLeft ),
   PolyhedronData::Edge( PlaneBottom, PlaneRight, FarBottomRight, NearBottomRight )
);


//-----------------------------------------------------------------------------

Frustum::Frustum( bool isOrtho,
                  F32 nearLeft, 
                  F32 nearRight, 
                  F32 nearTop, 
                  F32 nearBottom, 
                  F32 nearDist,                  
                  F32 farDist,
                  const MatrixF &transform )
{
   mTransform = transform;
   mPosition = transform.getPosition();

   mNearLeft = nearLeft;
   mNearRight = nearRight;
   mNearTop = nearTop;
   mNearBottom = nearBottom;
   mNearDist = nearDist;
   mFarDist = farDist;
   mIsOrtho = isOrtho;

   mNumTiles = 1;
   mCurrTile.set(0,0);
   mTileOverlap.set(0.0f, 0.0f);

   mProjectionOffset.zero();
   mProjectionOffsetMatrix.identity();
}

//-----------------------------------------------------------------------------

void Frustum::set(   bool isOrtho,
                     F32 fovYInRadians, 
                     F32 aspectRatio, 
                     F32 nearDist, 
                     F32 farDist,
                     const MatrixF &transform )
{
   F32 left, right, top, bottom;
   MathUtils::makeFrustum( &left, &right, &top, &bottom, fovYInRadians, aspectRatio, nearDist );

   tile( &left, &right, &top, &bottom, mNumTiles, mCurrTile, mTileOverlap );
   set( isOrtho, left, right, top, bottom, nearDist, farDist, transform );
}

//-----------------------------------------------------------------------------

void Frustum::set(   bool isOrtho,
                     F32 nearLeft, 
                     F32 nearRight, 
                     F32 nearTop, 
                     F32 nearBottom, 
                     F32 nearDist, 
                     F32 farDist,
                     const MatrixF &transform )
{
   mTransform = transform;
   mPosition = mTransform.getPosition();

   mNearLeft = nearLeft;
   mNearRight = nearRight;
   mNearTop = nearTop;
   mNearBottom = nearBottom;
   mNearDist = nearDist;
   mFarDist = farDist;
   mIsOrtho = isOrtho;

   mDirty = true;
}

//-----------------------------------------------------------------------------

#if 0
void Frustum::set( const MatrixF &projMat, bool normalize )
{ 
   // From "Fast Extraction of Viewing Frustum Planes from the World-View-Projection Matrix"
   // by Gil Gribb and Klaus Hartmann.
   //
   // http://www2.ravensoft.com/users/ggribb/plane%20extraction.pdf

   // Right clipping plane.
   mPlanes[ PlaneRight ].set(  projMat[3] - projMat[0], 
                                             projMat[7] - projMat[4], 
                                             projMat[11] - projMat[8],
                                             projMat[15] - projMat[12] );

   // Left clipping plane.
   mPlanes[ PlaneLeft ].set(   projMat[3] + projMat[0], 
                                             projMat[7] + projMat[4], 
                                             projMat[11] + projMat[8], 
                                             projMat[15] + projMat[12] );

   // Bottom clipping plane.
   mPlanes[ PlaneBottom ].set( projMat[3] + projMat[1], 
                                             projMat[7] + projMat[5], 
                                             projMat[11] + projMat[9], 
                                             projMat[15] + projMat[13] );

   // Top clipping plane.
   mPlanes[ PlaneTop ].set(    projMat[3] - projMat[1], 
                                             projMat[7] - projMat[5], 
                                             projMat[11] - projMat[9], 
                                             projMat[15] - projMat[13] );

   // Near clipping plane
   mPlanes[ PlaneNear ].set(   projMat[3] + projMat[2], 
                                             projMat[7] + projMat[6], 
                                             projMat[11] + projMat[10],
                                             projMat[15] + projMat[14] );

   // Far clipping plane.
   mPlanes[ PlaneFar ].set(    projMat[3] - projMat[2], 
                                             projMat[7] - projMat[6], 
                                             projMat[11] - projMat[10], 
                                             projMat[15] - projMat[14] );

   if( normalize )
   {
      for( S32 i = 0; i < PlaneCount; ++ i )
         mPlanes[ i ].normalize();
   }

   /* // Create the corner points via plane intersections.
   mPlanes[ PlaneNear ].intersect( mPlanes[ PlaneTop ], mPlanes[ PlaneLeft ], &mPoints[ NearTopLeft ] );
   mPlanes[ PlaneNear ].intersect( mPlanes[ PlaneTop ], mPlanes[ PlaneRight ], &mPoints[ NearTopRight ] );
   mPlanes[ PlaneNear ].intersect( mPlanes[ PlaneBottom ], mPlanes[ PlaneLeft ], &mPoints[ NearBottomLeft ] );
   mPlanes[ PlaneNear ].intersect( mPlanes[ PlaneBottom ], mPlanes[ PlaneRight ], &mPoints[ NearBottomRight ] );
   mPlanes[ PlaneFar ].intersect( mPlanes[ PlaneTop ], mPlanes[ PlaneLeft ], &mPoints[ FarTopLeft ] );
   mPlanes[ PlaneFar ].intersect( mPlanes[ PlaneTop ], mPlanes[ PlaneRight ], &mPoints[ FarTopRight ] );
   mPlanes[ PlaneFar ].intersect( mPlanes[ PlaneBottom ], mPlanes[ PlaneLeft ], &mPoints[ FarBottomLeft ] );
   mPlanes[ PlaneFar ].intersect( mPlanes[ PlaneBottom ], mPlanes[ PlaneRight ], &mPoints[ FarBottomRight ] );
   */

   // Update the axis aligned bounding box.
   _updateBounds();
}
#endif

//-----------------------------------------------------------------------------

void Frustum::setNearDist( F32 nearDist )
{
   setNearFarDist( nearDist, mFarDist );
}

//-----------------------------------------------------------------------------

void Frustum::setFarDist( F32 farDist )
{
   setNearFarDist( mNearDist, farDist );
}

//-----------------------------------------------------------------------------

void Frustum::setNearFarDist( F32 nearDist, F32 farDist )
{
   if( mNearDist == nearDist && mFarDist == farDist )
      return;

   // Recalculate the frustum.
   MatrixF xfm( mTransform ); 
   set( mIsOrtho, getFov(), getAspectRatio(), nearDist, farDist, xfm );
}

//-----------------------------------------------------------------------------

void Frustum::cropNearFar(F32 newNearDist, F32 newFarDist)
{
   const F32 newOverOld = newNearDist / mNearDist;

   set( mIsOrtho, mNearLeft * newOverOld, mNearRight * newOverOld, mNearTop * newOverOld, mNearBottom * newOverOld, 
      newNearDist, newFarDist, mTransform);
}

//-----------------------------------------------------------------------------

void FrustumData::_update() const
{
   if( !mDirty )
      return;

   PROFILE_SCOPE( Frustum_update );

   const Point3F& cameraPos = mPosition;

   // Build the frustum points in camera space first.

   if( mIsOrtho )
   {
      mPoints[ NearTopLeft ].set( mNearLeft, mNearDist, mNearTop );
      mPoints[ NearTopRight ].set( mNearRight, mNearDist, mNearTop );
      mPoints[ NearBottomLeft ].set( mNearLeft, mNearDist, mNearBottom );
      mPoints[ NearBottomRight ].set( mNearRight, mNearDist, mNearBottom );
      mPoints[ FarTopLeft ].set( mNearLeft, mFarDist, mNearTop );
      mPoints[ FarTopRight ].set( mNearRight, mFarDist, mNearTop );
      mPoints[ FarBottomLeft ].set( mNearLeft, mFarDist, mNearBottom );
      mPoints[ FarBottomRight ].set( mNearRight, mFarDist, mNearBottom );
   }
   else
   {
      const F32 farOverNear = mFarDist / mNearDist;

      mPoints[ NearTopLeft ].set( mNearLeft, mNearDist, mNearTop );
      mPoints[ NearTopRight ].set( mNearRight, mNearDist, mNearTop );
      mPoints[ NearBottomLeft ].set( mNearLeft, mNearDist, mNearBottom );
      mPoints[ NearBottomRight ].set( mNearRight, mNearDist, mNearBottom );
      mPoints[ FarTopLeft ].set( mNearLeft * farOverNear, mFarDist, mNearTop * farOverNear );
      mPoints[ FarTopRight ].set( mNearRight * farOverNear, mFarDist, mNearTop * farOverNear );
      mPoints[ FarBottomLeft ].set( mNearLeft * farOverNear, mFarDist, mNearBottom * farOverNear );
      mPoints[ FarBottomRight ].set( mNearRight * farOverNear, mFarDist, mNearBottom * farOverNear );
   }

   // Transform the points into the desired culling space.

   for( U32 i = 0; i < mPoints.size(); ++ i )
      mTransform.mulP( mPoints[ i ] );

   // Update the axis aligned bounding box from 
   // the newly transformed points.

   mBounds = Box3F::aroundPoints( mPoints.address(), mPoints.size() );

   // Finally build the planes.

   if( mIsOrtho )
   {
      mPlanes[ PlaneLeft ].set(   mPoints[ NearBottomLeft ], 
                                  mPoints[ FarTopLeft ], 
                                  mPoints[ FarBottomLeft ] );

      mPlanes[ PlaneRight ].set(  mPoints[ NearTopRight ], 
                                  mPoints[ FarBottomRight ], 
                                  mPoints[ FarTopRight ] );

      mPlanes[ PlaneTop ].set(    mPoints[ FarTopRight ], 
                                  mPoints[ NearTopLeft ], 
                                  mPoints[ NearTopRight ] );

      mPlanes[ PlaneBottom ].set( mPoints[ NearBottomRight ], 
                                  mPoints[ FarBottomLeft ], 
                                  mPoints[ FarBottomRight ] );

      mPlanes[ PlaneNear ].set(   mPoints[ NearTopLeft ], 
                                  mPoints[ NearBottomLeft ], 
                                  mPoints[ NearTopRight ] );

      mPlanes[ PlaneFar ].set(    mPoints[ FarTopLeft ], 
                                  mPoints[ FarTopRight ], 
                                  mPoints[ FarBottomLeft ] );
   }
   else
   {
      mPlanes[ PlaneLeft ].set(   cameraPos,
                                  mPoints[ NearTopLeft ], 
                                  mPoints[ NearBottomLeft ] );

      mPlanes[ PlaneRight ].set(  cameraPos,
                                  mPoints[ NearBottomRight ], 
                                  mPoints[ NearTopRight ] );

      mPlanes[ PlaneTop ].set(    cameraPos,
                                  mPoints[ NearTopRight ], 
                                  mPoints[ NearTopLeft ] );

      mPlanes[ PlaneBottom ].set( cameraPos,
                                  mPoints[ NearBottomLeft ], 
                                  mPoints[ NearBottomRight ] );

      mPlanes[ PlaneNear ].set(   mPoints[ NearTopLeft ],
                                  mPoints[ NearBottomLeft ], 
                                  mPoints[ NearTopRight ] );

      mPlanes[ PlaneFar ].set(    mPoints[ FarTopLeft ],
                                  mPoints[ FarTopRight ], 
                                  mPoints[ FarBottomLeft ] );
   }

   // If the frustum plane orientation doesn't match mIsInverted
   // now, invert all the plane normals.
   //
   // Note that if we have a transform matrix with a negative scale,
   // then the initial planes we have computed will always be inverted.

   const bool inverted = mPlanes[ PlaneNear ].whichSide( cameraPos ) == PlaneF::Front;
   if( inverted != mIsInverted )
   {
      for( U32 i = 0; i < mPlanes.size(); ++ i )
         mPlanes[ i ].invert();
   }

   AssertFatal( mPlanes[ PlaneNear ].whichSide( cameraPos ) != PlaneF::Front,
      "Frustum::_update - Viewpoint lies on front side of near plane!" );

   // And now the center points which are mostly just used in debug rendering.

   mPlaneCenters[ PlaneLeftCenter ] = (   mPoints[ NearTopLeft ] + 
                                          mPoints[ NearBottomLeft ] + 
                                          mPoints[ FarTopLeft ] + 
                                          mPoints[ FarBottomLeft ] ) / 4.0f;

   mPlaneCenters[ PlaneRightCenter ] = (  mPoints[ NearTopRight ] + 
                                          mPoints[ NearBottomRight ] + 
                                          mPoints[ FarTopRight ] + 
                                          mPoints[ FarBottomRight ] ) / 4.0f;

   mPlaneCenters[ PlaneTopCenter ] = ( mPoints[ NearTopLeft ] + 
                                       mPoints[ NearTopRight ] + 
                                       mPoints[ FarTopLeft ] + 
                                       mPoints[ FarTopRight ] ) / 4.0f;

   mPlaneCenters[ PlaneBottomCenter ] = ( mPoints[ NearBottomLeft ] + 
                                          mPoints[ NearBottomRight ] + 
                                          mPoints[ FarBottomLeft ] + 
                                          mPoints[ FarBottomRight ] ) / 4.0f;

   mPlaneCenters[ PlaneNearCenter ] = (   mPoints[ NearTopLeft ] + 
                                          mPoints[ NearTopRight ] + 
                                          mPoints[ NearBottomLeft ] + 
                                          mPoints[ NearBottomRight ] ) / 4.0f;

   mPlaneCenters[ PlaneFarCenter ] = ( mPoints[ FarTopLeft ] + 
                                       mPoints[ FarTopRight ] + 
                                       mPoints[ FarBottomLeft ] + 
                                       mPoints[ FarBottomRight ] ) / 4.0f;

   // Done.

   mDirty = false;
}

//-----------------------------------------------------------------------------

void Frustum::invert()
{
   mIsInverted = !mIsInverted;
   _update();
}

//-----------------------------------------------------------------------------

void Frustum::setTransform( const MatrixF &mat )
{
	mTransform = mat;
   mPosition = mTransform.getPosition();
   mDirty = true;
}

//-----------------------------------------------------------------------------

void Frustum::scaleFromCenter( F32 scale )
{
   // Extract the fov and aspect ratio.
   F32 fovInRadians = mAtan2( (mNearTop - mNearBottom)*mNumTiles/2.0f, mNearDist ) * 2.0f;
   F32 aspectRatio = (mNearRight - mNearLeft)/(mNearTop - mNearBottom);

   // Now move the near and far planes out.
   F32 halfDist = ( mFarDist - mNearDist ) / 2.0f;
   mNearDist   -= halfDist * ( scale - 1.0f );
   mFarDist    += halfDist * ( scale - 1.0f );

   // Setup the new scaled frustum.
   set( mIsOrtho, fovInRadians, aspectRatio, mNearDist, mFarDist, mTransform );
}

//-----------------------------------------------------------------------------

void Frustum::mul( const MatrixF& mat )
{
   mTransform.mul( mat );
   mDirty = true;
}

//-----------------------------------------------------------------------------

void Frustum::mulL( const MatrixF& mat )
{
   MatrixF last( mTransform );
   mTransform.mul( mat, last );

   mDirty = true;
}

//-----------------------------------------------------------------------------

void Frustum::setProjectionOffset(const Point2F& offsetMat)
{
   mProjectionOffset = offsetMat;
   mProjectionOffsetMatrix.identity();
   mProjectionOffsetMatrix.setPosition(Point3F(mProjectionOffset.x, mProjectionOffset.y, 0.0f));
}

//-----------------------------------------------------------------------------

void Frustum::getProjectionMatrix( MatrixF *proj, bool gfxRotate ) const
{
   if (mIsOrtho)
   {
      MathUtils::makeOrthoProjection(proj, mNearLeft, mNearRight, mNearTop, mNearBottom, mNearDist, mFarDist, gfxRotate);
      proj->mulL(mProjectionOffsetMatrix);
   }
   else
   {
      MathUtils::makeProjection(proj, mNearLeft, mNearRight, mNearTop, mNearBottom, mNearDist, mFarDist, gfxRotate);
      proj->mulL(mProjectionOffsetMatrix);
   }
}

//-----------------------------------------------------------------------------

void Frustum::tileFrustum(U32 numTiles, const Point2I& curTile, Point2F overlap)
{
   //These will be stored to re-tile the frustum if needed
   mNumTiles = numTiles; 
   mCurrTile = curTile; 
   mTileOverlap = overlap;
   
   tile(&mNearLeft, &mNearRight, &mNearTop, &mNearBottom, mNumTiles, mCurrTile, mTileOverlap);
}

//-----------------------------------------------------------------------------

void Frustum::tile( F32 *left, F32 *right, F32 *top, F32 *bottom, U32 numTiles, const Point2I& curTile, Point2F overlap )
{
   if (numTiles == 1)
      return;

   Point2F tileSize( ( *right - *left ) / (F32)numTiles, 
                     ( *top - *bottom ) / (F32)numTiles );
   
   F32 leftOffset   = tileSize.x*overlap.x;
   F32 rightOffset  = tileSize.x*overlap.x*2;
   F32 bottomOffset = tileSize.y*overlap.y;
   F32 topOffset    = tileSize.y*overlap.y*2;

   *left += tileSize.x * curTile.x - leftOffset;
   *right = *left + tileSize.x + rightOffset;
   *bottom += tileSize.y * curTile.y - bottomOffset;
   *top = *bottom + tileSize.y + topOffset;
}
