/*==LICENSE==*

CyanWorlds.com Engine - MMOG client, server and tools
Copyright (C) 2011  Cyan Worlds, Inc.

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.

Additional permissions under GNU GPL version 3 section 7

If you modify this Program, or any covered work, by linking or
combining it with any of RAD Game Tools Bink SDK, Autodesk 3ds Max SDK,
NVIDIA PhysX SDK, Microsoft DirectX SDK, OpenSSL library, Independent
JPEG Group JPEG library, Microsoft Windows Media SDK, or Apple QuickTime SDK
(or a modified version of those libraries),
containing parts covered by the terms of the Bink SDK EULA, 3ds Max EULA,
PhysX SDK EULA, DirectX SDK EULA, OpenSSL and SSLeay licenses, IJG
JPEG Library README, Windows Media SDK EULA, or QuickTime SDK EULA, the
licensors of this Program grant you additional
permission to convey the resulting work. Corresponding Source for a
non-source form of such a combination shall include the source code for
the parts of OpenSSL and IJG JPEG Library used as well as that of the covered
work.

You can contact Cyan Worlds, Inc. by email legal@cyan.com
 or by snail mail at:
      Cyan Worlds, Inc.
      14617 N Newport Hwy
      Mead, WA   99021

*==LICENSE==*/

#include "HeadSpin.h"
#include "plPipeline.h"
#include "plTweak.h"
#include "hsFastMath.h"

#include "plPerspDirSlave.h"

#include <cfloat>
#include <cmath>

void plPerspDirSlave::Init() 
{ 
    plShadowSlave::Init();
    fFlags |= kCastInCameraSpace; 
}

hsPoint3 plPerspDirSlave::IProject(const hsMatrix44& world2NDC, const hsPoint3& pos, float w) const
{
    hsPoint3 retVal;
    retVal.fX = world2NDC.fMap[0][0] * pos.fX
        + world2NDC.fMap[0][1] * pos.fY
        + world2NDC.fMap[0][2] * pos.fZ
        + world2NDC.fMap[0][3] * w;

    retVal.fY = world2NDC.fMap[1][0] * pos.fX
        + world2NDC.fMap[1][1] * pos.fY
        + world2NDC.fMap[1][2] * pos.fZ
        + world2NDC.fMap[1][3] * w;

    retVal.fZ = world2NDC.fMap[2][0] * pos.fX
        + world2NDC.fMap[2][1] * pos.fY
        + world2NDC.fMap[2][2] * pos.fZ
        + world2NDC.fMap[2][3] * w;

    float invW = 1.f / (
        world2NDC.fMap[3][0] * pos.fX
        + world2NDC.fMap[3][1] * pos.fY
        + world2NDC.fMap[3][2] * pos.fZ
        + world2NDC.fMap[3][3] * w);

    retVal *= invW;

    return retVal;
}

hsBounds3Ext plPerspDirSlave::IGetPerspCasterBound(const hsMatrix44& world2NDC) const
{
    hsPoint3 corners[8];
    fCasterWorldBounds.GetCorners(corners);
    hsPoint3 perspCorners[8];

    int i;
    for( i = 0; i < 8; i++ )
    {
        perspCorners[i] = IProject(world2NDC, corners[i]);

        if( perspCorners[i].fX < -1.f )
            perspCorners[i].fX = -1.f;
        else if( perspCorners[i].fX > 1.f )
            perspCorners[i].fX = 1.f;

        if( perspCorners[i].fY < -1.f )
            perspCorners[i].fY = -1.f;
        else if( perspCorners[i].fY > 1.f )
            perspCorners[i].fY = 1.f;

        if( perspCorners[i].fZ < 0 )
            perspCorners[i].Set(0.f, 0.f, 0.f);
    }
    hsBounds3Ext bnd;
    bnd.MakeEmpty();
    for( i = 0; i < 8; i++ )
        bnd.Union(&perspCorners[i]);

    return bnd;
}

