// anim.cc
// Simple animation in OpenGL
//
// Copyright (C) 2004-2005, 2009 Frank C. Langbein, http://www.langbein.org/
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
//
// Compile with
//
//   c++ -o anim anim.cc -L/usr/X11R6/lib -lm -lX11 -lXext -lXt \
//      -lXmu -lXi -lGL -lGLU -lglut

// Standard headers
#include <stdlib.h>
#include <unistd.h>
#include <math.h>

// OpenGL header (includes everything necessary for OpenGL)
#include <GL/glut.h>


////////////////////////////////////////////////////////////////////////
// Constants, global variables, etc.

// Conversion factors for degrees to radians
const double deg_to_rad (M_PI / 180.0);
const double rad_to_deg (180.0 / M_PI);

// Id of display list containing the scene describtion
GLuint scene_dl;

// First id of display lists containing the animated objects
GLuint anim_dl;

// Track mouse position
int mouse_x (0);
int mouse_y (0);
bool mouse_button1 (false);
bool mouse_button2 (false);
bool mouse_button3 (false);

// Camera position
double camera_x (10.0);
double camera_y (10.0);
double camera_z (12.0);
double camera_yaw (45.0 * deg_to_rad);
double camera_pitch (35.0 * deg_to_rad);

// Camera movement sensitivity
const double angle_sensitivity = 0.005;
const double dist_sensitivity = 0.01;

// Scene rotation
double scene_yaw (0.0);
double scene_pitch (0.0);

// Timer for animation
int time;


////////////////////////////////////////////////////////////////////////
// Functions to draw the models in the scene

// Draw a sphere
void sphere () {

  // Angle steps for facet size
  static double da (1.0);

  // Sphere material
  static GLfloat diffuse[] = {0.7, 0.0, 0.0, 0.4};
  static GLfloat ambient[] = {0.5, 0.0, 0.0, 0.4};
  static GLfloat specular[] = {1.0, 1.0, 1.0, 0.4};
  static GLfloat shine (127.0);

  // Set material
  glMaterialfv (GL_FRONT, GL_AMBIENT, ambient);
  glMaterialfv (GL_FRONT, GL_DIFFUSE, diffuse);
  glMaterialfv (GL_FRONT, GL_SPECULAR, specular);
  glMaterialfv (GL_FRONT, GL_SHININESS, &shine);

  // For shading we need surface normals in addition to vertex
  // positions; for the unit sphere the normal is the same than the
  // surface position (note that this is a very special case!).
  //
  // We first set the normal of the vertex and then the vertex itself.
  
  // Quadrilateral strips
  for (double phi = -90.0 + da; phi < 90.0; phi += da) {
    glBegin (GL_QUAD_STRIP);
    for (double thet = -180.0; thet <= 180.0; thet += da) {
      glNormal3d (sin (deg_to_rad * thet) * cos (deg_to_rad * phi),
		  cos (deg_to_rad * thet) * cos (deg_to_rad * phi),
		  sin (deg_to_rad * phi)); 
      glVertex3d (sin (deg_to_rad * thet) * cos (deg_to_rad * phi),
		  cos (deg_to_rad * thet) * cos (deg_to_rad * phi),
		  sin (deg_to_rad * phi)); 
      glNormal3d (sin (deg_to_rad * thet) * cos (deg_to_rad * (phi + da)),
		  cos (deg_to_rad * thet) * cos (deg_to_rad * (phi + da)),
		  sin (deg_to_rad * (phi + da)));  
      glVertex3d (sin (deg_to_rad * thet) * cos (deg_to_rad * (phi + da)),
		  cos (deg_to_rad * thet) * cos (deg_to_rad * (phi + da)),
		  sin (deg_to_rad * (phi + da)));  
    }
    glEnd();
  }
  
  // North pole
  glBegin (GL_TRIANGLE_FAN);
  glNormal3d (0.0, 0.0, 1.0);
  glVertex3d (0.0, 0.0, 1.0);
  for (double thet = -180.0; thet <= 180.0; thet += 10.0) {
    glNormal3d (sin (deg_to_rad * thet) * cos (deg_to_rad * (90.0 - da)),
		cos (deg_to_rad * thet) * cos (deg_to_rad * (90.0 - da)),
		sin (deg_to_rad * 80.0));
    glVertex3d (sin (deg_to_rad * thet) * cos (deg_to_rad * (90.0 - da)),
		cos (deg_to_rad * thet) * cos (deg_to_rad * (90.0 - da)),
		sin (deg_to_rad * 80.0));
  }
  glEnd();

  // South pole
  glBegin (GL_TRIANGLE_FAN);
  glNormal3d (0.0, 0.0, -1.0);
  glVertex3d (0.0, 0.0, -1.0);
  for (double thet = -180.0; thet <= 180.0; thet += 10.0) {
    glNormal3d (sin (deg_to_rad * thet) * cos (deg_to_rad * (-90.0 + da)),
		cos (deg_to_rad * thet) * cos (deg_to_rad * (-90.0 + da)),
		sin (deg_to_rad * (-90.0 + da)));
    glVertex3d (sin (deg_to_rad * thet) * cos (deg_to_rad * (-90.0 + da)),
		cos (deg_to_rad * thet) * cos (deg_to_rad * (-90.0 + da)),
		sin (deg_to_rad * (-90.0 + da)));
  }
  glEnd();

  return;
}

