/**
 * @file llcamera.cpp
 * @brief Implementation of the LLCamera class.
 *
 * $LicenseInfo:firstyear=2000&license=viewerlgpl$
 * Second Life Viewer Source Code
 * Copyright (C) 2010, Linden Research, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation;
 * version 2.1 of the License only.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * Linden Research, Inc., 945 Battery Street, San Francisco, CA  94111  USA
 * $/LicenseInfo$
 */

#include "linden_common.h"

#include "llmath.h"
#include "llcamera.h"

// ---------------- Constructors and destructors ----------------

LLCamera::LLCamera() :
    LLCoordFrame(),
    mView(DEFAULT_FIELD_OF_VIEW),
    mAspect(DEFAULT_ASPECT_RATIO),
    mViewHeightInPixels( -1 ),          // invalid height
    mNearPlane(DEFAULT_NEAR_PLANE),
    mFarPlane(DEFAULT_FAR_PLANE),
    mFixedDistance(-1.f),
    mPlaneCount(6),
    mFrustumCornerDist(0.f)
{
    for (U32 i = 0; i < PLANE_MASK_NUM; i++)
    {
        mPlaneMask[i] = PLANE_MASK_NONE;
    }

    calculateFrustumPlanes();
}

LLCamera::LLCamera(F32 vertical_fov_rads, F32 aspect_ratio, S32 view_height_in_pixels, F32 near_plane, F32 far_plane) :
    LLCoordFrame(),
    mViewHeightInPixels(view_height_in_pixels),
    mFixedDistance(-1.f),
    mPlaneCount(6),
    mFrustumCornerDist(0.f)
{
    for (U32 i = 0; i < PLANE_MASK_NUM; i++)
    {
        mPlaneMask[i] = PLANE_MASK_NONE;
    }

    mAspect = llclamp(aspect_ratio, MIN_ASPECT_RATIO, MAX_ASPECT_RATIO);
    mNearPlane = llclamp(near_plane, MIN_NEAR_PLANE, MAX_NEAR_PLANE);
    if(far_plane < 0) far_plane = DEFAULT_FAR_PLANE;
    mFarPlane = llclamp(far_plane, MIN_FAR_PLANE, MAX_FAR_PLANE);

    setView(vertical_fov_rads);
}

LLCamera::~LLCamera()
{

}

// ---------------- LLCamera::getFoo() member functions ----------------

F32 LLCamera::getMinView() const
{
    // minimum vertical fov needs to be constrained in narrow windows.
    return mAspect > 1
        ? MIN_FIELD_OF_VIEW // wide views
        : MIN_FIELD_OF_VIEW * 1/mAspect; // clamps minimum width in narrow views
}

F32 LLCamera::getMaxView() const
{
    // maximum vertical fov needs to be constrained in wide windows.
    return mAspect > 1
        ? MAX_FIELD_OF_VIEW / mAspect  // clamps maximum width in wide views
        : MAX_FIELD_OF_VIEW; // narrow views
}

LLPlane LLCamera::getUserClipPlane()
{
    return mAgentPlanes[AGENT_PLANE_USER_CLIP];
}

// ---------------- LLCamera::setFoo() member functions ----------------

void LLCamera::setUserClipPlane(LLPlane& plane)
{
    mPlaneCount = AGENT_PLANE_USER_CLIP_NUM;
    mAgentPlanes[AGENT_PLANE_USER_CLIP] = plane;
    mPlaneMask[AGENT_PLANE_USER_CLIP] = plane.calcPlaneMask();
}

void LLCamera::disableUserClipPlane()
{
    mPlaneCount = AGENT_PLANE_NO_USER_CLIP_NUM;
}

void LLCamera::setView(F32 vertical_fov_rads)
{
    mView = llclamp(vertical_fov_rads, MIN_FIELD_OF_VIEW, MAX_FIELD_OF_VIEW);
    calculateFrustumPlanes();
}

void LLCamera::setViewHeightInPixels(S32 height)
{
    mViewHeightInPixels = height;

    // Don't really need to do this, but update the pixel meter ratio with it.
    calculateFrustumPlanes();
}

void LLCamera::setAspect(F32 aspect_ratio)
{
    mAspect = llclamp(aspect_ratio, MIN_ASPECT_RATIO, MAX_ASPECT_RATIO);
    calculateFrustumPlanes();
}


void LLCamera::setNear(F32 near_plane)
{
    mNearPlane = llclamp(near_plane, MIN_NEAR_PLANE, MAX_NEAR_PLANE);
    calculateFrustumPlanes();
}


