// shader.cc
// Shader demo
//
// 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 shader shader.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>
#include <assert.h>

#include <iostream>

// OpenGL header (includes everything necessary for OpenGL)
#define GL_GLEXT_PROTOTYPES
#include <GL/glut.h>
#include <GL/glext.h>

#include <stdio.h>
#include <malloc.h>
#include <fcntl.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>

void world (int half);

//////////////////////////////////////////////////////////////////////////////
// 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 (0U);

// Time for animation
unsigned long T (0);

// 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 (8.0);
double camera_y (-14.0);
double camera_z (12.0);
double camera_yaw (130.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);

// Light source 0 position
GLfloat light0_pos[] = {30000.0f, -20000.0f, 20000.0f, 1.0f};

// Display window size
GLuint W;
GLuint H;

// Shader program handlers
GLhandleARB phong_shader;
GLhandleARB cell_shader;

//////////////////////////////////////////////////////////////////////////////
// Functions to draw the models in the scene

// Draw a sphere
void sphere () {

  // Sphere material
  static GLfloat diffuse[] = {0.6, 0.0, 0.0, 1.0};
  static GLfloat ambient[] = {0.3, 0.0, 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);

  // Draw sphere
  glutSolidSphere (1.0, 18, 18);

  return;
}

// Draw a teapot
void teapot () {

  // Teapot material
  static GLfloat diffuse[] = {0.0, 0.6, 0.0, 1.0};
  static GLfloat ambient[] = {0.0, 0.3, 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.


  // Multiply modelview matrix with additional transformation to move
  // and rotate the teapot to a different position.
  
  // Glut function for drawing a solid teapot
  glutSolidTeapot (1.0);

  return;
}

// Main function called to setup the scene contents
void scene () {

  glUseProgram (phong_shader);
  
  // Sphere
  glPushMatrix ();
  glTranslated (0.0, -4.5, 2.0);
  sphere ();
  glPopMatrix ();

  // Teapot
  glPushMatrix ();
  glTranslated (0.0, 1.5, 2.0);
  glRotated (90.0, 1.0, 0.0, 0.0);
  teapot ();
  glPopMatrix ();

  glUseProgram (cell_shader);
  
  // Teapot
  glPushMatrix ();
  glTranslated (0.0, 4.5, 2.0);
  glRotated (90.0, 1.0, 0.0, 0.0);
  teapot ();
  glPopMatrix ();

  // Sphere
  glPushMatrix ();
  glTranslated (0.0, -1.5, 2.0);
  sphere ();
  glPopMatrix ();

  glUseProgram (0);

  return;
}

//////////////////////////////////////////////////////////////////////////////
// Shader

char *textFileRead(const char *fn) {
  FILE *fp;
  char *content = NULL;
  int f, count;
  f = open(fn, O_RDONLY);
  count = lseek(f, 0, SEEK_END);
  close(f);
  if (fn != NULL) {
    fp = fopen(fn,"rt");
    if (fp != NULL) {
      if (count > 0) {
	content = (char *)malloc(sizeof(char) * (count+1));
	count = fread(content,sizeof(char),count,fp);
	content[count] = '\0';
      }
      fclose(fp);
    }
  }
  return content;
}

GLhandleARB init_shader (const char *vert_shader, const char *frag_shader) {

  GLhandleARB v = glCreateShaderObjectARB (GL_VERTEX_SHADER_ARB);
  GLhandleARB f = glCreateShaderObjectARB (GL_FRAGMENT_SHADER_ARB);
  
  char *vs = textFileRead (vert_shader);
  char *fs = textFileRead (frag_shader);
  const char *vvs = vs;
  const char *ffs = fs;

  glShaderSource (v, 1, &vvs, NULL);
  glShaderSource (f, 1, &ffs, NULL);

  free (vs);
  free (fs);

  glCompileShader (v);
  glCompileShader (f);

  GLhandleARB p = glCreateProgramObjectARB ();
  glAttachObjectARB (p, f);
  glAttachObjectARB (p, v);

  glLinkProgram (p);

  GLint length (0);
  glGetObjectParameterivARB (p, GL_OBJECT_INFO_LOG_LENGTH_ARB, &length);
  if (length > 1) {
    GLcharARB *buffer = new GLcharARB[length];
    GLsizei len;
    glGetInfoLogARB (p, length - 1, &len, buffer);
    std::cout << vert_shader << " / " << frag_shader << ":\n"
	      << buffer << std::endl;
    delete[] buffer;
  }
  
  return p;
}

//////////////////////////////////////////////////////////////////////////////
// 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.4, 0.4, 0.4, 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;
}

