/**
  @file GCamera.cpp

  @author Morgan McGuire, http://graphics.cs.williams.edu
  @author Jeff Marsceill, 08jcm@williams.edu
 
  @created 2005-07-20
  @edited  2010-02-22
*/
#include "G3D/GCamera.h"
#include "G3D/platform.h"
#include "G3D/Rect2D.h"
#include "G3D/BinaryInput.h"
#include "G3D/BinaryOutput.h"
#include "G3D/Ray.h"
#include "G3D/Matrix4.h"
#include "G3D/Any.h"
#include "G3D/stringutils.h"

namespace G3D {

GCamera::GCamera(const Any& any) {
    any.verifyName("GCamera");
    any.verifyType(Any::TABLE);
    *this = GCamera();

    const Any::AnyTable& table = any.table();
    Any::AnyTable::Iterator it = table.begin();
    while (it.hasMore()) {
        const std::string& k = toUpper(it->key);
        if (k == "FOVDIRECTION") {
            const std::string& v = toUpper(it->value);
            if (v == "HORIZONTAL") {
                m_direction = HORIZONTAL;
            } else if (v == "VERTICAL") {
                m_direction = VERTICAL;
            } else {
                any.verify(false, "fovDirection must be \"HORIZONTAL\" or \"VERTICAL\"");
            }
        } else if (k == "COORDINATEFRAME") {
            m_cframe = it->value;
        } else if (k == "FOVDEGREES") {
            m_fieldOfView = toRadians(it->value.number());
        } else if (k == "NEARPLANEZ") {
            m_nearPlaneZ = it->value;
        } else if (k == "FARPLANEZ") {
            m_farPlaneZ = it->value;
        } else if (k == "PIXELOFFSET") {
            m_pixelOffset = it->value;
        } else {
            any.verify(false, std::string("Illegal key in table: ") + it->key);
        }
        ++it;
    }
}


GCamera::operator Any() const {
    Any any(Any::TABLE, "GCamera");

    any.set("fovDirection", std::string((m_direction == HORIZONTAL) ? "HORIZONTAL" : "VERTICAL"));
    any.set("fovDegrees", toDegrees(m_fieldOfView));
    any.set("nearPlaneZ", nearPlaneZ());
    any.set("farPlaneZ", farPlaneZ());
    any.set("coordinateFrame", coordinateFrame());
    any.set("pixelOffset", pixelOffset());

    return any;
}


GCamera::GCamera() {
    setNearPlaneZ(-0.2f);
    setFarPlaneZ(-150.0f);
    setFieldOfView((float)toRadians(90.0f), HORIZONTAL);
}


GCamera::GCamera(const Matrix4& proj, const CFrame& frame) {
    float left, right, bottom, top, nearval, farval;
    proj.getPerspectiveProjectionParameters(left, right, bottom, top, nearval, farval);
    setNearPlaneZ(-nearval);
    setFarPlaneZ(-farval);
    float x = right;

    // Assume horizontal field of view
    setFieldOfView(atan2(x, -m_nearPlaneZ) * 2.0f, HORIZONTAL);
    setCoordinateFrame(frame);
}


GCamera::~GCamera() {
}


void GCamera::getCoordinateFrame(CoordinateFrame& c) const {
    c = m_cframe;
}


void GCamera::setCoordinateFrame(const CoordinateFrame& c) {
    m_cframe = c;
}


void GCamera::setFieldOfView(float angle, FOVDirection dir) {
    debugAssert((angle < pi()) && (angle > 0));
    
    m_fieldOfView = angle;
    m_direction = dir;
}

 
float GCamera::imagePlaneDepth() const{
    return -m_nearPlaneZ;
}

float GCamera::viewportWidth(const Rect2D& viewport) const {
    // Compute the side of a square at the near plane based on our field of view
    float s = 2.0f * -m_nearPlaneZ * tan(m_fieldOfView * 0.5f);

    if (m_direction == VERTICAL) {
        s *= viewport.width() / viewport.height();
    }

    return s;
}


float GCamera::viewportHeight(const Rect2D& viewport) const {
    // Compute the side of a square at the near plane based on our field of view
    float s = 2.0f * -m_nearPlaneZ * tan(m_fieldOfView * 0.5f);

    debugAssert(m_fieldOfView < toRadians(180));
    if (m_direction == HORIZONTAL) {
        s *= viewport.height() / viewport.width();
    }

    return s;
}


Ray GCamera::worldRay(float x, float y, const Rect2D& viewport) const {

    int screenWidth  = iFloor(viewport.width());
    int screenHeight = iFloor(viewport.height());

    Vector3 origin = m_cframe.translation;

    float cx = screenWidth  / 2.0f;
    float cy = screenHeight / 2.0f;

    float vw = viewportWidth(viewport);
    float vh = viewportHeight(viewport);

	Vector3 direction = Vector3( (x - cx) * vw / screenWidth,
                            -(y - cy) * vh / screenHeight,
                             m_nearPlaneZ);

    direction = m_cframe.vectorToWorldSpace(direction);

    // Normalize the direction (we didn't do it before)
    direction = direction.direction();

    return Ray::fromOriginAndDirection(origin, direction);
}


void GCamera::getProjectPixelMatrix(const Rect2D& viewport, Matrix4& P) const {
    getProjectUnitMatrix(viewport, P);
    float screenWidth  = viewport.width();
    float screenHeight = viewport.height();

    float sx = screenWidth / 2.0;
    float sy = screenHeight / 2.0;

    P = Matrix4(sx, 0, 0, sx + viewport.x0() - m_pixelOffset.x,
                0, -sy, 0, sy + viewport.y0() + m_pixelOffset.y,
                0, 0,  1, 0,
                0, 0,  0, 1) * P;
}


void GCamera::getProjectUnitMatrix(const Rect2D& viewport, Matrix4& P) const {

    float screenWidth  = viewport.width();
    float screenHeight = viewport.height();

    float r, l, t, b, n, f, x, y;

    float s = 1.0f;
    if (m_direction == VERTICAL) {
        y = -m_nearPlaneZ * tan(m_fieldOfView / 2);
        x = y * (screenWidth / screenHeight);
        s = screenHeight;
    } else { //m_direction == HORIZONTAL
        x = -m_nearPlaneZ * tan(m_fieldOfView / 2);
        y = x * (screenHeight / screenWidth);
        s = screenWidth;
    }

    n = -m_nearPlaneZ;
    f = -m_farPlaneZ;
    r = x  - m_pixelOffset.x/s;
    l = -x - m_pixelOffset.x/s;
    t = y  + m_pixelOffset.y/s;
    b = -y + m_pixelOffset.y/s;

    P = Matrix4::perspectiveProjection(l, r, b, t, n, f);
}


Vector3 GCamera::projectUnit(const Vector3& point, const Rect2D& viewport) const {
    Matrix4 M;
    getProjectUnitMatrix(viewport, M);

    Vector4 cameraSpacePoint(coordinateFrame().pointToObjectSpace(point), 1.0f);
    const Vector4& screenSpacePoint = M * cameraSpacePoint;

    return Vector3(screenSpacePoint.xyz() / screenSpacePoint.w);
}

Vector3 GCamera::project(const Vector3& point,
                          const Rect2D&  viewport) const {

    // Find the point in the homogeneous cube
    const Vector3& cube = projectUnit(point, viewport);

    return convertFromUnitToNormal(cube, viewport);
}

Vector3 GCamera::unprojectUnit(const Vector3& v, const Rect2D& viewport) const {

    const Vector3& projectedPoint = convertFromUnitToNormal(v, viewport);

    return unproject(projectedPoint, viewport);
}


Vector3 GCamera::unproject(const Vector3& v, const Rect2D& viewport) const {
    
    const float n = m_nearPlaneZ;
    const float f = m_farPlaneZ;

    float z;

    if (-f >= finf()) {
        // Infinite far plane
        z = 1.0f / (((-1.0f / n) * v.z) + 1.0f / n);
    } else {
        z = 1.0f / ((((1.0f / f) - (1.0f / n)) * v.z) + 1.0f / n);
    }

    const Ray& ray = worldRay(v.x - m_pixelOffset.x, v.y - m_pixelOffset.y, viewport);

    // Find out where the ray reaches the specified depth.
    const Vector3& out = ray.origin() + ray.direction() * -z / (ray.direction().dot(m_cframe.lookVector()));

    return out;
}


float GCamera::worldToScreenSpaceArea(float area, float z, const Rect2D& viewport) const {
    (void)viewport;
    if (z >= 0) {
        return finf();
    }
    return area * (float)square(imagePlaneDepth() / z);
}


void GCamera::getClipPlanes(
    const Rect2D&       viewport,
    Array<Plane>&       clip) const {

    Frustum fr;
    frustum(viewport, fr);
    clip.resize(fr.faceArray.size(), DONT_SHRINK_UNDERLYING_ARRAY);
    for (int f = 0; f < clip.size(); ++f) {
        clip[f] = fr.faceArray[f].plane;
    }
}
 

GCamera::Frustum GCamera::frustum(const Rect2D& viewport) const {
    Frustum f;
    frustum(viewport, f);
    return f;
}


void GCamera::frustum(const Rect2D& viewport, Frustum& fr) const {

    // The volume is the convex hull of the vertices definining the view
    // frustum and the light source point at infinity.

    const float x               = viewportWidth(viewport) / 2;
    const float y               = viewportHeight(viewport) / 2;
    const float zn              = m_nearPlaneZ;
    const float zf              = m_farPlaneZ;
    float xx, zz, yy;

    float halfFOV = m_fieldOfView * 0.5f;

    // This computes the normal, which is based on the complement of the 
    // halfFOV angle, so the equations are "backwards"
    if (m_direction == VERTICAL) {
        yy = -cosf(halfFOV);
        xx = yy * viewport.height() / viewport.width();
        zz = -sinf(halfFOV);
    } else {
        xx = -cosf(halfFOV);
        yy = xx * viewport.width() / viewport.height();
        zz = -sinf(halfFOV);
    } 

    // Near face (ccw from UR)
    fr.vertexPos.append(
        Vector4( x,  y, zn, 1),
        Vector4(-x,  y, zn, 1),
        Vector4(-x, -y, zn, 1),
        Vector4( x, -y, zn, 1));

    // Far face (ccw from UR, from origin)
    if (m_farPlaneZ == -finf()) {
        fr.vertexPos.append(Vector4( x,  y, zn, 0),
                            Vector4(-x,  y, zn, 0),
                            Vector4(-x, -y, zn, 0),
                            Vector4( x, -y, zn, 0));
    } else {
        // Finite
        const float s = zf / zn;
        fr.vertexPos.append(Vector4( x * s,  y * s, zf, 1),
                            Vector4(-x * s,  y * s, zf, 1),
                            Vector4(-x * s, -y * s, zf, 1),
                            Vector4( x * s, -y * s, zf, 1));
    }

    Frustum::Face face;

    // Near plane (wind backwards so normal faces into frustum)
    // Recall that nearPlane, farPlane are positive numbers, so
    // we need to negate them to produce actual z values.
    face.plane = Plane(Vector3(0,0,-1), Vector3(0,0,m_nearPlaneZ));
    face.vertexIndex[0] = 3;
    face.vertexIndex[1] = 2;
    face.vertexIndex[2] = 1;
    face.vertexIndex[3] = 0;
    fr.faceArray.append(face);

    // Right plane
    face.plane = Plane(Vector3(xx, 0, zz), Vector3::zero());
    face.vertexIndex[0] = 0;
    face.vertexIndex[1] = 4;
    face.vertexIndex[2] = 7;
    face.vertexIndex[3] = 3;
    fr.faceArray.append(face);

    // Left plane
    face.plane = Plane(Vector3(-fr.faceArray.last().plane.normal().x, 0, fr.faceArray.last().plane.normal().z), Vector3::zero());
    face.vertexIndex[0] = 5;
    face.vertexIndex[1] = 1;
    face.vertexIndex[2] = 2;
    face.vertexIndex[3] = 6;
    fr.faceArray.append(face);

    // Top plane
    face.plane = Plane(Vector3(0, yy, zz), Vector3::zero());
    face.vertexIndex[0] = 1;
    face.vertexIndex[1] = 5;
    face.vertexIndex[2] = 4;
    face.vertexIndex[3] = 0;
    fr.faceArray.append(face);

    // Bottom plane
    face.plane = Plane(Vector3(0, -fr.faceArray.last().plane.normal().y, fr.faceArray.last().plane.normal().z), Vector3::zero());
    face.vertexIndex[0] = 2;
    face.vertexIndex[1] = 3;
    face.vertexIndex[2] = 7;
    face.vertexIndex[3] = 6;
    fr.faceArray.append(face);

    // Far plane
    if (-m_farPlaneZ < finf()) {
    	face.plane = Plane(Vector3(0, 0, 1), Vector3(0, 0, m_farPlaneZ));
        face.vertexIndex[0] = 4;
        face.vertexIndex[1] = 5;
        face.vertexIndex[2] = 6;
        face.vertexIndex[3] = 7;
        fr.faceArray.append(face);
    }

    // Transform vertices to world space
    for (int v = 0; v < fr.vertexPos.size(); ++v) {
        fr.vertexPos[v] = m_cframe.toWorldSpace(fr.vertexPos[v]);
    }

    // Transform planes to world space
    for (int p = 0; p < fr.faceArray.size(); ++p) {
        // Since there is no scale factor, we don't have to 
        // worry about the inverse transpose of the normal.
        Vector3 normal;
        float d;
        
        fr.faceArray[p].plane.getEquation(normal, d);
        
        Vector3 newNormal = m_cframe.rotation * normal;
        
        if (isFinite(d)) {
            d = (newNormal * -d + m_cframe.translation).dot(newNormal);
            fr.faceArray[p].plane = Plane(newNormal, newNormal * d);
        } else {
            // When d is infinite, we can't multiply 0's by it without
            // generating NaNs.
            fr.faceArray[p].plane = Plane::fromEquation(newNormal.x, newNormal.y, newNormal.z, d);
        }
    }
}

void GCamera::getNearViewportCorners
(const Rect2D& viewport,
 Vector3&      outUR,
 Vector3&      outUL,
 Vector3&      outLL,
 Vector3&      outLR) const {
    
    // Must be kept in sync with getFrustum()
    const float w  = viewportWidth(viewport) / 2.0f;
    const float h  = viewportHeight(viewport) / 2.0f;
    const float z  = nearPlaneZ();

    // Compute the points
    outUR = Vector3( w,  h, z);
    outUL = Vector3(-w,  h, z);
    outLL = Vector3(-w, -h, z);
    outLR = Vector3( w, -h, z);

    // Take to world space
    outUR = m_cframe.pointToWorldSpace(outUR);
    outUL = m_cframe.pointToWorldSpace(outUL);
    outLR = m_cframe.pointToWorldSpace(outLR);
    outLL = m_cframe.pointToWorldSpace(outLL);
}

void GCamera::getFarViewportCorners(
    const Rect2D& viewport,
    Vector3& outUR,
    Vector3& outUL,
    Vector3& outLL,
    Vector3& outLR) const {

    // Must be kept in sync with getFrustum()
    const float w = viewportWidth(viewport) * m_farPlaneZ / m_nearPlaneZ;
    const float h = viewportHeight(viewport) * m_farPlaneZ / m_nearPlaneZ;
    const float z = m_farPlaneZ;
    
    // Compute the points
    outUR = Vector3( w/2,  h/2, z);
    outUL = Vector3(-w/2,  h/2, z);
    outLL = Vector3(-w/2, -h/2, z);
    outLR = Vector3( w/2, -h/2, z);

    // Take to world space
    outUR = m_cframe.pointToWorldSpace(outUR);
    outUL = m_cframe.pointToWorldSpace(outUL);
    outLR = m_cframe.pointToWorldSpace(outLR);
    outLL = m_cframe.pointToWorldSpace(outLL);
}



void GCamera::setPosition(const Vector3& t) { 
    m_cframe.translation = t;
}


void GCamera::lookAt(const Vector3& position, const Vector3& up) { 
    m_cframe.lookAt(position, up);
}


void GCamera::serialize(BinaryOutput& bo) const {
    bo.writeFloat32(m_fieldOfView);
    bo.writeFloat32(imagePlaneDepth());
    debugAssert(nearPlaneZ() < 0.0f);
    bo.writeFloat32(nearPlaneZ());
    debugAssert(farPlaneZ() < 0.0f);
    bo.writeFloat32(farPlaneZ());
    m_cframe.serialize(bo);
    bo.writeInt8(m_direction);
    m_pixelOffset.serialize(bo);
}


void GCamera::deserialize(BinaryInput& bi) {
    m_fieldOfView = bi.readFloat32();
    m_nearPlaneZ = bi.readFloat32();
    debugAssert(m_nearPlaneZ < 0.0f);
    m_farPlaneZ = bi.readFloat32();
    debugAssert(m_farPlaneZ < 0.0f);
    m_cframe.deserialize(bi);
    m_direction = (FOVDirection)bi.readInt8();
    m_pixelOffset.deserialize(bi);
}


Vector3 GCamera::convertFromUnitToNormal(const Vector3& in, const Rect2D& viewport) const{
    return (in + Vector3(1,1,1)) * 0.5 * Vector3(viewport.width(), -viewport.height(), 1) + 
           Vector3(viewport.x0(), viewport.y1(), 0);
}
} // namespace