bool plPerspDirSlave::SetupViewTransform(plPipeline* pipe)
{
    plViewTransform pipeView = pipe->GetViewTransform();

    plConst(float) kYon(100.f);
    plConst(float) kHither(30.f);
    pipeView.SetHither(kHither);
    pipeView.SetYon(kYon);

    hsMatrix44 cam2NDC = pipeView.GetCameraToNDC();
    hsMatrix44 world2NDC = cam2NDC * pipeView.GetWorldToCamera();
    
    fLightDir = fLightToWorld.GetAxis(hsMatrix44::kUp);
    hsVector3 worldLiDir = fLightDir;
    hsPoint3 pWorldLiDir(worldLiDir.fX, worldLiDir.fY, worldLiDir.fZ);
    hsPoint3 perspLiPos = IProject(world2NDC, pWorldLiDir, 0);

    bool reverseZ = fLightDir.InnerProduct(pipe->GetViewDirWorld()) > 0;
    SetFlag(kReverseZ, reverseZ);
    SetFlag(kReverseCull, reverseZ);

    hsPoint3 lookAt;
    plConst(bool) kUsePerspCenter(true);
    plConst(bool) kUseFrustCenter(true);
    if( kUsePerspCenter )
    {
        hsPoint3 lookAtCam = pipeView.GetWorldToCamera() * fCasterWorldBounds.GetCenter();
        hsPoint3 lookAtNDC = IProject(pipeView.GetCameraToNDC(), lookAtCam);
        lookAt = IProject(world2NDC, fCasterWorldBounds.GetCenter());
    }
    else if( kUseFrustCenter )
    {
        plConst(float) kDist(50.f);
        hsPoint3 camFrustCenter(0.f, 0.f, kDist);
        lookAt = IProject(cam2NDC, camFrustCenter);
    }
    else
    {
        hsBounds3Ext ndcBnd(IGetPerspCasterBound(world2NDC));
        lookAt = ndcBnd.GetCenter();
    }

    hsMatrix44 camNDC2Li;
    hsMatrix44 li2CamNDC;
    IComputeCamNDCToLight(perspLiPos, lookAt, camNDC2Li, li2CamNDC);

    float minZ, maxZ;
    float cotX, cotY;

    plConst(bool) kFixedPersp(true);
    if( !kFixedPersp )
    {
        hsBounds3Ext bnd(IGetPerspCasterBound(camNDC2Li * world2NDC));
        hsBounds3Ext bnd2(IGetPerspCasterBound(world2NDC));
        bnd2.Transform(&camNDC2Li);
        plConst(bool) kUseBnd2(false);
        if( kUseBnd2 )
            bnd = bnd2;

        minZ = bnd.GetMins().fZ;
        maxZ = bnd.GetMaxs().fZ; // THIS IS WRONG

        // EAP
        // This is my hack to get the Nexus age working.  The real problem
        // is probably data-side.  I take full responsibility for this
        // hack-around breaking the entire system, loosing data, causing
        // unauthorized credit card transactions, etc.
        if (std::isnan(bnd.GetMins().fX) || std::isnan(bnd.GetMins().fY))
            return false;
        if (std::isnan(bnd.GetMaxs().fX) || std::isnan(bnd.GetMaxs().fY))
            return false;

        // THIS IS EVEN MORE WRONG
        plConst(bool) kFakeDepth(false);
        if( kFakeDepth )
        {
            plConst(float) kMin(1.f);
            plConst(float) kMax(30.f);
            minZ = kMin;
            maxZ = kMax;
        }

        plConst(float) kMinMinZ(1.f);
        if( minZ < kMinMinZ )
            minZ = kMinMinZ;

        if( -bnd.GetMins().fX > bnd.GetMaxs().fX )
        {
            hsAssert(bnd.GetMins().fX < 0, "Empty shadow caster bounds?");
            cotX = -minZ / bnd.GetMins().fX;
        }
        else
        {
            hsAssert(bnd.GetMaxs().fX > 0, "Empty shadow caster bounds?");
            cotX = minZ / bnd.GetMaxs().fX;
        }

        if( -bnd.GetMins().fY > bnd.GetMaxs().fY )
        {
            hsAssert(bnd.GetMins().fY < 0, "Empty shadow caster bounds?");
            cotY = -minZ / bnd.GetMins().fY;
        }
        else
        {
            hsAssert(bnd.GetMaxs().fY > 0, "Empty shadow caster bounds?");
            cotY = minZ / bnd.GetMaxs().fY;
        }
    }
    else
    {
        plConst(float) kHi(1.f);
        hsBounds3Ext bnd;
        const hsPoint3 lo(-1.f, -1.f, 0.f);
        const hsPoint3 hi(1.f, 1.f, kHi);
        bnd.MakeEmpty();
        bnd.Union(&lo);
        bnd.Union(&hi);

        bnd.Transform(&camNDC2Li);

        minZ = bnd.GetMins().fZ;
        maxZ = bnd.GetMaxs().fZ; // THIS IS WRONG

        // EAP
        // This is my hack to get the Nexus age working.  The real problem
        // is probably data-side.  I take full responsibility for this
        // hack-around breaking the entire system, loosing data, causing
        // unauthorized credit card transactions, etc.
        if (std::isnan(bnd.GetMins().fX) || std::isnan(bnd.GetMins().fY))
            return false;
        if (std::isnan(bnd.GetMaxs().fX) || std::isnan(bnd.GetMaxs().fY))
            return false;

        plConst(float) kMinMinZ(1.f);
        if( minZ < kMinMinZ )
            minZ = kMinMinZ;

        if( -bnd.GetMins().fX > bnd.GetMaxs().fX )
        {
            hsAssert(bnd.GetMins().fX < 0, "Empty shadow caster bounds?");
            cotX = -minZ / bnd.GetMins().fX;
        }
        else
        {
            hsAssert(bnd.GetMaxs().fX > 0, "Empty shadow caster bounds?");
            cotX = minZ / bnd.GetMaxs().fX;
        }

        if( -bnd.GetMins().fY > bnd.GetMaxs().fY )
        {
            hsAssert(bnd.GetMins().fY < 0, "Empty shadow caster bounds?");
            cotY = -minZ / bnd.GetMins().fY;
        }
        else
        {
            hsAssert(bnd.GetMaxs().fY > 0, "Empty shadow caster bounds?");
            cotY = minZ / bnd.GetMaxs().fY;
        }
    }


    hsMatrix44 proj;
    proj.Reset();
    proj.NotIdentity();

    // LightToTexture

    // First the LightToTexture, which uses the above pretty much as is.
    // Note the remapping to range [0.5..width-0.5] etc. Also, the perspective
    // divide is by the 3rd output (not the fourth), so we make the 3rd
    // output be W (instead of Z).
    // This also means that our translate goes into [i][2] instead of [i][3].
    proj.fMap[0][0] = cotX * 0.5f;
    proj.fMap[0][2] = 0.5f * (1.f + 0.5f/fWidth);
    proj.fMap[1][1] = -cotY * 0.5f;
    proj.fMap[1][2] = 0.5f * (1.f + 0.5f/fHeight);
#if 1 // This computes correct Z, but we really just want W in 3rd component. HACKFISH
    proj.fMap[2][2] = maxZ / (maxZ - minZ);
    proj.fMap[2][3] = -minZ * maxZ / (maxZ - minZ);
#elif 1
    proj.fMap[2][2] = 1.f;
    proj.fMap[2][3] = 0;
#endif
    proj.fMap[3][2] = 1.f;
    proj.fMap[3][3] = 0;

//  fWorldToTexture = proj * camNDC2Li * pipeView.GetWorldToNDC();
    fWorldToTexture = proj * camNDC2Li * world2NDC;
    fWorldToTexture.fMap[2][0] = fWorldToTexture.fMap[3][0];
    fWorldToTexture.fMap[2][1] = fWorldToTexture.fMap[3][1];
    fWorldToTexture.fMap[2][2] = fWorldToTexture.fMap[3][2];
    fWorldToTexture.fMap[2][3] = fWorldToTexture.fMap[3][3];

    // Now the LightToNDC. This one's a little trickier, because we want to compensate for
    // having brought in the viewport to keep our border constant, so we can clamp the 
    // projected texture and not have the edges smear off to infinity.
    cotX -= cotX / (fWidth * 0.5f);
    cotY -= cotY / (fHeight * 0.5f);

    float tanX = 1.f / cotX;
    float tanY = 1.f / cotY;
    fView.SetScreenSize((uint16_t)fWidth, (uint16_t)fHeight);
    fView.SetCameraTransform(pipe->GetViewTransform().GetWorldToCamera(), pipe->GetViewTransform().GetCameraToWorld());
    fView.SetPerspective(true);
    fView.SetViewPort(0, 0, (float)fWidth, (float)fHeight, false);
    fView.SetView(hsPoint3(-tanX, -tanY, minZ), hsPoint3(tanX, tanY, maxZ));

    hsMatrix44 cam2Light = camNDC2Li * pipeView.GetCameraToNDC();
    fView.PostMultCameraToNDC(cam2Light);

    fLightPos = fLightToWorld.GetTranslate();
    SetFlag(kPositional, false);
    
    return true;
}

