//////////////////////////////////////////////////////////////////////////
//                                                                      //
//  This file contains the implementation of the CGLUTShaderContext     //
//  class.                                                              //
//                                                                      //
//////////////////////////////////////////////////////////////////////////
#include "popassert.hpp"
#include <GL/glut.h>
#include <cmath>
#include <iostream>
#include <string>
#include "gettime.hpp"
#include "CShaderProgram.hpp"
#include "CScene.hpp"
#include "CGLUTShaderContext.hpp"
using std::cerr;
using std::endl;
using std::string;




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




/////////////////////////////////////////////////////////////////////////
//                                                                     //
// Class    : CGLUTShaderContext                                       //
//                                                                     //
// Function : init()                                                   //
//                                                                     //
// Comments : This function initializes the OpenGL context.            //
//                                                                     //
/////////////////////////////////////////////////////////////////////////
bool CGLUTShaderContext::init(int *argc, char** argv,
                              const CScene& scene, const string& shaderCode)
{
  m_aspectRatio = ((real_t) scene.m_PropWidth) / ((real_t) scene.m_PropHeight);

  m_camera.m_position    = scene.m_PropCameraPos;
  m_camera.m_target      = scene.m_PropCameraTarget;
  m_camera.m_up          = scene.m_PropUpVector;
  m_camera.m_up.normalize();
  m_camera.m_moveSpeed   = (scene.m_PropCameraTarget -
                            scene.m_PropCameraPos).norm() *
                           MAKE_REAL(0.000001);
  m_camera.m_rotateSpeed = MAKE_REAL(0.01);

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

  // Create shader
  m_pShaderProgram.reset(CShaderProgram::getInstance());
  if (m_pShaderProgram.get() == NULL) {
    cerr << "No support for GLSL shaders." << endl;
    return false;
  }

  // Compile shader
  bool compileSuccess = m_pShaderProgram->addShader(CShaderProgram::FRAGMENT_SHADER,
                                                   shaderCode);
  if (!compileSuccess) {
    cerr << "Unable to compile shader code." << endl;
    return false;
  }

  // Link shader
  bool linkSuccess = m_pShaderProgram->link();
  if (!linkSuccess) {
    cerr << "Unable to link shader." << endl;
    return false;
  }

  // Compute angle (c.f. CGLUTContext.cpp for explanation)
  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;

  // GLUT callback setting
  glutDisplayFunc(displayFunc);
  glutReshapeFunc(reshapeFunc);
  glutKeyboardFunc(keyboardFunc);
  glutSpecialFunc(specialFunc);
  glutMouseFunc(mouseFunc);
  glutMotionFunc(motionFunc);

  return true;
}




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




/////////////////////////////////////////////////////////////////////////
//                                                                     //
// Class    : CGLUTShaderContext                                       //
//                                                                     //
// Function : CGLUTShaderContext (constructor)                         //
//                                                                     //
// Comments :                                                .         //
//                                                                     //
/////////////////////////////////////////////////////////////////////////
CGLUTShaderContext::CGLUTShaderContext()
  : m_aspectRatio(1)       ,
    m_camera()             ,
    m_pShaderProgram(NULL) ,
    m_buttonState(0)       ,
    m_lastFrameTime(0)
{
}




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




/////////////////////////////////////////////////////////////////////////
//                                                                     //
// Class    : CGLUTShaderContext                                       //
//                                                                     //
// Function : displayFunc()                                            //
//                                                                     //
// Comments : This function is a GLUT callback that is called when the //
//            scene needs to be redisplayed.                           //
//                                                                     //
/////////////////////////////////////////////////////////////////////////
void CGLUTShaderContext::displayFunc(void)
{
  CGLUTShaderContext *pInstance = CGLUTShaderContext::getInstance();

  // Start using shader
  pInstance->m_pShaderProgram->use();

  // Uniform variable setting
  pInstance->m_pShaderProgram->setUniform1f("u_aspectRatio",
                                            (float) pInstance->m_aspectRatio);
  pInstance->m_pShaderProgram->setUniform1f("u_angle",
                                            (float) pInstance->m_angle);
  pInstance->m_pShaderProgram->setUniform3f("u_target",
                                            (float) pInstance->m_camera.m_target.x,
                                            (float) pInstance->m_camera.m_target.y,
                                            (float) pInstance->m_camera.m_target.z);
  pInstance->m_pShaderProgram->setUniform3f("u_up",
                                            (float) pInstance->m_camera.m_up.x,
                                            (float) pInstance->m_camera.m_up.y,
                                            (float) pInstance->m_camera.m_up.z);
  pInstance->m_pShaderProgram->setUniform3f("u_position",
                                            (float) pInstance->m_camera.m_position.x,
                                            (float) pInstance->m_camera.m_position.y,
                                            (float) pInstance->m_camera.m_position.z);

  // Draw a quad for the shader
  glBegin(GL_QUADS);
    glTexCoord2d(0, 0); glVertex2i(-1,-1);
    glTexCoord2d(0, 1); glVertex2i(-1, 1);
    glTexCoord2d(1, 1); glVertex2i( 1, 1);
    glTexCoord2d(1, 0); glVertex2i( 1,-1);
  glEnd();

  // Stop using shader
  pInstance->m_pShaderProgram->unuse();

  glutSwapBuffers();
}




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

  getInstance()->m_aspectRatio = ((real_t) width) / ((real_t) height);
}




