#include "perspective_camera.h"

#include <cmath>
#include <cassert>

#include "math/quaternion.h"
#include "math/matrix.h"
#include "math/mathutils.h"


using namespace FireRays;

PerspectiveCamera::PerspectiveCamera(float3 const& eye, float3 const& at, float3 const& up,
                       float2 const& zcap, float fovy, float aspect) 
                       : p_(eye)
                       , zcap_(zcap)
                       , fovy_(fovy)
                       , aspect_(aspect)
{
    // Construct camera frame
    forward_ = normalize(at - eye);
    right_   = cross(forward_, normalize(up));
    up_      = cross(right_, forward_);

    // Image plane parameters
    // tan(fovy/2) = (height / 2) / znear
    // height = 2 * tan(fovy/2) * znear
    // width = aspect * height
    dim_.x = 2 * tanf(fovy * 0.5f) * zcap_.x;
    dim_.y = dim_.x / aspect_;
}

void PerspectiveCamera::GenerateRay(float2 const& sample, ray& r) const
{
    // Transform into [-0.5,0.5]
    float2 hsample = sample - float2(0.5f, 0.5f);
    // Transform into [-dim/2, dim/2]
    float2 csample = hsample * dim_;
    // Direction to image plane
    r.d = normalize(zcap_.x * forward_ + csample.x * right_ + csample.y * up_);
    // Origin == camera position
    r.o = p_ + zcap_.x * r.d;
    // Zcap == (znear,zfar)
    r.o.w = zcap_.y;
}

// Rotate camera around world Z axis
void PerspectiveCamera::Rotate(float angle)
{
    Rotate(float3(0,1,0), angle);
}

void PerspectiveCamera::Rotate(float3 v, float angle)
{
    /// matrix should have basis vectors in rows
    /// to be used for quaternion construction
    /// would be good to add several options
    /// to quaternion class
    matrix cam_matrix = matrix(
                               up_.x, up_.y, up_.z, 0,
                               right_.x, right_.y, right_.z, 0,
                               forward_.x, forward_.y, forward_.z, 0,
                                 0,   0,   0, 1);

    quaternion q = normalize(quaternion(cam_matrix));

    q = q * rotation_quaternion(v, -angle);

    q.to_matrix(cam_matrix);

    up_ = normalize(float3(cam_matrix.m00, cam_matrix.m01, cam_matrix.m02));
    right_ = normalize(float3(cam_matrix.m10, cam_matrix.m11, cam_matrix.m12));
    forward_ = normalize(float3(cam_matrix.m20, cam_matrix.m21, cam_matrix.m22));
}

// Tilt camera
void PerspectiveCamera::Tilt(float angle)
{
    Rotate(right_, angle);
}

// Move along camera Z direction
void PerspectiveCamera::MoveForward(float distance)
{
    p_ += distance * forward_;
}