static inline hsVector3 CrossProd(const hsVector3& a, const hsPoint3& b)
{
    return hsVector3(a.fY*b.fZ - a.fZ*b.fY, a.fZ*b.fX - a.fX*b.fZ, a.fX*b.fY - a.fY*b.fX);
}

static inline void InverseOfPureRotTran(const hsMatrix44& src, hsMatrix44& inv)
{
    inv = src;

    // We know this is a pure rotation and translation matrix, so
    // we won't have to do a full inverse. Okay kids, don't try this
    // at home.
    inv.fMap[0][1] = src.fMap[1][0];
    inv.fMap[0][2] = src.fMap[2][0];

    inv.fMap[1][0] = src.fMap[0][1];
    inv.fMap[1][2] = src.fMap[2][1];

    inv.fMap[2][0] = src.fMap[0][2];
    inv.fMap[2][1] = src.fMap[1][2];

    hsPoint3 newTran(-src.fMap[0][3], -src.fMap[1][3], -src.fMap[2][3]);
    inv.fMap[0][3] = newTran.InnerProduct((hsVector3*)&inv.fMap[0][0]);
    inv.fMap[1][3] = newTran.InnerProduct((hsVector3*)&inv.fMap[1][0]);
    inv.fMap[2][3] = newTran.InnerProduct((hsVector3*)&inv.fMap[2][0]);
}

void plPerspDirSlave::IComputeCamNDCToLight(const hsPoint3& from, const hsPoint3& at, hsMatrix44& camNDC2Li, hsMatrix44& li2CamNDC)
{

    hsVector3 atToFrom(&from, &at);
    float distSq = atToFrom.MagnitudeSquared();
    atToFrom *= hsFastMath::InvSqrtAppr(distSq);

    const float kMinMag = 0.5f;
    hsVector3 up(0.f, 0.f, 1.f);
    if( CrossProd(up, (at - from)).MagnitudeSquared() < kMinMag )
    {
        up.Set(0.f, 1.f, 0.f);
    }
    hsMatrix44 w2light;
    w2light.MakeCamera(&from, &at, &up);

    hsMatrix44 light2w;
    InverseOfPureRotTran(w2light, light2w);

#ifdef CHECK_INVERSE
    hsMatrix44 inv;
    w2light.GetInverse(&inv);
#endif // CHECK_INVERSE

    camNDC2Li = w2light;
    li2CamNDC = light2w;
}