/////////////////////////////////////////////////////////////////////////
//                                                                     //
// Class    : CGLUTShaderContext                                       //
//                                                                     //
// Function : keyboardFunc()                                           //
//                                                                     //
// Comments : This function is a GLUT callback that is called when a   //
//            keyboard key is pressed.                                 //
//                                                                     //
/////////////////////////////////////////////////////////////////////////
void CGLUTShaderContext::keyboardFunc(unsigned char key, int x, int y)
{
  switch(key) {
  case ('q'):
  case ('Q'):
  case (27) :
    exit(0);
    break;
  }
}




/////////////////////////////////////////////////////////////////////////
//                                                                     //
// Class    : CGLUTShaderContext                                       //
//                                                                     //
// Function : specialFunc()                                            //
//                                                                     //
// Comments : This function is a GLUT callback that is called when a   //
//            special keyboard key (arrows in this case) is pressed.   //
//                                                                     //
/////////////////////////////////////////////////////////////////////////
void CGLUTShaderContext::specialFunc(int key, int x, int y)
{
  glutPostRedisplay();
}




/////////////////////////////////////////////////////////////////////////
//                                                                     //
// Class    : CGLUTShaderContext                                       //
//                                                                     //
// Function : mouseFunc()                                              //
//                                                                     //
// Comments : This function is a GLUT callback that is called when a   //
//            mouse button is pressed.                                 //
//                                                                     //
/////////////////////////////////////////////////////////////////////////
void CGLUTShaderContext::mouseFunc(int button, int state, int x, int y)
{
  CGLUTShaderContext *pInstance = CGLUTShaderContext::getInstance();

  BUTTON_ID curButton = BUTTON_NONE;

  switch (button) {
  case GLUT_LEFT_BUTTON:   curButton = BUTTON_LEFT;   break;
  case GLUT_MIDDLE_BUTTON: curButton = BUTTON_MIDDLE; break;
  case GLUT_RIGHT_BUTTON:  curButton = BUTTON_RIGHT;  break;
  }

  if (curButton != BUTTON_NONE) {
    if (state == GLUT_DOWN) {
      pInstance->m_lastPosX      = x;
      pInstance->m_lastPosY      = y;
      pInstance->m_lastFrameTime = GetTime();

      pInstance->m_buttonState |= curButton;
    }
    else {
      pInstance->m_buttonState &= ~curButton;
    }
  }

  if ((pInstance->m_buttonState & BUTTON_LEFT) ||
      (pInstance->m_buttonState & BUTTON_RIGHT)) {
    glutIdleFunc(moveIdleFunc);
  }
  else {
    glutIdleFunc(NULL);
  }
}




/////////////////////////////////////////////////////////////////////////
//                                                                     //
// Class    : CGLUTShaderContext                                       //
//                                                                     //
// Function : motionFunc()                                             //
//                                                                     //
// Comments : This function is a GLUT callback that is called when     //
//            the mouse is moved when button is pressed.               //
//                                                                     //
/////////////////////////////////////////////////////////////////////////
void CGLUTShaderContext::motionFunc(int x, int y)
{
  CGLUTShaderContext *pInstance = CGLUTShaderContext::getInstance();

  if (pInstance->m_buttonState & BUTTON_MIDDLE) {
    pInstance->m_camera.VerticalRotate((pInstance->m_lastPosX - x));
    pInstance->m_camera.HorizontalRotate((pInstance->m_lastPosY - y));

    pInstance->m_lastPosX = x;
    pInstance->m_lastPosY = y;

    glutPostRedisplay();
  }
}




/////////////////////////////////////////////////////////////////////////
//                                                                     //
// Class    : CGLUTShaderContext                                       //
//                                                                     //
// Function : moveIdleFunc()                                           //
//                                                                     //
// Comments : This function is a GLUT callback that is called when     //
//            the mouse is moved when button is pressed.               //
//                                                                     //
/////////////////////////////////////////////////////////////////////////
void CGLUTShaderContext::moveIdleFunc(void)
{
  CGLUTShaderContext *pInstance = CGLUTShaderContext::getInstance();

  float delta = 0;

  if (pInstance->m_buttonState & BUTTON_LEFT) {
    // Going forward
    delta += 1;
  }
  if (pInstance->m_buttonState & BUTTON_RIGHT) {
    // Going backward
    delta -= 1;
  }

  int curTime = GetTime();
  delta *= (curTime - pInstance->m_lastFrameTime);
  pInstance->m_lastFrameTime = curTime;

  pInstance->m_camera.Move(delta);

  glutPostRedisplay();
}