void LLCamera::setFar(F32 far_plane)
{
    mFarPlane = llclamp(far_plane, MIN_FAR_PLANE, MAX_FAR_PLANE);
    calculateFrustumPlanes();
}


// ---------------- read/write to buffer ----------------

size_t LLCamera::writeFrustumToBuffer(char *buffer) const
{
    memcpy(buffer, &mView, sizeof(F32));        /* Flawfinder: ignore */
    buffer += sizeof(F32);
    memcpy(buffer, &mAspect, sizeof(F32));      /* Flawfinder: ignore */
    buffer += sizeof(F32);
    memcpy(buffer, &mNearPlane, sizeof(F32));   /* Flawfinder: ignore */
    buffer += sizeof(F32);
    memcpy(buffer, &mFarPlane, sizeof(F32));        /* Flawfinder: ignore */
    return 4*sizeof(F32);
}

size_t LLCamera::readFrustumFromBuffer(const char *buffer)
{
    memcpy(&mView, buffer, sizeof(F32));        /* Flawfinder: ignore */
    buffer += sizeof(F32);
    memcpy(&mAspect, buffer, sizeof(F32));      /* Flawfinder: ignore */
    buffer += sizeof(F32);
    memcpy(&mNearPlane, buffer, sizeof(F32));   /* Flawfinder: ignore */
    buffer += sizeof(F32);
    memcpy(&mFarPlane, buffer, sizeof(F32));        /* Flawfinder: ignore */
    return 4*sizeof(F32);
}


// ---------------- test methods  ----------------

static  const LLVector4a sFrustumScaler[] =
{
    LLVector4a(-1,-1,-1),
    LLVector4a( 1,-1,-1),
    LLVector4a(-1, 1,-1),
    LLVector4a( 1, 1,-1),
    LLVector4a(-1,-1, 1),
    LLVector4a( 1,-1, 1),
    LLVector4a(-1, 1, 1),
    LLVector4a( 1, 1, 1)        // 8 entries
};

bool LLCamera::isChanged()
{
    bool changed = false;
    for (U32 i = 0; i < mPlaneCount; i++)
    {
        U8 mask = mPlaneMask[i];
        if (mask != 0xff && !changed)
        {
            changed = !mAgentPlanes[i].equal(mLastAgentPlanes[i]);
        }
        mLastAgentPlanes[i].set(mAgentPlanes[i]);
    }

    return changed;
}

S32 LLCamera::AABBInFrustum(const LLVector4a &center, const LLVector4a& radius, const LLPlane* planes)
{
    if(!planes)
    {
        //use agent space
        planes = mAgentPlanes;
    }

    U8 mask = 0;
    bool result = false;
    LLVector4a rscale, maxp, minp;
    LLSimdScalar d;
    U32 max_planes = llmin(mPlaneCount, (U32) AGENT_PLANE_USER_CLIP_NUM);       // mAgentPlanes[] size is 7
    for (U32 i = 0; i < max_planes; i++)
    {
        mask = mPlaneMask[i];
        if (mask < PLANE_MASK_NUM)
        {
            const LLPlane& p(planes[i]);
            p.getAt<3>(d);
            rscale.setMul(radius, sFrustumScaler[mask]);
            minp.setSub(center, rscale);
            d = -d;
            if (p.dot3(minp).getF32() > d)
            {
                return 0;
            }

            if(!result)
            {
                maxp.setAdd(center, rscale);
                result = (p.dot3(maxp).getF32() > d);
            }
        }
    }

    return result?1:2;
}

//exactly same as the function AABBInFrustum(...)
//except uses mRegionPlanes instead of mAgentPlanes.
S32 LLCamera::AABBInRegionFrustum(const LLVector4a& center, const LLVector4a& radius)
{
    return AABBInFrustum(center, radius, mRegionPlanes);
}

S32 LLCamera::AABBInFrustumNoFarClip(const LLVector4a& center, const LLVector4a& radius, const LLPlane* planes)
{
    if(!planes)
    {
        //use agent space
        planes = mAgentPlanes;
    }

    U8 mask = 0;
    bool result = false;
    LLVector4a rscale, maxp, minp;
    LLSimdScalar d;
    U32 max_planes = llmin(mPlaneCount, (U32) AGENT_PLANE_USER_CLIP_NUM);       // mAgentPlanes[] size is 7
    for (U32 i = 0; i < max_planes; i++)
    {
        mask = mPlaneMask[i];
        if ((i != 5) && (mask < PLANE_MASK_NUM))
        {
            const LLPlane& p(planes[i]);
            p.getAt<3>(d);
            rscale.setMul(radius, sFrustumScaler[mask]);
            minp.setSub(center, rscale);
            d = -d;
            if (p.dot3(minp).getF32() > d)
            {
                return 0;
            }

            if(!result)
            {
                maxp.setAdd(center, rscale);
                result = (p.dot3(maxp).getF32() > d);
            }
        }
    }

    return result?1:2;
}

