/////////////////////////////////////////////////////////////////////////
//                                                                     //
// This file contains the implementation of the CGLUTContext class.    //
//                                                                     //
/////////////////////////////////////////////////////////////////////////
#include "popassert.hpp"

#include <GL/glut.h>
#include <cmath>

#include "CMatrix3.hpp"
#include "rotationmatrix.hpp"
#include "CLight.hpp"
#include "CSurface.hpp"
#include "CScene.hpp"
#include "CGLUTContext.hpp"




/////////////////////////////////////////////////////////////////////////
//                                                                     //
// Class    : CGLUTContext                                             //
//                                                                     //
// Function : getInstance()                                            //
//                                                                     //
// Comments : This function returns a pointer to the unique instance   //
//            of the class.                                            //
//                                                                     //
/////////////////////////////////////////////////////////////////////////
CGLUTContext* CGLUTContext::getInstance()
{
  static CGLUTContext s_context;
  return &s_context;
}




/////////////////////////////////////////////////////////////////////////
//                                                                     //
// Class    : CGLUTContext                                             //
//                                                                     //
// Function : init()                                                   //
//                                                                     //
// Comments : This function initializes the OpenGL context.            //
//                                                                     //
/////////////////////////////////////////////////////////////////////////
void CGLUTContext::init(int *argc, char** argv, CScene& scene)
{
  m_pScene = &scene;

  glutInit(argc, argv);
  glutInitDisplayMode( GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH | GLUT_STENCIL );
  glutInitWindowSize(scene.m_PropWidth, scene.m_PropHeight);
  glutCreateWindow("GL display");

  initGL();

  glutDisplayFunc(displayFunc);
  glutReshapeFunc(reshapeFunc);
  glutKeyboardFunc(keyboardFunc);
  glutSpecialFunc(specialFunc);

  glMatrixMode(GL_MODELVIEW);


  // Preprocess surfaces
  for (CScene::surface_container::const_iterator it
         = scene.m_vSurfaces.begin();
       it != scene.m_vSurfaces.end();
       ++it)
    {
      (*it)->PreprocessGL();
    }


  // angle of the camera
  //
  //
  //                  lens                      |
  //                    |                       |
  // screen/            |                       y2
  // film               |                       |
  // -+---------+-------+-------+---------------+
  //  |         |<- f ->|<- f ->|
  //  y1                |
  //  |                 |
  //  |
  //
  //  <------ d1 ------>|<--------- d2 --------->
  //
  // From: 1/f   = 1/d1 + 1/d2
  //  and: y1/y2 = d1/d2
  //  and: d1    = DIM_FILM
  //       y2    = (d2/f - 1) * DIM_FILM
  //
  // We need to get atan(y2/d2) for gluPerspective()
  real_t d2 = (scene.m_PropCameraPos - scene.m_PropCameraTarget).norm();
  real_t y2 =
    (d2 / (scene.m_PropFocale*MAKE_REAL(0.001)) - 1) *
    (scene.m_PropDimFilm);
  m_angle = 2*180*atan2(y2/2, d2) / PI;
}




/////////////////////////////////////////////////////////////////////////
//                                                                     //
// Class    : CGLUTContext                                             //
//                                                                     //
// Function : show()                                                   //
//                                                                     //
// Comments : This function actually shows the OpenGL context.         //
//                                                                     //
/////////////////////////////////////////////////////////////////////////
void CGLUTContext::show()
{
  glutMainLoop();
}




/////////////////////////////////////////////////////////////////////////
//                                                                     //
// Class    : CGLUTContext                                             //
//                                                                     //
// Function : CGLUTContext (constructor)                               //
//                                                                     //
// Comments :                                                .         //
//                                                                     //
/////////////////////////////////////////////////////////////////////////
CGLUTContext::CGLUTContext()
  : m_pScene(NULL),
    m_angle(0),
    m_displayMode(E_SMOOTH)
{
}




/////////////////////////////////////////////////////////////////////////
//                                                                     //
// Class    : CGLUTContext                                             //
//                                                                     //
// Function : ~CGLUTContext (destructor)                               //
//                                                                     //
// Comments :                                                .         //
//                                                                     //
/////////////////////////////////////////////////////////////////////////
CGLUTContext::~CGLUTContext()
{
}




