//=============================================================================
// Copyright (C) 2011-2017 The pmp-library developers
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice, this
//   list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright notice,
//   this list of conditions and the following disclaimer in the documentation
//   and/or other materials provided with the distribution.
// * Neither the name of the copyright holder nor the names of its
//   contributors may be used to endorse or promote products derived from
//   this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
//=============================================================================

#include "TrackballViewer.h"
#include <algorithm>

//=============================================================================

namespace pmp {

//=============================================================================

TrackballViewer::TrackballViewer(const char* title, int width, int height,
                                 bool showgui)
    : Window(title, width, height, showgui)
{
    // init mouse buttons
    for (bool& i : m_buttonDown)
        i      = false;
    m_wheelPos = 0;

    // define basic draw modes
    addDrawMode("Wireframe");
    addDrawMode("Solid Flat");
    addDrawMode("Solid Smooth");
    setDrawMode("Solid Smooth");

    // init OpenGL state
    init();
}

//-----------------------------------------------------------------------------

TrackballViewer::~TrackballViewer() = default;

//-----------------------------------------------------------------------------

void TrackballViewer::clearDrawModes()
{
    m_nDrawModes = 0;
    m_drawModeNames.clear();
}

//-----------------------------------------------------------------------------

unsigned int TrackballViewer::addDrawMode(const std::string& _s)
{
    ++m_nDrawModes;
    m_drawModeNames.push_back(_s);

    return m_nDrawModes - 1;
}

//-----------------------------------------------------------------------------

void TrackballViewer::setDrawMode(const std::string& _s)
{
    for (unsigned int i = 0; i < m_drawModeNames.size(); ++i)
    {
        if (m_drawModeNames[i] == _s)
        {
            m_drawMode = i;
            break;
        }
    }
}

//-----------------------------------------------------------------------------

void TrackballViewer::keyboard(int key, int /*code*/, int action, int /*mods*/)
{
    if (action != GLFW_PRESS && action != GLFW_REPEAT)
        return;

    switch (key)
    {
#ifndef __EMSCRIPTEN__
        case GLFW_KEY_ESCAPE:
        case GLFW_KEY_Q:
        {
            exit(0);
            break;
        }
#endif
        case GLFW_KEY_G:
        {
            showImGUI(!showImGUI());
            break;
        }

        case GLFW_KEY_SPACE:
        {
            if (++m_drawMode >= m_nDrawModes)
                m_drawMode   = 0;
            std::string mode = m_drawModeNames[m_drawMode];
            std::cout << "setting draw mode to " << mode << std::endl;
            setDrawMode(mode);
            break;
        }

        case GLFW_KEY_LEFT:
        {
            rotate(vec3(0,1,0), -5.0);
            break;
        }
        case GLFW_KEY_RIGHT:
        {
            rotate(vec3(0,1,0), 5.0);
            break;
        }
        case GLFW_KEY_UP:
        {
            rotate(vec3(1,0,0), -5.0);
            break;
        }
        case GLFW_KEY_DOWN:
        {
            rotate(vec3(1,0,0), 5.0);
            break;
        }
    }
}

//-----------------------------------------------------------------------------

void TrackballViewer::resize(int width, int height)
{
    m_width  = width;
    m_height = height;

    glViewport(0, 0, width, height);
}

//-----------------------------------------------------------------------------

void TrackballViewer::display()
{
    // clear buffers
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // adjust clipping planes to tightly fit bounding sphere
    vec4  mc(m_center, 1.0);
    vec4  ec = m_modelviewMatrix * mc;
    float z  = -ec[2];
    m_near   = 0.01 * m_radius;
    m_far    = 10.0 * m_radius;
    m_fovy   = 45.0;
    m_near   = std::max(0.001f * m_radius, z - m_radius);
    m_far    = std::max(0.002f * m_radius, z + 2*m_radius);

    // update projection matrix
    m_projectionMatrix = perspectiveMatrix(
        m_fovy, (float)m_width / (float)m_height, m_near, m_far);

    // draw the scene in current draw mode
    if (m_drawMode < m_drawModeNames.size())
        draw(m_drawModeNames[m_drawMode]);
    else
        draw("");
}

//-----------------------------------------------------------------------------

void TrackballViewer::mouse(int button, int action, int mods)
{
    // record current modifier keys
    m_modifiers = mods;

    // mouse press
    if (action == GLFW_PRESS)
    {
        double x, y;
        cursorPos(x,y);
        m_lastPoint2D = ivec2(x, y);
        m_lastPointOk        = mapToSphere(m_lastPoint2D, m_lastPoint3D);
        m_buttonDown[button] = true;

        // set rotation center
        if (m_modifiers == GLFW_MOD_CONTROL)
        {
            double x, y;
            cursorPos(x, y);
            flyTo(x, y);
        }
    }

    // mouse release
    else
    {
        m_lastPointOk        = false;
        m_buttonDown[button] = false;
    }
}

//-----------------------------------------------------------------------------

void TrackballViewer::scroll(double /*xoffset*/, double yoffset)
{
    m_wheelPos += yoffset;
    float d = -(float)yoffset * 0.12 * m_radius;
#ifdef __EMSCRIPTEN__
    d *= 0.5; // scrolling in browser is faster
#endif
    translate(vec3(0.0, 0.0, d));
}

//-----------------------------------------------------------------------------

void TrackballViewer::motion(double xpos, double ypos)
{
    // zoom
    if ((m_buttonDown[GLFW_MOUSE_BUTTON_MIDDLE]) ||
        (m_buttonDown[GLFW_MOUSE_BUTTON_LEFT] &&
         (m_modifiers == GLFW_MOD_SHIFT)))
    {
        zoom(xpos, ypos);
    }

    // translation
    else if (m_buttonDown[GLFW_MOUSE_BUTTON_RIGHT] ||
             (m_buttonDown[GLFW_MOUSE_BUTTON_LEFT] &&
              (m_modifiers == GLFW_MOD_ALT)))
    {
        translation(xpos, ypos);
    }

    // rotation
    else if (m_buttonDown[GLFW_MOUSE_BUTTON_LEFT])
    {
        rotation(xpos, ypos);
    }

    // remember points
    m_lastPoint2D = ivec2(xpos, ypos);
    m_lastPointOk = mapToSphere(m_lastPoint2D, m_lastPoint3D);
}

//-----------------------------------------------------------------------------

void TrackballViewer::init()
{
    // set initial state
    glClearColor(1.0, 1.0, 1.0, 1.0);
    glEnable(GL_DEPTH_TEST);

    // init modelview
    m_modelviewMatrix = mat4::identity();

// turn on multi-sampling to anti-alias lines
#ifndef __EMSCRIPTEN__
    glEnable(GL_MULTISAMPLE);
    GLint n_samples;
    glGetIntegerv(GL_SAMPLES, &n_samples);
    std::cout << "Multi-sampling uses " << n_samples << " per pixel\n";
#endif
}

//-----------------------------------------------------------------------------

void TrackballViewer::setScene(const vec3& center, float radius)
{
    m_center = center;
    m_radius = radius;
    viewAll();
}

//-----------------------------------------------------------------------------

void TrackballViewer::viewAll()
{
    vec4 c = vec4(m_center, 1.0);
    vec4 t = m_modelviewMatrix * c;
    translate(vec3(-t[0], -t[1], -t[2] - 2.5 * m_radius));
}

//-----------------------------------------------------------------------------

bool TrackballViewer::pick(int x, int y, vec3& result)
{
#ifndef __EMSCRIPTEN__ // WebGL cannot read depth buffer

    // get viewport data
    GLint viewport[4];
    glGetIntegerv(GL_VIEWPORT, viewport);

    // take into accout highDPI scaling
    x *= m_scaling;
    y *= m_scaling;

    // in OpenGL y=0 is at the 'bottom'
    y = viewport[3] - y;

    // read depth buffer value at (x, y_new)
    float zf;
    glReadPixels(x, y, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, &zf);

    if (zf != 1.0f)
    {
        float xf = ((float)x - (float) viewport[0]) / ((float) viewport[2]) * 2.0f - 1.0f;
        float yf = ((float)y - (float) viewport[1]) / ((float) viewport[3]) * 2.0f - 1.0f;
        zf = zf * 2.0f - 1.0f;

        mat4 mvp  = m_projectionMatrix * m_modelviewMatrix;
        mat4 inv  = inverse(mvp);
        vec4 p    = inv * vec4(xf, yf, zf, 1.0f);
        p /= p[3];

        result = vec3(p[0], p[1], p[2]);

        return true;
    }

#endif

    return false;
}

//-----------------------------------------------------------------------------

void TrackballViewer::flyTo(int x, int y)
{
    vec3 p;
    if (pick(x,y,p))
    {
        m_center = p;
        vec4 c = vec4(m_center, 1.0);
        vec4 t = m_modelviewMatrix * c;
        translate(vec3(-t[0], -t[1], -0.5*t[2]));
    }
}

//-----------------------------------------------------------------------------

bool TrackballViewer::mapToSphere(const ivec2& point2D, vec3& result)
{
    if ((point2D[0] >= 0) && (point2D[0] <= m_width) && (point2D[1] >= 0) &&
        (point2D[1] <= m_height))
    {
        double x    = (double)(point2D[0] - 0.5 * m_width) / (double)m_width;
        double y    = (double)(0.5 * m_height - point2D[1]) / (double)m_height;
        double sinx = sin(M_PI * x * 0.5);
        double siny = sin(M_PI * y * 0.5);
        double sinx2siny2 = sinx * sinx + siny * siny;

        result[0] = sinx;
        result[1] = siny;
        result[2] = sinx2siny2 < 1.0 ? sqrt(1.0 - sinx2siny2) : 0.0;

        return true;
    }
    else
        return false;
}

//-----------------------------------------------------------------------------

void TrackballViewer::rotation(int x, int y)
{
    if (m_lastPointOk)
    {
        ivec2 newPoint2D;
        vec3  newPoint3D;
        bool  newPointok;

        newPoint2D = ivec2(x, y);
        newPointok = mapToSphere(newPoint2D, newPoint3D);

        if (newPointok)
        {
            vec3  axis     = cross(m_lastPoint3D, newPoint3D);
            float cosAngle = dot(m_lastPoint3D, newPoint3D);

            if (fabs(cosAngle) < 1.0)
            {
                float angle = 2.0 * acos(cosAngle) * 180.0 / M_PI;
                rotate(axis, angle);
            }
        }
    }
}

//-----------------------------------------------------------------------------

void TrackballViewer::translation(int x, int y)
{
    float dx = x - m_lastPoint2D[0];
    float dy = y - m_lastPoint2D[1];

    vec4  mc = vec4(m_center, 1.0);
    vec4  ec = m_modelviewMatrix * mc;
    float z  = -(ec[2] / ec[3]);

    float aspect = (float)m_width / (float)m_height;
    float up     = tan(m_fovy / 2.0f * M_PI / 180.f) * m_near;
    float right  = aspect * up;

    translate(vec3(2.0 * dx / m_width * right / m_near * z,
                   -2.0 * dy / m_height * up / m_near * z, 0.0f));
}

//-----------------------------------------------------------------------------

void TrackballViewer::zoom(int, int y)
{
    float dy = y - m_lastPoint2D[1];
    float h  = m_height;
    translate(vec3(0.0, 0.0, m_radius * dy * 3.0 / h));
}

//-----------------------------------------------------------------------------

void TrackballViewer::translate(const vec3& t)
{
    m_modelviewMatrix = translationMatrix(t) * m_modelviewMatrix;
}

//-----------------------------------------------------------------------------

void TrackballViewer::rotate(const vec3& axis, float angle)
{
    // center in eye coordinates
    vec4 mc = vec4(m_center, 1.0);
    vec4 ec = m_modelviewMatrix * mc;
    vec3 c(ec[0] / ec[3], ec[1] / ec[3], ec[2] / ec[3]);

    m_modelviewMatrix = translationMatrix(c) * rotationMatrix(axis, angle) *
                        translationMatrix(-c) * m_modelviewMatrix;
}

//=============================================================================
} // namespace pmp
//=============================================================================