//////////////////////////////////////////////////////////////////////////////
// Callback functions for user interaction, etc.

void world () {
 
  glLightfv (GL_LIGHT0, GL_POSITION, light0_pos);

  glCallList (scene_dl);
  
  return;
}

// Display callback to render scene
void display_callback () {

  glViewport (0, 0, W, H);
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  glMatrixMode (GL_MODELVIEW);
  glLoadIdentity ();

  gluLookAt (camera_x, camera_y, camera_z,
	     camera_x + cos (camera_yaw) * cos (camera_pitch),
	     camera_y + sin (camera_yaw) * cos (camera_pitch),
	     camera_z + sin (camera_pitch),
	     0.0, 0.0, 1.0);
  
  world ();

  glFlush ();

  glutSwapBuffers ();

  return;
}

void idle_callback () {
  ++T;
  glutPostRedisplay ();
  return;
}

// Window reshape callback
void reshape_callback (int w, int h) {

  W = w;
  H = h;
  
  // Setup viewport size
  glViewport (0, 0, w, h);
  
  // Setup projection matrix
  glMatrixMode (GL_PROJECTION);
  glLoadIdentity ();
  gluPerspective (60.0, 
		  static_cast<double>(w) / static_cast<double> (h),
		  0.1, 1000.0);

  return;
}

// Keyboard callback called when key pressed
void keyboard_callback (unsigned char key, int x, int y) {

  // ESC for exit
  switch (key) {
  case 27:
    exit (0);
    break;
  case 'w':
    camera_x += dist_sensitivity * 50.0
      * cos (camera_yaw) * cos (camera_pitch);
    camera_y += dist_sensitivity * 50.0
      * sin (camera_yaw) * cos (camera_pitch);
    camera_z += dist_sensitivity * 50.0
      
      * sin (camera_pitch);
    break;
  case 's':
    camera_x -= dist_sensitivity * 50.0
      * cos (camera_yaw) * cos (camera_pitch);
    camera_y -= dist_sensitivity * 50.0
      * sin (camera_yaw) * cos (camera_pitch);
    camera_z -= dist_sensitivity * 50.0
      * sin (camera_pitch);
    break;
  case 'a':
    camera_x += dist_sensitivity * 50.0
      * cos (camera_yaw + M_PI/2.0);
    camera_y += dist_sensitivity * 50.0
      * sin (camera_yaw + M_PI/2.0);
    break;
  case 'd':
    camera_x += dist_sensitivity * 50.0
      * cos (camera_yaw - M_PI/2.0);
    camera_y += dist_sensitivity * 50.0
      * sin (camera_yaw - M_PI/2.0);
    break;
  }
  
  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> (mouse_y - 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> (mouse_y - y)
      * cos (camera_yaw) * cos (camera_pitch);
    camera_y += dist_sensitivity * static_cast<double> (mouse_y - y)
      * sin (camera_yaw) * cos (camera_pitch);
    camera_z += dist_sensitivity * static_cast<double> (mouse_y - y)
      * sin (camera_pitch);
  }

  // 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, RGB colours and depth buffer (for visible
  // surface detection)
  glutInitDisplayMode (GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);

  // Create display window
  glutInitWindowSize (512, 512);
  glutCreateWindow ("sphere");

  // Setup callback functions for rendering the scene, user interaction, ...
  glutDisplayFunc (display_callback);
  glutIdleFunc (idle_callback);
  glutReshapeFunc (reshape_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);

  // Blending
  glEnable (GL_BLEND);
  glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
  
  // Initialise lights
  init_lights ();
 
  // Create shaders
  phong_shader = init_shader ("phong.vert", "phong.frag");
  cell_shader = init_shader ("cell.vert", "cell.frag");

  // 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;
}