//exactly same as the function AABBInFrustumNoFarClip(...)
//except uses mRegionPlanes instead of mAgentPlanes.
S32 LLCamera::AABBInRegionFrustumNoFarClip(const LLVector4a& center, const LLVector4a& radius)
{
    return AABBInFrustumNoFarClip(center, radius, mRegionPlanes);
}

int LLCamera::sphereInFrustumQuick(const LLVector3 &sphere_center, const F32 radius)
{
    LLVector3 dist = sphere_center-mFrustCenter;
    float dsq = dist * dist;
    float rsq = mFarPlane*0.5f + radius;
    rsq *= rsq;

    if (dsq < rsq)
    {
        return 1;
    }

    return 0;
}

// Return 1 if sphere is in frustum, 2 if fully in frustum, otherwise 0.
// NOTE: 'center' is in absolute frame.
int LLCamera::sphereInFrustum(const LLVector3 &sphere_center, const F32 radius) const
{
    // Returns 1 if sphere is in frustum, 0 if not.
    bool res = false;
    for (int i = 0; i < 6; i++)
    {
        if (mPlaneMask[i] != PLANE_MASK_NONE)
        {
            float d = mAgentPlanes[i].dist(sphere_center);

            if (d > radius)
            {
                return 0;
            }
            res = res || (d > -radius);
        }
    }

    return res?1:2;
}


// return height of a sphere of given radius, located at center, in pixels
F32 LLCamera::heightInPixels(const LLVector3 &center, F32 radius ) const
{
    if (radius == 0.f) return 0.f;

    // If height initialized
    if (mViewHeightInPixels > -1)
    {
        // Convert sphere to coord system with 0,0,0 at camera
        LLVector3 vec = center - mOrigin;

        // Compute distance to sphere
        F32 dist = vec.magVec();

        // Calculate angle of whole object
        F32 angle = 2.0f * (F32) atan2(radius, dist);

        // Calculate fraction of field of view
        F32 fraction_of_fov = angle / mView;

        // Compute number of pixels tall, based on vertical field of view
        return (fraction_of_fov * mViewHeightInPixels);
    }
    else
    {
        // return invalid height
        return -1.0f;
    }
}


// ---------------- friends and operators ----------------

std::ostream& operator<<(std::ostream &s, const LLCamera &C)
{
    s << "{ \n";
    s << "  Center = " << C.getOrigin() << "\n";
    s << "  AtAxis = " << C.getXAxis() << "\n";
    s << "  LeftAxis = " << C.getYAxis() << "\n";
    s << "  UpAxis = " << C.getZAxis() << "\n";
    s << "  View = " << C.getView() << "\n";
    s << "  Aspect = " << C.getAspect() << "\n";
    s << "  NearPlane   = " << C.mNearPlane << "\n";
    s << "  FarPlane    = " << C.mFarPlane << "\n";
    s << "}";
    return s;
}



// ----------------  private member functions ----------------

void LLCamera::calculateFrustumPlanes()
{
    // The planes only change when any of the frustum descriptions change.
    // They are not affected by changes of the position of the Frustum
    // because they are known in the view frame and the position merely
    // provides information on how to get from the absolute frame to the
    // view frame.

    F32 left,right,top,bottom;
    top = mFarPlane * (F32)tanf(0.5f * mView);
    bottom = -top;
    left = top * mAspect;
    right = -left;

    calculateFrustumPlanes(left, right, top, bottom);
}

LLPlane planeFromPoints(LLVector3 p1, LLVector3 p2, LLVector3 p3)
{
    LLVector3 n = ((p2-p1)%(p3-p1));
    n.normVec();

    return LLPlane(p1, n);
}


void LLCamera::ignoreAgentFrustumPlane(S32 idx)
{
    if (idx < 0 || idx > (S32) mPlaneCount)
    {
        return;
    }

    mPlaneMask[idx] = PLANE_MASK_NONE;
    mAgentPlanes[idx].clear();
}

