// *********************************************************
// OpenGL Viewer Class, based on LibQGLViewer, compatible
// with most hardware (OpenGL 1.2).
// Author : Tamy Boubekeur (boubek@gmail.com).
// Copyright (C) 2012 Tamy Boubekeur.
// All rights reserved.
// *********************************************************

#include "GLViewer.h"

#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <cassert>
#include <string>
#include <QMouseEvent>
#include "Color.h"
#include "Light.h"
#include "Object.h"
#include "Window.h"
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>

using namespace std;

static const GLuint OpenGLLightID[] = {GL_LIGHT0, GL_LIGHT1, GL_LIGHT2, GL_LIGHT3, GL_LIGHT4, GL_LIGHT5, GL_LIGHT6, GL_LIGHT7};

GLViewer::GLViewer (Window *win) : QGLViewer (), wnd(win), cmode(Move), showLights(false)
{
    wireframe = false;
    renderingMode = Smooth;

    // Load light mesh
    Assimp::Importer importer;

    const aiScene* lightModel = importer.ReadFile("models/light.3ds",
        aiProcess_CalcTangentSpace       |
        aiProcess_Triangulate            |
        aiProcess_JoinIdenticalVertices  |
        aiProcess_SortByPType);
    if(lightModel && lightModel->mNumMeshes > 0)
    {
        const aiMesh* mesh = lightModel->mMeshes[0];

        for(unsigned int vertexIndex = 0; vertexIndex < mesh->mNumVertices; vertexIndex++)
        {
            float x = mesh->mVertices[vertexIndex].x;
            float y = mesh->mVertices[vertexIndex].y;
            float z = mesh->mVertices[vertexIndex].z;

            lightMesh.addVertex(Vertex(Vec3Df(x, y, z), Vec3Df(1.0, 0.0, 0.0)));
        }

        for(unsigned int faceIndex = 0; faceIndex < mesh->mNumFaces; faceIndex++)
        {
            int i1 = mesh->mFaces[faceIndex].mIndices[0];
            int i2 = mesh->mFaces[faceIndex].mIndices[1];
            int i3 = mesh->mFaces[faceIndex].mIndices[2];

            lightMesh.addTriangle(Triangle(i1, i2, i3));
        }

        lightMesh.updateBoundingBox();
        lightMesh.recomputeSmoothVertexNormals(0);
    }
    else
        printf("Failed to load light mesh\n");
}

GLViewer::~GLViewer ()
{
}

void GLViewer::showSceneLights(bool l)
{
    showLights = l;
    updateGL();
}

void GLViewer::drawBox(Vec3Df min, Vec3Df max, Color c) const
{
    float dX = max[0] - min[0];
    float dY = max[1] - min[1];
    float dZ = max[2] - min[2];

    glDisable (GL_LIGHTING);

    glBegin (GL_LINE_LOOP);
    glColor3f(c.redF(), c.greenF(), c.blueF());
    glVertex3f(min[0],min[1],min[2]);
    glVertex3f(min[0],min[1]+dY,min[2]);
    glVertex3f(min[0]+dX,min[1]+dY,min[2]);
    glVertex3f(min[0]+dX,min[1],min[2]);
    glEnd ();

    glBegin (GL_LINE_LOOP);
    glColor3f(c.redF(), c.greenF(), c.blueF());
    glVertex3f(min[0]+dX,min[1],min[2]+dZ);
    glVertex3f(min[0]+dX,min[1]+dY,min[2]+dZ);
    glVertex3f(min[0],min[1]+dY,min[2]+dZ);
    glVertex3f(min[0],min[1],min[2]+dZ);
    glEnd ();

    glBegin (GL_LINE_LOOP);
    glColor3f(c.redF(), c.greenF(), c.blueF());
    glVertex3f(min[0],min[1]+dY,min[2]);
    glVertex3f(min[0]+dX,min[1]+dY,min[2]);
    glVertex3f(min[0]+dX,min[1]+dY,min[2]+dZ);
    glVertex3f(min[0],min[1]+dY,min[2]+dZ);
    glEnd ();

    glBegin (GL_LINE_LOOP);
    glColor3f(c.redF(), c.greenF(), c.blueF());
    glVertex3f(min[0],min[1],min[2]);
    glVertex3f(min[0]+dX,min[1],min[2]);
    glVertex3f(min[0]+dX,min[1],min[2]+dZ);
    glVertex3f(min[0],min[1],min[2]+dZ);
    glEnd ();

    glEnable (GL_LIGHTING);
}

