//-----------------------------------------------------------------------------
// 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 "lighting/common/projectedShadow.h"

#include "gfx/primBuilder.h"
#include "gfx/gfxTextureManager.h"
#include "gfx/bitmap/gBitmap.h"
#include "gfx/gfxDebugEvent.h"
#include "math/mathUtils.h"
#include "lighting/lightInfo.h"
#include "lighting/lightingInterfaces.h"
#include "T3D/shapeBase.h"
#include "scene/sceneManager.h"
#include "lighting/lightManager.h"
#include "ts/tsMesh.h"
#include "T3D/decal/decalManager.h"
#include "T3D/decal/decalInstance.h"
#include "renderInstance/renderPassManager.h"
#include "renderInstance/renderMeshMgr.h"
#include "gfx/gfxTransformSaver.h"
#include "materials/customMaterialDefinition.h"
#include "materials/materialFeatureTypes.h"
#include "console/console.h"
#include "postFx/postEffect.h"
#include "lighting/basic/basicLightManager.h"
#include "lighting/shadowMap/shadowMatHook.h"
#include "materials/materialManager.h"
#include "lighting/shadowMap/lightShadowMap.h"


SimObjectPtr<RenderPassManager> ProjectedShadow::smRenderPass = NULL;
SimObjectPtr<PostEffect> ProjectedShadow::smShadowFilter = NULL;
F32 ProjectedShadow::smDepthAdjust = 10.0f;

float ProjectedShadow::smFadeStartPixelSize = 200.0f;
float ProjectedShadow::smFadeEndPixelSize = 35.0f;


GFX_ImplementTextureProfile( BLProjectedShadowProfile,
                              GFXTextureProfile::DiffuseMap,
                              GFXTextureProfile::PreserveSize | 
                              GFXTextureProfile::RenderTarget |
                              GFXTextureProfile::Pooled,
                              GFXTextureProfile::None );

GFX_ImplementTextureProfile( BLProjectedShadowZProfile,
                              GFXTextureProfile::DiffuseMap,
                              GFXTextureProfile::PreserveSize | 
                              GFXTextureProfile::ZTarget |
                              GFXTextureProfile::Pooled,
                              GFXTextureProfile::None );


ProjectedShadow::ProjectedShadow( SceneObject *object )
{
   mParentObject = object;
   mShapeBase = dynamic_cast<ShapeBase*>( object );

   mRadius = 0;
   mLastRenderTime = 0;
   mUpdateTexture = false;

   mShadowLength = 10.0f;

   mDecalData = new DecalData;
   mDecalData->skipVertexNormals = true;

   mDecalInstance = NULL;
   
   mLastLightDir.set( 0, 0, 0 );
   mLastObjectPosition.set( object->getRenderPosition() );
   mLastObjectScale.set( object->getScale() );

   CustomMaterial *customMat = NULL;
   Sim::findObject( "BL_ProjectedShadowMaterial", customMat );
   if ( customMat )
   {
      mDecalData->material = customMat;
      mDecalData->matInst = customMat->createMatInstance();
   }
   else
      mDecalData->matInst = MATMGR->createMatInstance( "WarningMaterial" );

   mDecalData->matInst->init( MATMGR->getDefaultFeatures(), getGFXVertexFormat<GFXVertexPNTT>() );

   mCasterPositionSC = NULL;
   mShadowLengthSC = NULL;
}

ProjectedShadow::~ProjectedShadow()
{
   if ( mDecalInstance )
      gDecalManager->removeDecal( mDecalInstance );

   delete mDecalData;
   
   mShadowTexture = NULL;
   mRenderTarget = NULL;
}