/////////////////////////////////////////////////////////////////////////
//                                                                     //
// Class    : CGLUTContext                                             //
//                                                                     //
// Function : initGL()                                                 //
//                                                                     //
// Comments : This function does the OpenGL specific initialization.   //
//                                                                     //
/////////////////////////////////////////////////////////////////////////
void CGLUTContext::initGL()
{
  glClearColor((GLclampf) m_pScene->m_PropBackColor.red(),
               (GLclampf) m_pScene->m_PropBackColor.green(),
               (GLclampf) m_pScene->m_PropBackColor.blue(),
               0.0f);

  glShadeModel(GL_SMOOTH);

  glEnable(GL_NORMALIZE);
  glEnable(GL_DEPTH_TEST);
  glEnable(GL_LIGHTING);
  glLightModeli(GL_LIGHT_MODEL_TWO_SIDE, GL_TRUE);

  GLfloat ambient_light[] = { 1, 1, 1, 1 };
  glLightModelfv(GL_LIGHT_MODEL_AMBIENT, ambient_light);
}




/////////////////////////////////////////////////////////////////////////
//                                                                     //
// Class    : CGLUTContext                                             //
//                                                                     //
// Function : displayFunc()                                            //
//                                                                     //
// Comments : This function is a GLUT callback that is called when the //
//            scene needs to be redisplayed.                           //
//                                                                     //
/////////////////////////////////////////////////////////////////////////
void CGLUTContext::displayFunc(void)
{
  const CScene& scene = *(getInstance()->m_pScene);
  glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

  glMatrixMode(GL_MODELVIEW);
  glPushMatrix();
  gluLookAt( scene.m_PropCameraPos.x,
             scene.m_PropCameraPos.y,
             scene.m_PropCameraPos.z,
             scene.m_PropCameraTarget.x,
             scene.m_PropCameraTarget.y,
             scene.m_PropCameraTarget.z,
             scene.m_PropUpVector.x,
             scene.m_PropUpVector.y,
             scene.m_PropUpVector.z );


  // Light sources
  int size = (int) scene.m_vLights.size();
  for (int i = 0; i < size; i++)
    {
      GLfloat position[4], color[4];

      CLight *pLight = scene.m_vLights[i];
      position[0] = (GLfloat) pLight->m_PropPosition.x;
      position[1] = (GLfloat) pLight->m_PropPosition.y;
      position[2] = (GLfloat) pLight->m_PropPosition.z;
      position[3] = 1;

      color[0] = (GLfloat) (pLight->m_PropIntensity * pLight->m_PropLightColor.red());
      color[1] = (GLfloat) (pLight->m_PropIntensity * pLight->m_PropLightColor.green());
      color[2] = (GLfloat) (pLight->m_PropIntensity * pLight->m_PropLightColor.blue());
      color[3] = 1;

      glLightfv( (GLenum) (GL_LIGHT0+i), GL_POSITION, position);
      glLightfv( (GLenum) (GL_LIGHT0+i), GL_DIFFUSE, color);
      glLightfv( (GLenum) (GL_LIGHT0+i), GL_SPECULAR, color);
      glEnable( (GLenum) (GL_LIGHT0+i) );
    }


  // Display
  for (CScene::surface_container::const_iterator it
         = scene.m_vSurfaces.begin();
       it != scene.m_vSurfaces.end();
       ++it)
    {
      (*it)->DrawGL();
    }


  // Matrix restoration
  glPopMatrix();

  glutSwapBuffers();
}




/////////////////////////////////////////////////////////////////////////
//                                                                     //
// Class    : CGLUTContext                                             //
//                                                                     //
// Function : reshapeFunc()                                            //
//                                                                     //
// Comments : This function is a GLUT callback that is called when the //
//            OpenGL context has just been resized.                    //
//                                                                     //
/////////////////////////////////////////////////////////////////////////
void CGLUTContext::reshapeFunc(int width, int height)
{
  glViewport(0, 0, (GLsizei) width, (GLsizei) height);

  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  gluPerspective( getInstance()->m_angle,
                  (GLfloat) width / (GLfloat) height,
                  getInstance()->m_pScene->m_PropNearClipPlane,
                  getInstance()->m_pScene->m_PropFarClipPlane );

  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
}