void GLViewer::setWireframe (bool b) {
    wireframe = b;
    if (wireframe)
        glPolygonMode (GL_FRONT_AND_BACK, GL_LINE);
    else
        glPolygonMode (GL_FRONT_AND_BACK, GL_FILL);
    updateGL ();
}

void GLViewer::setRenderingMode (RenderingMode m) {
    renderingMode = m;
    updateGL ();
}

void GLViewer::setDisplayMode (DisplayMode m) {
    displayMode = m;
    updateGL ();
}

QString GLViewer::helpString() const
{
  QString text("<h2>Raymini</h2>");
  text += "Author: <b>Tamy Boubekeur</b> (boubek@gmail.com)<br>Version: 0.1<br<br>";
  text += "<h3>Disclaimer</h3>";
  text += "This code is not bug-free, use it at your own risks.";
  text += "<h3>Controls</h3>";
  text += "Use the right control panel to setup rendering options.";
  text += "You can respectively zoom and translate with the left and middle mouse buttons. ";
  text += "Pressing <b>Alt</b> and one of the function keys (<b>F1</b>..<b>F12</b>) defines a camera keyFrame. ";
  text += "Simply press the function key again to restore it. Several keyFrames define a ";
  text += "camera path. Paths are saved when you quit the application and restored at next start.<br><br>";
  text += "Press <b>F</b> to display the frame rate, <b>A</b> for the world axis, ";
  text += "<b>Alt+Return</b> for full screen mode and <b>Control+S</b> to save a snapshot. ";
  text += "See the <b>Keyboard</b> tab in this window for a complete shortcut list.<br><br>";
  text += "Double clicks automates single click actions: A left button double click aligns the closer axis with the camera (if close enough). ";
  text += "A middle button double click fits the zoom of the camera and the right button re-centers the scene.<br><br>";
  text += "A left button double click while holding right button pressed defines the camera <i>Revolve Around Point</i>. ";
  text += "See the <b>Mouse</b> tab and the documentation web pages for details.<br><br>";
  text += "Press <b>Escape</b> to exit the viewer.";
  return text;
}

void GLViewer::keyPressEvent (QKeyEvent*) {
}

void GLViewer::keyReleaseEvent (QKeyEvent * /*event*/) {

}

void GLViewer::mousePressEvent (QMouseEvent * event)
{
    if(cmode == Focus)
         wnd->setClickFocusDistance (event->pos().x(), event->pos().y());
    else if(cmode == Select)
    {
        SRenderParameters rp = wnd->initRenderParameters();
        rp.cam.up *= -1;

        float nearSizeY = 2 * tan(rp.cam.fov / 2);
        float nearSizeX = nearSizeY * rp.cam.aspect;
        float xRatio = event->pos().x() / float(rp.screen.width);
        float yRatio = event->pos().y() / float(rp.screen.height);
        Vec3Df nearOrigin = rp.cam.dir + rp.cam.up * nearSizeY / 2.0f - rp.cam.right * nearSizeX / 2.0f;
        Vec3Df rayDir = nearOrigin + xRatio * nearSizeX * rp.cam.right - yRatio * nearSizeY * rp.cam.up;
        Ray ray(rp.cam.pos, rayDir);
        IntersectionResult intersection;

        if(Scene::getInstance()->intersect(ray, intersection))
            wnd->setSelectedObject(const_cast<Object*>(intersection.obj));
        else
            wnd->setSelectedObject(0);
    }
    else if(cmode == Move)
    {
        setDisplayMode (OpenGLDisplayMode);
        QGLViewer::mousePressEvent(event);
    }
    cmode = Move;
    wnd->resetDofObjButtons();
}


void GLViewer::wheelEvent (QWheelEvent * e) {
    setDisplayMode (OpenGLDisplayMode);
    QGLViewer::wheelEvent (e);
}

// -----------------------------------------------
// Drawing functions
// -----------------------------------------------