bool ProjectedShadow::shouldRender( const SceneRenderState *state )
{
   // Don't render if our object has been removed from the
   // scene graph.
      
   if( !mParentObject->getSceneManager() )
      return false;

   // Don't render if the ShapeBase 
   // object's fade value is greater 
   // than the visibility epsilon.
   bool shapeFade = mShapeBase && mShapeBase->getFadeVal() < TSMesh::VISIBILITY_EPSILON;

   // Get the shapebase datablock if we have one.
   ShapeBaseData *data = NULL;
   if ( mShapeBase )
      data = static_cast<ShapeBaseData*>( mShapeBase->getDataBlock() );
   
   // Also don't render if 
   // the camera distance is greater
   // than the shadow length.
   if (  shapeFade || !mDecalData || 
         (  mDecalInstance && 
            mDecalInstance->calcPixelSize( state->getViewport().extent.y, state->getCameraPosition(), state->getWorldToScreenScale().y ) < mDecalInstance->mDataBlock->fadeEndPixelSize ) )
   {
      // Release our shadow texture
      // so that others can grab it out
      // of the pool.
      mShadowTexture = NULL;

      return false;
   }

   return true;
}

bool ProjectedShadow::_updateDecal( const SceneRenderState *state )
{
   PROFILE_SCOPE( ProjectedShadow_UpdateDecal );

   if ( !LIGHTMGR )
      return false;

   // Get the position of the decal first.   
   const Box3F &objBox = mParentObject->getObjBox();
   const Point3F boxCenter = objBox.getCenter();
   Point3F decalPos = boxCenter;
   const MatrixF &renderTransform = mParentObject->getRenderTransform();
   {
      // Set up the decal position.
      // We use the object space box center
      // multiplied by the render transform
      // of the object to ensure we benefit
      // from interpolation.
      MatrixF t( renderTransform );   
      t.setColumn(2,Point3F::UnitZ);
      t.mulP( decalPos );
   }

   if ( mDecalInstance )
   {
      mDecalInstance->mPosition = decalPos;
      if ( !shouldRender( state ) )
         return false;
   }

   // Get the sunlight for the shadow projection.
   // We want the LightManager to return NULL if it can't
   // get the "real" sun, so we specify false for the useDefault parameter.
   LightInfo *lights[4] = {0};
   LightQuery query;
   query.init( mParentObject->getWorldSphere() );
	query.getLights( lights, 4 );

   Point3F pos = renderTransform.getPosition();

   Point3F lightDir( 0, 0, 0 );
   Point3F tmp( 0, 0, 0 );
   F32 weight = 0;
   F32 range = 0;
   U32 lightCount = 0;
   F32 dist = 0;
   F32 fade = 0;
   for ( U32 i = 0; i < 4; i++ )
   {
      // If we got a NULL light, 
      // we're at the end of the list.
      if ( !lights[i] )
         break;

      if ( !lights[i]->getCastShadows() )
         continue;

      if ( lights[i]->getType() != LightInfo::Point )
         tmp = lights[i]->getDirection();
      else
         tmp = pos - lights[i]->getPosition();

      range = lights[i]->getRange().x;
      dist = ( (tmp.lenSquared()) / ((range * range) * 0.5f));
      weight = mClampF( 1.0f - ( tmp.lenSquared() / (range * range)), 0.00001f, 1.0f );

      if ( lights[i]->getType() == LightInfo::Vector )
         fade = getMax( fade, 1.0f );
      else
         fade = getMax( fade, mClampF( 1.0f - dist, 0.00001f, 1.0f ) );

      lightDir += tmp * weight;
      lightCount++;
   }

   lightDir.normalize();
   
   // No light... no shadow.
   if ( !lights[0] )
      return false;
     
   // Has the light direction
   // changed since last update?
   bool lightDirChanged = !mLastLightDir.equal( lightDir );

   // Has the parent object moved
   // or scaled since the last update?
   bool hasMoved = !mLastObjectPosition.equal( mParentObject->getRenderPosition() );
   bool hasScaled = !mLastObjectScale.equal( mParentObject->getScale() );

   // Set the last light direction
   // to the current light direction.
   mLastLightDir = lightDir;
   mLastObjectPosition = mParentObject->getRenderPosition();
   mLastObjectScale = mParentObject->getScale();


   // Temps used to generate
   // tangent vector for DecalInstance below.
   VectorF right( 0, 0, 0 );
   VectorF fwd( 0, 0, 0 );
   VectorF tmpFwd( 0, 0, 0 );
   
   U32 idx = lightDir.getLeastComponentIndex();

   tmpFwd[idx] = 1.0f;

   right = mCross( tmpFwd, lightDir );
   fwd = mCross( lightDir, right );
   right = mCross( fwd, lightDir );

   right.normalize();

   // Set up the world to light space
   // matrix, along with proper position
   // and rotation to be used as the world
   // matrix for the render to texture later on.
   static MatrixF sRotMat(EulerF( 0.0f, -(M_PI_F/2.0f), 0.0f));
   mWorldToLight.identity();
   MathUtils::getMatrixFromForwardVector( lightDir, &mWorldToLight );
   mWorldToLight.setPosition( ( pos + boxCenter ) - ( ( (mRadius * smDepthAdjust) + 0.001f ) * lightDir ) );
   mWorldToLight.mul( sRotMat );
   mWorldToLight.inverse();
   
   // Get the shapebase datablock if we have one.
   ShapeBaseData *data = NULL;
   if ( mShapeBase )
      data = static_cast<ShapeBaseData*>( mShapeBase->getDataBlock() );

   // We use the object box's extents multiplied
   // by the object's scale divided by 2 for the radius
   // because the object's worldsphere radius is not
   // rotationally invariant.
   mRadius = (objBox.getExtents() * mParentObject->getScale()).len() * 0.5f;

   if ( data )
      mRadius *= data->shadowSphereAdjust;

   // Create the decal if we don't have one yet.
   if ( !mDecalInstance )
      mDecalInstance = gDecalManager->addDecal( decalPos, 
                                                lightDir, 
                                                right, 
                                                mDecalData, 
                                                1.0f, 
                                                0, 
                                                PermanentDecal | ClipDecal | CustomDecal );

   if ( !mDecalInstance )
      return false;

   mDecalInstance->mVisibility = fade;

   // Setup decal parameters.
   mDecalInstance->mSize = mRadius * 2.0f;
   mDecalInstance->mNormal = -lightDir;
   mDecalInstance->mTangent = -right;
   mDecalInstance->mRotAroundNormal = 0;
   mDecalInstance->mPosition = decalPos;
   mDecalInstance->mDataBlock = mDecalData;
   
   // If the position of the world 
   // space box center is the same
   // as the decal's position, and
   // the light direction has not 
   // changed, we don't need to clip.
   bool shouldClip = lightDirChanged || hasMoved || hasScaled;

   // Now, check and see if the object is visible.
   const Frustum &frust = state->getFrustum();
   if ( frust.isCulled( SphereF( mDecalInstance->mPosition, mDecalInstance->mSize * mDecalInstance->mSize ) ) && !shouldClip )
      return false;

   F32 shadowLen = 10.0f;
   if ( data )
      shadowLen = data->shadowProjectionDistance;

   const Point3F &boxExtents = objBox.getExtents();
   
   
   mShadowLength = shadowLen * mParentObject->getScale().z;

   // Set up clip depth, and box half 
   // offset for decal clipping.
   Point2F clipParams(  mShadowLength, (boxExtents.x + boxExtents.y) * 0.25f );

   bool render = false;
   bool clipSucceeded = true;

   // Clip!
   if ( shouldClip )
   {
      clipSucceeded = gDecalManager->clipDecal( mDecalInstance, 
                                                NULL, 
                                                &clipParams );
   }

   // If the clip failed,
   // we'll return false in
   // order to keep from
   // unnecessarily rendering
   // into the texture.  If
   // there was no reason to clip
   // on this update, we'll assume we
   // should update the texture.
   render = clipSucceeded;

   // Tell the DecalManager we've changed this decal.
   gDecalManager->notifyDecalModified( mDecalInstance );

   return render;
}