// Draw a cone
void cone () {

  // Angle steps for facet size
  static double da (1.0);

  // Cone material
  static GLfloat diffuse[] = {0.6, 0.0, 0.6, 1.0};
  static GLfloat ambient[] = {0.4, 0.0, 0.4, 1.0};
  static GLfloat specular[] = {0.2, 0.0, 0.3, 1.0};
  static GLfloat shine = 1.0;

  // Set Material
  glMaterialfv (GL_FRONT, GL_AMBIENT, ambient);
  glMaterialfv (GL_FRONT, GL_DIFFUSE, diffuse);
  glMaterialfv (GL_FRONT, GL_SPECULAR, specular);
  glMaterialfv (GL_FRONT, GL_SHININESS, &shine);

  // Draw conical surface of finite cone.
  glBegin (GL_TRIANGLE_FAN);
  glNormal3d (0.0, 0.0, 0.0);
  glVertex3d (0.0, 0.0, -1.0);
  for (double thet = 180.0; thet >= -180.0; thet -= da) {
    glNormal3d (sin (deg_to_rad * thet),
		cos (deg_to_rad * thet),
		1.0);
    glVertex3d (sin (deg_to_rad * thet),
		cos (deg_to_rad * thet),
		1.0);
  }
  glEnd();

  // Draw top of finite cone
  glBegin (GL_TRIANGLE_FAN);
  glNormal3d (0.0, 0.0, 1.0);
  glVertex3d (0.0, 0.0, 1.0);
  for (double thet = 180.0; thet >= -180.0; thet -= da) {
    glNormal3d (sin (deg_to_rad * thet),
		cos (deg_to_rad * thet),
		1.0);
    glVertex3d (sin (deg_to_rad * thet),
		cos (deg_to_rad * thet),
		1.0);
  }
  glEnd();
  
  return;
}

// Draw a teapot
void teapot () {

  // Teapot material
  static GLfloat diffuse[] = {0.0, 0.7, 0.0, 1.0};
  static GLfloat ambient[] = {0.0, 0.5, 0.0, 1.0};
  static GLfloat specular[] = {1.0, 1.0, 1.0, 1.0};
  static GLfloat shine (127.0);

  // Set material
  glMaterialfv (GL_FRONT, GL_AMBIENT, ambient);
  glMaterialfv (GL_FRONT, GL_DIFFUSE, diffuse);
  glMaterialfv (GL_FRONT, GL_SPECULAR, specular);
  glMaterialfv (GL_FRONT, GL_SHININESS, &shine);

  // Push current modelview matrix on a matrix stack to save current
  // transformation.
  glPushMatrix ();

  // Multiply modelview matrix with additional transformation to
  // rotate the teapot to a different position.
  glRotated (90.0, 1.0, 0.0, 0.0);
  
  // Glut function for drawing a solid teapot
  glutSolidTeapot (1.0);

  // Get original matrix back from stack (undo above transformation
  // for objects drawn after this one)
  glPopMatrix ();
    
  return;
}

