//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      Img3D/View/Camera.cpp
//! @brief     Implements class Camera.
//!
//! @homepage  http://www.bornagainproject.org
//! @license   GNU General Public License v3 or higher (see COPYING)
//! @copyright Forschungszentrum Jülich GmbH 2018
//! @authors   Scientific Computing Group at MLZ (see CITATION, AUTHORS)
//
//  ************************************************************************************************

#include "Img3D/View/Camera.h"

namespace Img3D {

Camera::Camera()
    : pos({0, 0, 1}, {0, 0, 0}, {1, 0, 0})
    , pos3DAxes({0, 0, 1}, {0, 0, 0}, {1, 0, 0})
    , zoom(1)
    , vertAngle(60)
    , nearPlane(1)
    , farPlane(10000)
    , lightPos1(500.f, 1000.f, 1000.f)
    , lightPosRotated1(lightPos1)
{
    setAspectRatio(1);
}

void Camera::lookAt(const CameraParams& pos_)
{
    pos = pos_;
    //    lightPos = pos.eye;
    set();
}

void Camera::lookAt3DAxes(const CameraParams& pos3DAxes_)
{
    pos3DAxes = pos3DAxes_;
    set();
}

// recalculate dependent params
void Camera::set()
{
    // For 3D object
    m_mat_model.setToIdentity();
    m_mat_model.lookAt((pos.eye - pos.ctr) * zoom + pos.ctr, pos.ctr, pos.up);
    QQuaternion rt(pos.rot * addRot);
    m_mat_model.translate(pos.ctr);
    m_mat_model.rotate(rt);
    m_mat_model.translate(-pos.ctr);

    // For 3D axes
    m_mat_model3DAxes.setToIdentity(); //
    m_mat_model3DAxes.lookAt((pos3DAxes.eye - pos3DAxes.ctr) + pos3DAxes.ctr, pos3DAxes.ctr,
                             pos3DAxes.up); //
    QQuaternion rt3DAxes(pos3DAxes.rot * addRot);
    m_mat_model3DAxes.rotate(rt3DAxes);

    lightPosRotated1 = rt.inverted().rotatedVector(lightPos1);

    emit updated(*this);
}

void Camera::setAspectRatio(float ratio)
{
    m_mat_proj.setToIdentity();
    m_mat_proj.perspective(vertAngle, ratio, nearPlane, farPlane);
}

void Camera::zoomBy(float zoom_)
{
    zoom = zoom_;
    set();
}

void Camera::endTransform(bool keep)
{
    if (keep) {
        pos.rot = (pos.rot * addRot).normalized();
        pos.eye = pos.eye * zoom; // TODO limit

        pos3DAxes.rot = (pos3DAxes.rot * addRot).normalized(); // no zooming for 3D axes
    }
    addRot = {};
    zoom = 1;
    set();
}

void Camera::horizontalTurn(float theta)
{
    const CameraParams initial_pos3DAxes = getPos3DAxes();

    const F3 v_eye3DAxes = initial_pos3DAxes.eye; // camera's position vector
    const F3 v_ctr3DAxes = initial_pos3DAxes.ctr;
    const F3 v_up3DAxes = initial_pos3DAxes.up;

    const F3 v_axis3DAxes = v_up3DAxes.normalized(); // normalized rotation axis

    // Rotating camera's position (eye) about up vector
    const F3 v_rot_eye3DAxes =
        v_up3DAxes * (1 - std::cos(theta)) * F3::dotProduct(v_axis3DAxes, v_eye3DAxes)
        + v_eye3DAxes * std::cos(theta)
        + F3::crossProduct(v_axis3DAxes, v_eye3DAxes) * std::sin(theta);

    const CameraParams rotated_pos3DAxes(v_rot_eye3DAxes, v_ctr3DAxes, v_up3DAxes);

    lookAt3DAxes(rotated_pos3DAxes);

    // Horizontal camera turn for 3D object
    const CameraParams initial_pos = getPos();

    const F3 v_eye = initial_pos.eye; // camera's position vector
    const F3 v_ctr = initial_pos.ctr;
    const F3 v_up = initial_pos.up;

    const F3 v_axis = v_up.normalized(); // normalized rotation axis

    // Rotating camera's position (eye) about up vector
    const F3 v_rot_eye = v_up * (1 - std::cos(theta)) * F3::dotProduct(v_axis, v_eye)
                         + v_eye * std::cos(theta)
                         + F3::crossProduct(v_axis, v_eye) * std::sin(theta);

    const CameraParams rotated_pos(v_rot_eye, v_ctr, v_up);

    lookAt(rotated_pos);

    endTransform(true);
}

void Camera::verticalTurn(float theta)
{
    const CameraParams initial_pos3DAxes = getPos3DAxes();

    const F3 v_eye3DAxes = initial_pos3DAxes.eye; // camera's position vector
    const F3 v_ctr3DAxes = initial_pos3DAxes.ctr;
    const F3 v_up3DAxes = initial_pos3DAxes.up;

    const F3 v_axis3DAxes =
        F3::crossProduct(v_up3DAxes, v_eye3DAxes).normalized(); // normalized rotation axis

    // Rotating camera's position (eye) about an axis perpendicular to up and eye vectors
    const F3 v_rot_eye3DAxes =
        v_up3DAxes * (1 - std::cos(theta)) * F3::dotProduct(v_axis3DAxes, v_eye3DAxes)
        + v_eye3DAxes * std::cos(theta)
        + F3::crossProduct(v_axis3DAxes, v_eye3DAxes) * std::sin(theta);

    if (v_rot_eye3DAxes.y() * v_eye3DAxes.y() < 0) {
        endTransform(true);
        return;
    }

    const CameraParams rotated_pos3DAxes(v_rot_eye3DAxes, v_ctr3DAxes, v_up3DAxes);

    lookAt3DAxes(rotated_pos3DAxes);

    // Vertical camera turn for 3D object
    const CameraParams initial_pos = getPos();

    const F3 v_eye = initial_pos.eye; // camera's position vector
    const F3 v_ctr = initial_pos.ctr;
    const F3 v_up = initial_pos.up;

    const F3 v_axis = F3::crossProduct(v_up, v_eye).normalized(); // normalized rotation axis

    // Rotating camera's position (eye) about an axis perpendicular to up and eye vectors
    const F3 v_rot_eye = v_up * (1 - std::cos(theta)) * F3::dotProduct(v_axis, v_eye)
                         + v_eye * std::cos(theta)
                         + F3::crossProduct(v_axis, v_eye) * std::sin(theta);

    const CameraParams rotated_pos(v_rot_eye, v_ctr, v_up);

    lookAt(rotated_pos);

    endTransform(true);
}

} // namespace Img3D