void ProjectedShadow::_calcScore( const SceneRenderState *state )
{
   if ( !mDecalInstance )
      return;

   F32 pixRadius = mDecalInstance->calcPixelSize( state->getViewport().extent.y, state->getCameraPosition(), state->getWorldToScreenScale().y ); 
 
   F32 pct = pixRadius / mDecalInstance->mDataBlock->fadeStartPixelSize;

   U32 msSinceLastRender = Platform::getVirtualMilliseconds() - getLastRenderTime();

   ShapeBaseData *data = NULL;
   if ( mShapeBase )
      data = static_cast<ShapeBaseData*>( mShapeBase->getDataBlock() );

   // For every 1s this shadow hasn't been
   // updated we'll add 10 to the score.
   F32 secs = mFloor( (F32)msSinceLastRender / 1000.0f );
   
   mScore = pct + secs;
   mClampF( mScore, 0.0f, 2000.0f );
}

void ProjectedShadow::update( const SceneRenderState *state )
{
   mUpdateTexture = true;

   // Set the decal lod settings.
   mDecalData->fadeStartPixelSize = smFadeStartPixelSize;
   mDecalData->fadeEndPixelSize = smFadeEndPixelSize;

   // Update our decal before
   // we render to texture.
   // If it fails, something bad happened
   // (no light to grab/failed clip) and we should return.
   if ( !_updateDecal( state ) )
   {
      // Release our shadow texture
      // so that others can grab it out
      // of the pool.
      mShadowTexture = NULL;      
      mUpdateTexture = false;
      return;
   }

   _calcScore( state );

   if ( !mCasterPositionSC || !mCasterPositionSC->isValid() )
      mCasterPositionSC = mDecalData->matInst->getMaterialParameterHandle( "$shadowCasterPosition" );

   if ( !mShadowLengthSC || !mShadowLengthSC->isValid() )
      mShadowLengthSC = mDecalData->matInst->getMaterialParameterHandle( "$shadowLength" );

   MaterialParameters *matParams = mDecalData->matInst->getMaterialParameters();

   matParams->setSafe( mCasterPositionSC, mParentObject->getRenderPosition() );   
   matParams->setSafe( mShadowLengthSC, mShadowLength / 4.0f );
}