// Draw floor
void floor () {

  // Floor tile size
  static float d = 2.0;

  // Floor material
  static GLfloat floor1_diffuse[] = {0.0, 0.0, 0.7, 1.0};
  static GLfloat floor1_ambient[] = {0.1, 0.1, 0.1, 1.0};
  static GLfloat floor1_specular[] = {0.1, 0.1, 0.1, 1.0};
  static GLfloat floor1_shine = 1.0;
  static GLfloat floor2_diffuse[] = {0.7, 0.7, 0.7, 1.0};
  static GLfloat floor2_ambient[] = {0.1, 0.1, 0.1, 1.0};
  static GLfloat floor2_specular[] = {0.1, 0.1, 0.1, 1.0};
  static GLfloat floor2_shine = 1.0;

  // Set constant normal
  glNormal3d (0.0, 0.0, 1.0);

  // Draw tiled floor
  for (int l = -15; l < 16;  ++l) {
    for (int k = -15; k < 16;  ++k) {
      // Set Material
      if ( (l + k) % 2 == 0) {
	glMaterialfv (GL_FRONT, GL_AMBIENT, floor1_ambient);
	glMaterialfv (GL_FRONT, GL_DIFFUSE, floor1_diffuse);
	glMaterialfv (GL_FRONT, GL_SPECULAR, floor1_specular);
	glMaterialfv (GL_FRONT, GL_SHININESS, &floor1_shine);
      } else {
	glMaterialfv (GL_FRONT, GL_AMBIENT, floor2_ambient);
	glMaterialfv (GL_FRONT, GL_DIFFUSE, floor2_diffuse);
	glMaterialfv (GL_FRONT, GL_SPECULAR, floor2_specular);
	glMaterialfv (GL_FRONT, GL_SHININESS, &floor2_shine);
      }

      glBegin (GL_QUAD_STRIP);
      glVertex3f (l * d, k * d, 0.0);
      glVertex3f ((l + 1) * d, k * d, 0.0);
      glVertex3f (l * d, (k + 1) * d, 0.0);
      glVertex3f ((l + 1) * d, (k + 1) * d, 0.0);
      glEnd ();
    }
  }
  
  return;
}

// Main function called to setup the static scene contents
void scene () {

  // Draw floor
  glPushMatrix ();
  glTranslatef (0.0, 0.0, -0.5);
  floor ();
  glPopMatrix ();

  return;
}

////////////////////////////////////////////////////////////////////////
// Render animated objects

// Initialise animated objects
void init_animated () {

  // Display lists for cone, sphere and teapot
  anim_dl = glGenLists (3);

  // Cone
  glNewList (anim_dl, GL_COMPILE);
  cone ();
  glEndList ();

  // Sphere
  glNewList (anim_dl + 1, GL_COMPILE);
  sphere ();
  glEndList ();

  // Teapot
  glNewList (anim_dl + 2, GL_COMPILE);
  teapot ();
  glEndList ();

  return;
}

// Render animated objects
void animated () {

  // Positions for bouncing ball
  static int loop1 (130);
  static float h1 (loop1 * loop1 / 5.0);
  static int loop2 (76);
  static float h2 (loop2 * loop2 / 5.0);
  
  // Save matrix
  glPushMatrix ();

  // Animate cone
  glTranslatef (0.0, 0.0, 0.5);
  glRotatef (static_cast<float> (time % 360), 0.0, 0.0, 1.0);
  glTranslatef (0.0, 6.0, 0.0);
  glCallList (anim_dl);
  
  // Animate sphere
  float t (time % loop1);
  float H (5.0 / h1 * t * (loop1 - t));
  glTranslatef (0.0, 0.0, 1.5 + H);
  glScalef (1.0 + (5.0 - H) * 0.1, 1.0 + (5.0 - H) * 0.1, 1.0 + H * 0.15);
  glCallList (anim_dl + 1);

  // Restore matrix
  glPopMatrix ();

  // Save matrix
  glPushMatrix ();

  // Animate teapot
  t = ((time + 15) % loop2);
  H = 5.0 / h2 * t * (loop2 - t);
  glTranslatef (0.0, 0.0, 0.5);
  glRotatef (-static_cast<float> (time % 360), 0.0, 0.0, 1.0);
  glScalef (1.0 + (5.0 - H) * 0.2, 1.0 + (5.0 - H) * 0.2, 1.0 + H * 0.4);
  glCallList (anim_dl + 2);

  // Restore matrix
  glPopMatrix ();

  // Draw another sphere
  // This sphere is not animated, but in order for it to be
  // transparent such that the teapot can be seen it has to
  // be drawn after the teapot!
  glPushMatrix ();
  glTranslatef (0.0, 0.0, 0.5);
  glScalef (3.5, 3.5, 3.0);
  glCallList (anim_dl + 1);
  glPopMatrix ();

  // FIXME: If you look closely here, there is a problem. If the
  // bouncing sphere is in front of the central sphere and teapot,
  // these become invisible even if the sphere is transparent. The
  // drawing sequence would have to be reverse (but only if the sphere
  // is in front of the sphere/teapot, otherwise it would be invisible
  // if it is behind the sphere/teapot.

  return;
}