void LLCamera::calcAgentFrustumPlanes(LLVector3* frust)
{

    for (int i = 0; i < AGENT_FRUSTRUM_NUM; i++)
    {
        mAgentFrustum[i] = frust[i];
    }

    mFrustumCornerDist = (frust[5] - getOrigin()).magVec();

    //frust contains the 8 points of the frustum, calculate 6 planes

    //order of planes is important, keep most likely to fail in the front of the list

    //near - frust[0], frust[1], frust[2]
    mAgentPlanes[AGENT_PLANE_NEAR] = planeFromPoints(frust[0], frust[1], frust[2]);

    //far
    mAgentPlanes[AGENT_PLANE_FAR] = planeFromPoints(frust[5], frust[4], frust[6]);

    //left
    mAgentPlanes[AGENT_PLANE_LEFT] = planeFromPoints(frust[4], frust[0], frust[7]);

    //right
    mAgentPlanes[AGENT_PLANE_RIGHT] = planeFromPoints(frust[1], frust[5], frust[6]);

    //top
    mAgentPlanes[AGENT_PLANE_TOP] = planeFromPoints(frust[3], frust[2], frust[6]);

    //bottom
    mAgentPlanes[AGENT_PLANE_BOTTOM] = planeFromPoints(frust[1], frust[0], frust[4]);

    //cache plane octant facing mask for use in AABBInFrustum
    for (U32 i = 0; i < mPlaneCount; i++)
    {
        mPlaneMask[i] = mAgentPlanes[i].calcPlaneMask();
    }
}

//calculate regional planes from mAgentPlanes.
//vector "shift" is the vector of the region origin in the agent space.
void LLCamera::calcRegionFrustumPlanes(const LLVector3& shift, F32 far_clip_distance)
{
    F32 far_w;
    {
        LLVector3 p = getOrigin();
        LLVector3 n(mAgentPlanes[5][0], mAgentPlanes[5][1], mAgentPlanes[5][2]);
        F32 dd = n * p;
        if(dd + mAgentPlanes[5][3] < 0) //signed distance
        {
            far_w = -far_clip_distance - dd;
        }
        else
        {
            far_w = far_clip_distance - dd;
        }
        far_w += n * shift;
    }

    F32 d;
    LLVector3 n;
    for(S32 i = 0 ; i < 7; i++)
    {
        if (mPlaneMask[i] != 0xff)
        {
            n.setVec(mAgentPlanes[i][0], mAgentPlanes[i][1], mAgentPlanes[i][2]);

            if(i != 5)
            {
                d = mAgentPlanes[i][3] + n * shift;
            }
            else
            {
                d = far_w;
            }
            mRegionPlanes[i].setVec(n, d);
        }
    }
}

void LLCamera::calculateFrustumPlanes(F32 left, F32 right, F32 top, F32 bottom)
{
    //calculate center and radius squared of frustum in world absolute coordinates
    static LLVector3 const X_AXIS(1.f, 0.f, 0.f);
    mFrustCenter = X_AXIS*mFarPlane*0.5f;
    mFrustCenter = transformToAbsolute(mFrustCenter);
    mFrustRadiusSquared = mFarPlane*0.5f;
    mFrustRadiusSquared *= mFrustRadiusSquared * 1.05f; //pad radius squared by 5%
}

// x and y are in WINDOW space, so x = Y-Axis (left/right), y= Z-Axis(Up/Down)
void LLCamera::calculateFrustumPlanesFromWindow(F32 x1, F32 y1, F32 x2, F32 y2)
{
    F32 bottom, top, left, right;
    F32 view_height = (F32)tanf(0.5f * mView) * mFarPlane;
    F32 view_width = view_height * mAspect;

    left =   x1 * -2.f * view_width;
    right =  x2 * -2.f * view_width;
    bottom = y1 * 2.f * view_height;
    top =    y2 * 2.f * view_height;

    calculateFrustumPlanes(left, right, top, bottom);
}

// NOTE: this is the OpenGL matrix that will transform the default OpenGL view
// (-Z=at, Y=up) to the default view of the LLCamera class (X=at, Z=up):
//
// F32 cfr_transform =  {  0.f,  0.f, -1.f,  0.f,   // -Z becomes X
//                        -1.f,  0.f,  0.f,  0.f,   // -X becomes Y
//                         0.f,  1.f,  0.f,  0.f,   //  Y becomes Z
//                         0.f,  0.f,  0.f,  1.f };