void GLViewer::init() {
    glClearColor (0.f, 0.f, 0.f, 0.0);
    glCullFace (GL_BACK);
    glEnable (GL_CULL_FACE);
    glDepthFunc (GL_LEQUAL);
    glHint (GL_POLYGON_SMOOTH_HINT, GL_NICEST);
    glEnable (GL_POINT_SMOOTH);

    Scene * scene = Scene::getInstance ();

    glLoadIdentity ();

    //glDisable (GL_LIGHTING);
    glEnable (GL_LIGHTING);
    for (unsigned int i = 0; i < scene->getLightNb() && i < 8; i++) {
        GLuint glID = OpenGLLightID[i];
        glEnable (glID);
        const Light& light = scene->getLightRef(i);
        const Vec3Df & p = light.getPos ();
        float intensity = light.getIntensity ();
        const Colorf& c = intensity * light.getColor();
        GLfloat glPos[4] = {p[0], p[1], p[2], 0};
        GLfloat glColor[4] = {c.r, c.g, c.b, 0};
        glLightfv (glID, GL_POSITION, glPos);
        glLightfv (glID, GL_DIFFUSE, glColor);
    }

    const BoundingBox & sceneBBox = scene->getBoundingBox ();
    Vec3Df c = sceneBBox.getCenter ();

    setSceneCenter (qglviewer::Vec (c[0], c[1], c[2]));
    setSceneRadius (scene->getRadius());
    showEntireScene ();

    // Replace the camera to focus on the objects, not the whole scene
    float radius = sceneBBox.getRadius();
    const float yview = radius / sin(camera()->fieldOfView()/2.0);
    const float xview = radius / sin(camera()->horizontalFieldOfView()/2.0);
    qglviewer::Vec cpos = camera()->position();
    cpos.normalize();
    cpos *= std::max(xview, yview);
    camera()->setPosition(cpos);
}

void GLViewer::draw ()
{
    if (displayMode == RayDisplayMode) {
        glDrawPixels (rayImage.width(),
                      rayImage.height(),
                      Color::format,
                      Color::type,
                      rayImage.bits ());
        return;
    }
    Scene * scene = Scene::getInstance ();

    if(showLights)
    {
        glDisable(GL_LIGHTING);
        glDisable(GL_CULL_FACE);
        for(unsigned int i = 0; i < scene->getLightNb(); i++)
        {
            const Light& l = scene->getLightRef(i);
            const Vec3Df& trans = l.getPos();
            const Colorf& c = l.getColor();
            float extent = l.getExtent();
            glPushMatrix();
            glTranslatef(trans[0], trans[1], trans[2]);
            glScalef(extent, extent, extent);
            static const GLfloat glMatDiff[4] = {c.r, c.g, c.b, 1};

            glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, glMatDiff);
            glDisable (GL_COLOR_MATERIAL);
            lightMesh.renderGL(false);
            glPopMatrix ();
        }
        glEnable(GL_CULL_FACE);
        glEnable(GL_LIGHTING);
    }

    for (unsigned int i = 0; i < scene->getObjectNb(); i++)
    {
        const Object & o = scene->getObjectRef(i);
        //o.getMesh().getTree().draw(o.getBoundingBox(), this, o.getTrans());
        const Vec3Df & trans = o.getTrans ();
        glPushMatrix ();
        glTranslatef (trans[0], trans[1], trans[2]);
        const Material& mat = o.getMaterial();
        const Color& color = mat.getColor ();
        float dif = mat.getDiffuse ();
        float spec = mat.getSpecular ();
        static GLfloat glMatDiff[4];
        static GLfloat glMatSpec[4];
        static const GLfloat glMatAmb[4] = {0.f, 0.f, 0.f, 1.f};
        for (unsigned int j = 0; j < 3; j++)
        {
            glMatDiff[j] = dif * color.atF(j);
            glMatSpec[j] = spec;
        }
        glMatDiff[3] = 1.0f;
        glMatSpec[3] = 1.0f;

        glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, glMatDiff);
        glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, glMatSpec);
        glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, glMatAmb);
        glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, 128);
        glDisable (GL_COLOR_MATERIAL);
        o.getMesh ().renderGL (renderingMode == Flat);

        if(o.isSelected())
        {
            glMatDiff[0] = glMatDiff[1] = glMatDiff[2] = glMatDiff[3] = 1;
            glMatSpec[0] = glMatSpec[1] = glMatSpec[2] = glMatSpec[3] = 1;

            glDisable (GL_LIGHTING);
            glPolygonMode (GL_FRONT_AND_BACK, GL_LINE);
            glPolygonOffset(0.1f, 0.1f);
            glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, glMatDiff);
            glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, glMatSpec);

            o.getMesh().renderGL(renderingMode == Flat);

            glPolygonOffset(0, 0);
            glPolygonMode (GL_FRONT_AND_BACK, wireframe ? GL_LINE : GL_FILL);
            glEnable (GL_LIGHTING);
        }

        glPopMatrix ();
    }
}

void GLViewer::setRayImage (const QImage & image)
{
    rayImage = image;
}



