//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      Img3D/Model/PlottableBody.cpp
//! @brief     Implements class PlottableBody.
//!
//! @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/Model/PlottableBody.h"
#include "Base/Const/PhysicalConstants.h"
#include "Img3D/Model/Geometry.h"
#include "Img3D/Model/GeometryStore.h"

using PhysConsts::pi;

namespace {

QQuaternion EulerToQuaternion(const Img3D::F3& euler)
{
    const float cpsi2 = std::cos(euler.x() / 2.0f);
    const float spsi2 = std::sin(euler.x() / 2.0f);
    const float cth2 = std::cos(euler.y() / 2.0f);
    const float sth2 = std::sin(euler.y() / 2.0f);
    const float cphi2 = std::cos(euler.z() / 2.0f);
    const float sphi2 = std::sin(euler.z() / 2.0f);
    const float a = cphi2 * cth2 * cpsi2 - sphi2 * cth2 * spsi2;
    const float b = cphi2 * cpsi2 * sth2 + sphi2 * sth2 * spsi2;
    const float c = cphi2 * sth2 * spsi2 - sphi2 * cpsi2 * sth2;
    const float d = cphi2 * cth2 * spsi2 + cth2 * cpsi2 * sphi2;
    return QQuaternion{a, b, c, d};
}

Img3D::F3 QuaternionToEuler(const QQuaternion& q)
{
    auto qvec = q.toVector4D();

    const float a = qvec.w(); // scalar part of quaternion
    const float b = qvec.x();
    const float c = qvec.y();
    const float d = qvec.z();

    const float term1 = std::atan(d / a);
    const float term2 = b == 0 ? static_cast<float>((pi / 2)) : std::atan(c / b);

    const float x = term1 + term2;
    const float y = 2 * std::atan(std::sqrt((b * b + c * c) / (a * a + d * d)));
    const float z = term1 - term2;

    return {x, y, z};
}

} // namespace


namespace Img3D {

#ifdef Q_OS_LINUX
QColor const clrObject = Qt::lightGray;
#else
QColor const clrObject = Qt::black;
#endif

PlottableBody::PlottableBody(GeometricID::Key gky_, const double2d_t* top, const double2d_t* bottom,
                             bool drawBottom)
    : isNull(false)
    , m_color(clrObject)
    , gky(gky_)
{
    if (top || bottom)
        m_geo = std::make_shared<Geometry>(gky, top, bottom, drawBottom);
}

PlottableBody::~PlottableBody()
{
    releaseGeometry();
}

bool PlottableBody::isTransparent() const
{
    return color().alpha() < 255;
}

void PlottableBody::transform(F3 scale, F3 rotate, F3 translate)
{
    m_matrix.setToIdentity();
    m_matrix.translate(translate);
    m_matrix.rotate(EulerToQuaternion(rotate));
    m_matrix.scale(scale);
}

void PlottableBody::transform(F3 turn, F3 scale, F3 rotate, F3 translate)
{
    // 1. turn to align with x/y/z as needed
    // 2. scale to desired x/y/z size
    // 3. rotate as needed by the scene
    // 4. move to the position
    m_matrix.setToIdentity();
    m_matrix.translate(translate);
    m_matrix.rotate(EulerToQuaternion(rotate));
    m_matrix.scale(scale);
    m_matrix.rotate(EulerToQuaternion(turn));
}

// This method allows the addition of an extrinsic global rotation to an object i.e.
// it rotates the object about the global origin of the coordinate system
void PlottableBody::addExtrinsicRotation(F3 turn, F3 scale, F3& rotate, F3 rotateExtrinsic,
                                         F3& translate)
{
    m_matrix.setToIdentity();
    m_matrix.rotate(EulerToQuaternion(rotateExtrinsic));
    m_matrix.translate(translate);
    m_matrix.rotate(EulerToQuaternion(rotate));
    m_matrix.scale(scale);
    m_matrix.rotate(EulerToQuaternion(turn));

    // first apply the particle's intrinsic and then extrinsic rotations
    QQuaternion q = EulerToQuaternion(rotateExtrinsic) * EulerToQuaternion(rotate);
    rotate = QuaternionToEuler(q);

    // translate the object to the extrinsically rotated translation vector
    translate = EulerToQuaternion(rotateExtrinsic).rotatedVector(translate);
}

void PlottableBody::releaseGeometry()
{
    m_geo.reset();
}

const Geometry& PlottableBody::geo() const
{
    if (!m_geo)
        m_geo = geometryStore().getGeometry(gky);
    return *m_geo;
}

} // namespace Img3D