/////////////////////////////////////////////////////////////////////////
//                                                                     //
// Class    : CGLUTContext                                             //
//                                                                     //
// Function : keyboardFunc()                                           //
//                                                                     //
// Comments : This function is a GLUT callback that is called when a   //
//            keyboard key is pressed.                                 //
//                                                                     //
/////////////////////////////////////////////////////////////////////////
void CGLUTContext::keyboardFunc(unsigned char key, int x, int y)
{
  const real_t FWD_SPEED = 5;

  switch (key)
    {
    case ' ':
      {
        switch (getInstance()->m_displayMode)
          {
          case (E_SMOOTH):
            glShadeModel(GL_FLAT);
            getInstance()->m_displayMode = E_FLAT;
            break;
          case (E_FLAT):
            glShadeModel(GL_SMOOTH);
            glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
            getInstance()->m_displayMode = E_LINES;
            break;
          case (E_LINES):
            glShadeModel(GL_SMOOTH);
            glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
            getInstance()->m_displayMode = E_SMOOTH;
            break;
          default:
            POP_ILLEGAL("Unallowed switch case, should not be here.");
            break;
          }
        break;
      }
    case '+':
      {
        CScene &scene = *(getInstance()->m_pScene);
        // Zoom in (can't zoom if too close from origin)
        if (scene.m_PropCameraPos.norm() > FWD_SPEED)
          {
            CVector direction = scene.m_PropCameraTarget - scene.m_PropCameraPos;
            direction.normalize();
            scene.m_PropCameraPos += FWD_SPEED * direction;
          }
        break;
      }
    case '-':
      {
        CScene &scene = *(getInstance()->m_pScene);
        CVector direction = scene.m_PropCameraPos - scene.m_PropCameraTarget;
        direction.normalize();
        scene.m_PropCameraPos += FWD_SPEED * direction;
        break;
      }
    case 'q':
    case 'Q':
    case 27:
      {
        exit(0);
      }
    default:
      {
        // Nothing to do
        break;
      }
    }

   // We always redisplay when a key is pressed
   glutPostRedisplay();
}




/////////////////////////////////////////////////////////////////////////
//                                                                     //
// Class    : CGLUTContext                                             //
//                                                                     //
// Function : specialFunc()                                            //
//                                                                     //
// Comments : This function is a GLUT callback that is called when a   //
//            special keyboard key (arrows in this case) is pressed.   //
//                                                                     //
/////////////////////////////////////////////////////////////////////////
void CGLUTContext::specialFunc(int key, int x, int y)
{
  const real_t ROT_ANGLE = 2 * PI / MAKE_REAL(180.0);

  CScene& scene = *(getInstance()->m_pScene);
  CVector axis;
  real_t angle;

  switch (key)
    {
    case (GLUT_KEY_LEFT):
      angle = ROT_ANGLE;
      axis  = scene.m_PropUpVector;
      break;
    case (GLUT_KEY_RIGHT):
      angle = -ROT_ANGLE;
      axis  = scene.m_PropUpVector;
      break;
    case (GLUT_KEY_UP):
      angle = ROT_ANGLE;
      axis  =
        (scene.m_PropCameraTarget - scene.m_PropCameraPos) ^
        scene.m_PropUpVector;
      axis.normalize();
      break;
    case (GLUT_KEY_DOWN):
      angle = -ROT_ANGLE;
      axis  =
        (scene.m_PropCameraTarget - scene.m_PropCameraPos) ^
        scene.m_PropUpVector;
      axis.normalize();
      break;
    default:
      angle = 0;
      axis  = scene.m_PropUpVector;
    };

  CMatrix3<real_t> mat;
  axis.normalize();
  rotationMatrix(angle, axis.x, axis.y, axis.z, mat);
  scene.m_PropCameraPos *= mat;
  glutPostRedisplay();
}