////////////////////////////////////////////////////////////////////////
// Lights

// Initialise lighting
void init_lights () {

  // Specify light emitted by light source 0
  static GLfloat diffuse[] = {1.0, 1.0, 1.0, 1.0};
  static GLfloat ambient[] = {0.3, 0.3, 0.3, 1.0};
  static GLfloat specular[] = {1.0, 1.0, 1.0, 1.0};
  static GLfloat attenuation = 1;

  // Enable lighting
  glEnable (GL_LIGHTING);

  // Enable first light source
  glEnable (GL_LIGHT0);
  // Set light emitted by light source 0
  glLightfv (GL_LIGHT0, GL_AMBIENT, ambient);
  glLightfv (GL_LIGHT0, GL_DIFFUSE, diffuse);
  glLightfv (GL_LIGHT0, GL_SPECULAR, specular);
  glLightfv (GL_LIGHT0, GL_CONSTANT_ATTENUATION, &attenuation);

  return;
}

// Render (set position of) light source
void lights () {

  // Light source 0 position
  static GLfloat light0_pos[] = {10, 15, 20, 1.0};

  // Set light source 0 position
  glLightfv (GL_LIGHT0, GL_POSITION, light0_pos);

  return;
}

////////////////////////////////////////////////////////////////////////
// Callback functions for user interaction, etc.

// Display callback to render scene
void display_callback () {

  // Clear frame buffer
  glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  
  // Setup modelview matrix (scene and viewing transformation)
  glMatrixMode (GL_MODELVIEW);
  glLoadIdentity ();

  // Camera
  gluLookAt (camera_x + cos (camera_yaw) * cos (camera_pitch),
	     camera_y + sin (camera_yaw) * cos (camera_pitch),
	     camera_z + sin (camera_pitch),
	     camera_x, camera_y, camera_z,
	     0.0, 0.0, 1.0);

  // Scene
  glRotated (scene_yaw * rad_to_deg, 0.0, 0.0, 1.0);
  glRotated (scene_pitch * rad_to_deg, 0.0, 1.0, 0.0);

  // Set light positions: fixed position with respect to objects; if
  // fixed position with respect to viewer (e.g. viewer has a flash
  // light) draw after setting modelview matrix to identity above.
  lights ();
  
  // Render scene by calling display list
  // (without display lists we'd call scene () here...)
  glCallList (scene_dl);

  // Render animated objects (no display list as updated every time;
  // but animated objects are stored in separate display lists)
  animated ();

  // Flush OpenGL pipeline
  glFlush ();

  // Swap buffers (double buffering for smooth animation)
  glutSwapBuffers ();

  return;
}

// Window reshape callback
void reshape_callback (int w, int h) {

  // Setup viewport size
  glViewport (0, 0, w, h);
  
  // Setup projection matrix
  glMatrixMode (GL_PROJECTION);
  glLoadIdentity ();

  // Perspective projection
  gluPerspective (80.0, static_cast<double>(w) / static_cast<double> (h),
		  0.1, 1000.0);

  return;
}

// Idle callback
void idle_callback () {

  // Count up timer
  ++time;

  // Redisplay with updated timer
  glutPostRedisplay ();
  
  return;
}

// Keyboard callback called when key pressed
void keyboard_callback (unsigned char key, int x, int y) {

  // ESC for exit
  if (key == 27) {
    exit (0);
  }

  return;
}

// Mouse callback called when mouse button is pressed
void mouse_callback (int btn, int state, int x, int y) {

  switch (btn) {
  case GLUT_LEFT_BUTTON:
    mouse_button1 = (state == GLUT_DOWN);
    break;
  case GLUT_MIDDLE_BUTTON:
    mouse_button2 = (state == GLUT_DOWN);
    break;
  case GLUT_RIGHT_BUTTON:
    mouse_button3 = (state == GLUT_DOWN);
    break;
  }

  // Update mouse position
  mouse_x = x;
  mouse_y = y;

  // Mark window for redrawing.
  glutPostRedisplay ();
  
  return;
}