void ProjectedShadow::render( F32 camDist, const TSRenderState &rdata )
{
   if ( !mUpdateTexture )
      return;
            
   // Do the render to texture,
   // DecalManager handles rendering
   // the shadow onto the world.
   _renderToTexture( camDist, rdata );
}

BaseMatInstance* ProjectedShadow::_getShadowMaterial( BaseMatInstance *inMat )
{
   // See if we have an existing material hook.
   ShadowMaterialHook *hook = static_cast<ShadowMaterialHook*>( inMat->getHook( ShadowMaterialHook::Type ) );
   if ( !hook )
   {
      // Create a hook and initialize it using the incoming material.
      hook = new ShadowMaterialHook;
      hook->init( inMat ); 
      inMat->addHook( hook );
   }

   return hook->getShadowMat( ShadowType_Spot );
}

void ProjectedShadow::_renderToTexture( F32 camDist, const TSRenderState &rdata )
{
   PROFILE_SCOPE( ProjectedShadow_RenderToTexture );

   GFXDEBUGEVENT_SCOPE( ProjectedShadow_RenderToTexture, ColorI( 255, 0, 0 ) );

   RenderPassManager *renderPass = _getRenderPass();
   if ( !renderPass )
      return;

   GFXTransformSaver saver;
   
   // NOTE: GFXTransformSaver does not save/restore the frustum
   // so we must save it here before we modify it.
   F32 l, r, b, t, n, f;
   bool ortho;
   GFX->getFrustum( &l, &r, &b, &t, &n, &f, &ortho );  

   // Set the orthographic projection
   // matrix up, to be based on the radius
   // generated based on our shape.
   GFX->setOrtho( -mRadius, mRadius, -mRadius, mRadius, 0.001f, (mRadius * 2) * smDepthAdjust, true );

   // Set the world to light space
   // matrix set up in shouldRender().
   GFX->setWorldMatrix( mWorldToLight );

   // Get the shapebase datablock if we have one.
   ShapeBaseData *data = NULL;
   if ( mShapeBase )
      data = static_cast<ShapeBaseData*>( mShapeBase->getDataBlock() );

   // Init or update the shadow texture size.
   if ( mShadowTexture.isNull() || ( data && data->shadowSize != mShadowTexture.getWidth() ) )
   {
      U32 texSize = getNextPow2( data ? data->shadowSize : 256 * LightShadowMap::smShadowTexScalar );
      mShadowTexture.set( texSize, texSize, GFXFormatR8G8B8A8, &PostFxTargetProfile, "BLShadow" );
   }

   GFX->pushActiveRenderTarget();

   if ( !mRenderTarget )
      mRenderTarget = GFX->allocRenderToTextureTarget();

   mRenderTarget->attachTexture( GFXTextureTarget::DepthStencil, _getDepthTarget( mShadowTexture->getWidth(), mShadowTexture->getHeight() ) );
   mRenderTarget->attachTexture( GFXTextureTarget::Color0, mShadowTexture );
   GFX->setActiveRenderTarget( mRenderTarget );

   GFX->clear( GFXClearZBuffer | GFXClearStencil | GFXClearTarget, ColorI( 0, 0, 0, 0 ), 1.0f, 0 );

   const SceneRenderState *diffuseState = rdata.getSceneState();
   SceneManager *sceneManager = diffuseState->getSceneManager();

   SceneRenderState baseState
   (
      sceneManager,
      SPT_Shadow,
      SceneCameraState::fromGFXWithViewport( diffuseState->getViewport() ),
      renderPass
   );

   baseState.getMaterialDelegate().bind( &ProjectedShadow::_getShadowMaterial );
   baseState.setDiffuseCameraTransform( diffuseState->getCameraTransform() );
   baseState.setWorldToScreenScale( diffuseState->getWorldToScreenScale() );
   baseState.getCullingState().disableZoneCulling( true );
   
   mParentObject->prepRenderImage( &baseState );
   renderPass->renderPass( &baseState );

   // Delete the SceneRenderState we allocated.
   mRenderTarget->resolve();
   GFX->popActiveRenderTarget();

   // If we're close enough then filter the shadow.
   if ( camDist < BasicLightManager::getShadowFilterDistance() )
   {
      if ( !smShadowFilter )
      {
         PostEffect *filter = NULL;

         if ( !Sim::findObject( "BL_ShadowFilterPostFx", filter ) )
            Con::errorf( "ProjectedShadow::_renderToTexture() - 'BL_ShadowFilterPostFx' not found!" );

         smShadowFilter = filter;
      }

      if ( smShadowFilter )
         smShadowFilter->process( NULL, mShadowTexture );
   }

   // Restore frustum
   if (!ortho)
      GFX->setFrustum(l, r, b, t, n, f);
   else
      GFX->setOrtho(l, r, b, t, n, f);

   // Set the last render time.
   mLastRenderTime = Platform::getVirtualMilliseconds();

   // HACK: Will remove in future release!
   mDecalInstance->mCustomTex = &mShadowTexture;
}

RenderPassManager* ProjectedShadow::_getRenderPass()
{
   if ( smRenderPass.isNull() )
   {
      SimObject* renderPass = NULL;

      if ( !Sim::findObject( "BL_ProjectedShadowRPM", renderPass ) )
         Con::errorf( "ProjectedShadow::init() - 'BL_ProjectedShadowRPM' not initialized" );
      else
         smRenderPass = dynamic_cast<RenderPassManager*>(renderPass);
   }

   return smRenderPass;
}

GFXTextureObject* ProjectedShadow::_getDepthTarget( U32 width, U32 height )
{
   // Get a depth texture target from the pooled profile
   // which is returned as a temporary.
   GFXTexHandle depthTex( width, height, GFXFormatD24S8, &BLProjectedShadowZProfile, 
      "ProjectedShadow::_getDepthTarget()" );

   return depthTex;
}
