//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      Img3D/View/Canvas.cpp
//! @brief     Implements class Canvas.
//!
//! @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/Canvas.h"
#include "Base/Util/Assert.h"
#include "Img3D/Model/Geometry.h"
#include "Img3D/Model/GeometryStore.h"
#include "Img3D/Model/Model.h"
#include "Img3D/Plot/AxesPlotter.h"
#include "Img3D/Plot/BodyPlotter.h"
#include "Img3D/View/Camera.h"
#include "Img3D/View/Shader.h"
#include <QMouseEvent>
#include <QVBoxLayout>

namespace {

float ZoomInScale()
{
    if (QSysInfo::productType() == "osx")
        return 1.02f;
    return 1.25f;
}
float ZoomOutScale()
{
    if (QSysInfo::productType() == "osx")
        return 0.98f;
    return 0.8f;
}

// Default camera position in accordance with RealspaceBuilder.h
const float cameraDefaultPosY = -200.0f; // default camera position on Y axis
const float cameraDefaultPosZ = 120.0f;  // default camera position on Z axis

} // namespace


namespace Img3D {

Canvas::Canvas()
    : aspectRatio(1)
    , currentZoomLevel(0)
    , m_model(nullptr)
    , m_is_initializedGL(false)
{
    connect(&geometryStore(), &GeometryStore::deletingGeometry, this, &Canvas::releaseBuffer);

    // layout for caution messages
    setLayout(new QVBoxLayout);
    layout()->setAlignment(Qt::AlignCenter);
}

Canvas::~Canvas()
{
    releaseBuffers();
}

void Canvas::setCamera(Camera* c)
{
    m_camera.reset(c);
    switchCamera(true);
}

void Canvas::setShader(Shader* p)
{
    m_shader.reset(p);
    if (m_shader)
        m_shader->needsInit();
    update();
}

void Canvas::setModel(Model* m)
{
    releaseBuffers();
    m_model = m;
    switchCamera(true);
    ASSERT(m_camera);
    m_camera->set();
}

void Canvas::switchCamera(bool full)
{
    if (m_camera) {
        m_camera->setAspectRatio(aspectRatio);
        if (full && m_model)
            m_camera->lookAt(m_model->defaultCameraPosition);
    }
    update();
}

void Canvas::initializeGL()
{
    setCamera(new Camera);
    setShader(new Shader);

    initializeOpenGLFunctions();
    glEnable(GL_DEPTH_TEST);
    glEnable(GL_CULL_FACE);
    m_is_initializedGL = true;
}

void Canvas::resizeGL(int w, int h)
{
    int w1 = qMax(1, w), h1 = qMax(1, h);
    viewport.setRect(0, 0, w1, h1);
    aspectRatio = float(w1) / float(h1);
    switchCamera(false);
}

void Canvas::paintGL()
{
    glClearColor(1, 1, 1, 1);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    if (!m_camera || !m_shader || !m_model || m_model->modelIsEmpty())
        return;

    m_shader->init();
    m_shader->bind();
    m_shader->setCamera(*m_camera);
    m_shader->setAxis(false);

    // opaque objects
    for (const PlottableBody* o : m_model->objects())
        drawBody(*o);

    // transparent objects
    glEnable(GL_BLEND);
    glDepthMask(false);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    for (const PlottableBody* o : m_model->transparentObjects())
        drawBody(*o);
    glDisable(GL_BLEND);
    glDepthMask(true);

    // Draw 3D coordinate axes in lower left corner
    glViewport(0, 0, viewport.width() / 9, viewport.height() / 5);
    QMatrix4x4 matObject3DAxes;
    matObject3DAxes.setToIdentity(); // 3D axes transformation matrix is Identity
    m_shader->setMatObject(matObject3DAxes);
    m_shader->setMatModel(m_camera->matAxes());
    m_shader->setAxis(true);
    std::unique_ptr<AxesPlotter> buf3DAxes(new AxesPlotter());
    buf3DAxes->draw3DAxes();

    m_shader->release();
}

void Canvas::mousePressEvent(QMouseEvent* e)
{
    switch (e->button()) {
    case Qt::LeftButton:
        mouseButton = btnTURN;
        break;
    case Qt::RightButton:
        mouseButton = btnZOOM;
        break;
    default:
        mouseButton = btnNONE;
        break;
    }

    if (!m_camera)
        return;

    matModel = m_camera->matModel();
    matProj = m_camera->matProj();
    m_last_mouse_coords = e->pos();
}

void Canvas::mouseMoveEvent(QMouseEvent* e)
{
    if (!m_camera)
        return;

    const float delta_x = e->pos().x() - m_last_mouse_coords.x();
    const float delta_y = e->pos().y() - m_last_mouse_coords.y();

    switch (mouseButton) {
    case btnTURN: {
        if (!isInitialized())
            break;

        // minus sign for consistency with Blender; arbitrary prefactor for decent rotation speed
        if (delta_x != 0)
            m_camera->horizontalTurn(-0.007f * delta_x);
        if (delta_y != 0)
            m_camera->verticalTurn(-0.007f * delta_y);

        m_last_mouse_coords = e->pos();
        break;
    }
    case btnZOOM: {
        const float d = (e->y() - m_last_mouse_coords.y()) / float(viewport.height());
        m_camera->zoomBy(1 + d);
        break;
    }
    default:
        break;
    }

    update();
}

void Canvas::mouseReleaseEvent(QMouseEvent*)
{
    if (!m_camera)
        return;

    m_camera->endTransform(true);
    update();
}

void Canvas::wheelEvent(QWheelEvent* e)
{
    if (m_camera) {
        if (e->angleDelta().y() < 0) {
            // Zoom in
            m_camera->zoomBy(ZoomInScale());
            currentZoomLevel += 1;
        } else {
            // Zoom out
            m_camera->zoomBy(ZoomOutScale());
            currentZoomLevel -= 1;
        }
        m_camera->endTransform(true);
        update();
    }
    e->accept(); // disable the event from propagating further to the parent widgets
}

void Canvas::releaseBuffer(Geometry const* g)
{
    delete m_buffers.take(g);
}

void Canvas::releaseBuffers()
{
    for (BodyPlotter* b : m_buffers.values())
        delete b;
    m_buffers.clear();
}

void Canvas::drawBody(const PlottableBody& object)
{
    if (!object.valid())
        return;

    ASSERT(m_shader);
    m_shader->setColor(object.color());
    m_shader->setMatObject(object.matrix());

    Geometry const& geo = object.geo();
    auto it = m_buffers.find(&geo);
    BodyPlotter* buf;
    if (m_buffers.end() == it)
        m_buffers.insert(&geo, buf = new BodyPlotter(geo)); // created on demand
    else
        buf = *it;

    buf->draw();
}

bool Canvas::isInitialized() const
{
    return m_is_initializedGL && m_model != nullptr;
}

void Canvas::defaultView()
{
    // Default view
    if (!isInitialized())
        return;

    const CameraParams defPos({0, cameraDefaultPosY, cameraDefaultPosZ}, // eye
                              {0, 0, 0},                                 // center
                              {0, 0, 1});                                // up

    // Default position of camera for 3D axes and object are the same
    m_camera->lookAt3DAxes(defPos);
    m_camera->lookAt(defPos);
    m_camera->endTransform(true);

    currentZoomLevel = 0; // reset zoom level to default value
    update();
}

void Canvas::sideView()
{
    // Side view at current zoom level
    if (!isInitialized())
        return;

    F3 eye(0, cameraDefaultPosY, 0);

    // Side view 3D axes is zoom scale independent
    m_camera->lookAt3DAxes(CameraParams(eye,         // eye
                                        {0, 0, 0},   // center
                                        {0, 0, 1})); // up

    // Side view 3D object is zoom scale dependent
    if (currentZoomLevel >= 0)
        eye.setY(eye.y() * std::pow(ZoomInScale(), std::abs(currentZoomLevel)));
    else
        eye.setY(eye.y() * std::pow(ZoomOutScale(), std::abs(currentZoomLevel)));

    m_camera->lookAt(CameraParams(eye,         // eye
                                  {0, 0, 0},   // center
                                  {0, 0, 1})); // up

    m_camera->endTransform(true);
    update();
}

void Canvas::topView()
{
    // Top view at current zoom level
    if (!isInitialized())
        return;

    // Setting a tiny offset in y value of eye such that eye and up vectors are not parallel
    Img3D::F3 eye(0, -0.5, -cameraDefaultPosY);

    // Top view 3D axes is zoom scale independent
    m_camera->lookAt3DAxes(CameraParams(eye,         // eye
                                        {0, 0, 0},   // center
                                        {0, 0, 1})); // up

    // Top view 3D object is zoom scale dependent
    if (currentZoomLevel >= 0)
        eye.setZ(eye.z() * std::pow(ZoomInScale(), std::abs(currentZoomLevel)));
    else
        eye.setZ(eye.z() * std::pow(ZoomOutScale(), std::abs(currentZoomLevel)));

    m_camera->lookAt(CameraParams(eye,         // eye
                                  {0, 0, 0},   // center
                                  {0, 0, 1})); // up

    m_camera->endTransform(true);
    update();
}

} // namespace Img3D