// Motion callback to capture mouse movements while buttons pressed
void motion_callback (int x, int y) {

  // Button 1 pressed: change viewing direction
  if (mouse_button1) {
    camera_yaw += angle_sensitivity * static_cast<double> (mouse_x - x);
    if (camera_yaw > M_PI) {
      camera_yaw -= 2.0 * M_PI;
    } else if (camera_yaw < -M_PI) {
      camera_yaw += 2.0 * M_PI;
    }
    camera_pitch += angle_sensitivity * static_cast<double> (y - mouse_y);
    if (camera_pitch > M_PI_2) {
      camera_pitch = M_PI_2;
    } else if (camera_pitch < -M_PI_2) {
      camera_pitch = -M_PI_2;
    }
  }

  // Button 2 pressed: change position
  if (mouse_button2) {
    camera_x += dist_sensitivity * static_cast<double> (y - mouse_y)
      * cos (camera_yaw) * cos (camera_pitch);
    camera_y += dist_sensitivity * static_cast<double> (y - mouse_y)
      * sin (camera_yaw) * cos (camera_pitch);
    camera_z += dist_sensitivity * static_cast<double> (y - mouse_y)
      * sin (camera_pitch);
  }

  // Button 3 pressed: rotate scene
  if (mouse_button3) {
    scene_yaw += angle_sensitivity * static_cast<double> (x - mouse_x);
    if (scene_yaw > M_PI) {
      scene_yaw -= 2.0 * M_PI;
    } else if (scene_yaw < -M_PI) {
      scene_yaw += 2.0 * M_PI;
    }
    scene_pitch += angle_sensitivity * static_cast<double> (y - mouse_y);
    if (scene_pitch > M_PI) {
      scene_pitch -= 2.0 * M_PI;
    } else if (scene_pitch < -M_PI) {
      scene_pitch += 2.0 * M_PI;
    }
  }

  // Update mouse position
  mouse_x = x;
  mouse_y = y;
    
  // Mark window for redrawing.
  glutPostRedisplay ();
  
  return;
}

// Passive motion callback to capture mouse movements while buttons
// are not pressed
void passive_motion_callback (int x, int y) {

  // Update mouse position
  mouse_x = x;
  mouse_y = y;

  return;
}


////////////////////////////////////////////////////////////////////////
// Main Function

int main(int argc, char **argv) {

  // Initialise GLUT
  glutInit (&argc, argv);
  // Use double buffering and depth buffer
  // Use double buffering, RGB colours and depth buffer (for visible
  // surface detection)
  glutInitDisplayMode (GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);

  // Create display window
  glutInitWindowSize (500, 500);
  glutCreateWindow ("anim");

  // Setup callback functions for rendering the scene, user interaction, ...
  glutDisplayFunc (display_callback);
  glutReshapeFunc (reshape_callback);
  glutIdleFunc (idle_callback);
  glutKeyboardFunc (keyboard_callback);
  glutMouseFunc (mouse_callback);
  glutMotionFunc (motion_callback);
  glutPassiveMotionFunc (passive_motion_callback);

  // Enable visible surface detection via depth tests
  glDepthFunc (GL_LEQUAL);
  glClearDepth (1.0);
  glEnable (GL_DEPTH_TEST);

  // Set clear color to black
  glClearColor (0.0, 0.0, 0.0, 1.0);

  // Fill polygons for shading
  glPolygonMode(GL_FRONT, GL_FILL);

  // Treat polygon front and back side equal (no two-sided polygons)
  glLightModeli (GL_LIGHT_MODEL_TWO_SIDE, GL_FALSE);

  // Enable local viewer model for specular light
  glLightModeli (GL_LIGHT_MODEL_LOCAL_VIEWER, GL_TRUE);

  // Enable smoooth shading (multi-coloured polygons)
  glShadeModel (GL_SMOOTH);

  // Enable blending for transparancy
  glEnable (GL_BLEND);
  // Blend function: specifies how the alpha channel is used for blending
  glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

  // Initialise lights
  init_lights ();

  // Initialise animated objects
  init_animated ();

  // Store scene in a display list
  //   first create one display list
  scene_dl = glGenLists (1);
  //   and then call the rendering functions to store the primitives
  //   in the list
  glNewList (scene_dl, GL_COMPILE);
  scene ();
  glEndList ();

  // Start GLUT event loop
  // (display callback will render the scene)
  glutMainLoop();

  return 0;
}

