/***************************************************************************
 *   Copyright (C) 2007 by A.J. Tavakoli                                   *
 *                                                                         *
 *   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 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 *   This program is distributed in the hope that it will be useful,       *
 *   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, write to the                         *
 *   Free Software Foundation, Inc.,                                       *
 *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
 ***************************************************************************/

#include <stdexcept>

#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <windows.h> // yuck
#endif

#include <GL/gl.h>
#include <GL/glu.h>
#include <SDL/SDL.h>

#include "glext.h"
#include "Video.h"
#include "TGAFile.h"


// function pointers for OpenGL extensions
static PFNGLCREATEPROGRAMOBJECTARBPROC      glCreateProgramObjectARB;
static PFNGLDELETEOBJECTARBPROC             glDeleteObjectARB;
static PFNGLUSEPROGRAMOBJECTARBPROC         glUseProgramObjectARB;
static PFNGLCREATESHADEROBJECTARBPROC       glCreateShaderObjectARB;
static PFNGLSHADERSOURCEARBPROC             glShaderSourceARB;
static PFNGLCOMPILESHADERARBPROC            glCompileShaderARB;
static PFNGLGETOBJECTPARAMETERIVARBPROC     glGetObjectParameterivARB;
static PFNGLATTACHOBJECTARBPROC             glAttachObjectARB;
static PFNGLGETINFOLOGARBPROC               glGetInfoLogARB;
static PFNGLLINKPROGRAMARBPROC              glLinkProgramARB;
static PFNGLGETUNIFORMLOCATIONARBPROC       glGetUniformLocationARB;
static PFNGLGETATTRIBLOCATIONARBPROC        glGetAttribLocationARB;
static PFNGLVERTEXATTRIB3FARBPROC           glVertexAttrib3fARB;
static PFNGLVERTEXATTRIB4FARBPROC           glVertexAttrib4fARB;
static PFNGLVERTEXATTRIB1FARBPROC           glVertexAttrib1fARB;
static PFNGLUNIFORM3FARBPROC                glUniform3fARB;
static PFNGLUNIFORM4FARBPROC                glUniform4fARB;
static PFNGLUNIFORM1IARBPROC                glUniform1iARB;
static PFNGLENABLEVERTEXATTRIBARRAYPROC     glEnableVertexAttribArrayARB;
static PFNGLDISABLEVERTEXATTRIBARRAYARBPROC glDisableVertexAttribArrayARB;
static PFNGLVERTEXATTRIBPOINTERARBPROC      glVertexAttribPointerARB;
static PFNGLACTIVETEXTUREARBPROC            glActiveTextureARB;
static PFNGLCLIENTACTIVETEXTUREARBPROC      glClientActiveTextureARB;

static GLenum lightGLEnum[8] = { GL_LIGHT0, GL_LIGHT1, GL_LIGHT2, GL_LIGHT3,
                                 GL_LIGHT4, GL_LIGHT5, GL_LIGHT6, GL_LIGHT7 };

// static members of Video
int Video::normalMapShader = 0;

// vertex shader for normal mapping
static const GLcharARB *vertShaderSrc =
     "// light vector in tangent space                         \n"
     "varying vec3 lightVec;                                     "
     "                                                           "
     "// view vector in tangent space                          \n"
     "varying vec3 viewVec;                                      "
     "                                                           "
     "void main() {                                              "
     "  // NORMALS AND TANGENTS ARE ASSUMED TO BE NORMALIZED!! \n"
     "  // (and normal matrix is assumed not to change this!)  \n"
     "  vec3 t = gl_NormalMatrix*gl_MultiTexCoord2.xyz,          "
     "       n = gl_NormalMatrix*gl_Normal.xyz;                  "
     "                                                           "
     "  vec3 b = normalize( cross(n, t) );                       "
     "                                                           "
     "  // transform vertex into eye space                     \n"
     "  vec3 vertex = vec3(gl_ModelViewMatrix*gl_Vertex);        "
     "                                                           "
     "  // temporary variable to store light vector in viewspace\n"
     "  // TODO: this eye position is probably incorrect       \n"
     "  vec3 temp = vertex - gl_LightSource[0].position.xyz;     "
     "  lightVec.x = dot(t, temp);                               "
     "  lightVec.y = dot(b, temp);                               "
     "  lightVec.z = dot(n, temp);                               "
     "                                                           "
     "  // transform view vector into tangent space            \n"
     "  temp = -vertex;                                          "
     "  viewVec.x = dot(t, vertex);                              "
     "  viewVec.y = dot(b, vertex);                              "
     "  viewVec.z = dot(n, vertex);                              "
     "                                                           "
     "  /* normalize view vector */                              "
     "  viewVec = normalize(viewVec);                            "
     "                                                           "
     "  /* set texture coordinates */                            "
     "  gl_TexCoord[0] = gl_MultiTexCoord0;                      "
     "                                                           "
     "  /* set light map texture coordinates */                  "
     "  gl_TexCoord[1] = gl_MultiTexCoord3;                      "
     "                                                           "
     "  /* transform vertex */                                   "
     "  gl_Position = ftransform();                              "
     "}                                                          ";


// fragment shader for normal mapping source
static const GLcharARB *fragShaderSrc =
     "uniform sampler2D texture,                                                     "
     "                  normalMap;                                                   "
     "varying vec3 lightVec,                                                         "
     "             viewVec;                                                          "
     "                                                                               "
     "void main() {                                                                  "
     "  // compute distance to light                                               \n"
     "  float lightDist = sqrt( dot(lightVec,lightVec) );                            "
     "                                                                               "
     "  // compute attenuation based on distance to light                          \n"
     "  float att = 1.0/(gl_LightSource[0].constantAttenuation +                     "
     "                   gl_LightSource[0].linearAttenuation*lightDist +             "
     "                   gl_LightSource[0].quadraticAttenuation*lightDist*lightDist);"
     "                                                                               "
     "  // normalize interpolated light vector                                     \n"
     "  vec3 l = lightVec*(1.0/lightDist);                                           "
     "                                                                               "
     "  // sample color from texture                                               \n"
     "  vec4 texSample = texture2D(texture, gl_TexCoord[0].st);                      "
     "                                                                               "
     "  // sample normal from normal map                                           \n"
     "  // NOTE: apparently the sample MUST be normalized for the calculations to be\n"
     "  // remotely correct!                                                       \n"
     "  vec3 normal =normalize(2.0*(texture2D(normalMap,gl_TexCoord[0].st).rgb-0.5));"
     "                                                                               "
     "  // calculate ambient contribution                                          \n"
     "  vec4 litColor = (gl_LightModel.ambient + gl_LightSource[0].ambient)*         "
     "                   gl_FrontMaterial.ambient;                                   "
     "                                                                               "
     "  // calculate dot product of normal vector and light vector                 \n"
     "  float NdotL = dot(normal, l);                                                "
     "                                                                               "
     "  if ( NdotL > 0.0 ) {                                                         "
     "    // add diffuse contribution                                              \n"
     "    litColor += att*NdotL*(gl_FrontMaterial.diffuse*gl_LightSource[0].diffuse);"
     "                                                                               "
     "    // add specular contribution                                             \n"
     "    if ( dot(l, viewVec) > 0.0 ) {                                             "
     "      //viewVec = normalize(viewVec);                                       \n "
     "      vec3 halfv = (l + viewVec)*0.5;                                          "
     "      float NdotH = max( dot(normal, halfv), 0.0 );                            "
     "      litColor += att*gl_FrontMaterial.specular*gl_LightSource[0].specular*    "
     "                  pow(NdotH, gl_FrontMaterial.shininess);                      "
     "    }                                                                          "
     "                                                                               "
     "  }                                                                            "
     "  // compute fragment color                                                  \n"
     "  gl_FragColor = litColor*texSample;                                           "
     "}                                                                              ";

float Video::modelTranslation[3] = { 0.0f, 0.0f, 0.0f };
float Video::viewTranslation[3]  = { 0.0f, 0.0f, 0.0f };
float Video::modelRotation[16] = { 1.0f, 0.0f, 0.0f, 0.0f,
                                   0.0f, 1.0f, 0.0f, 0.0f,
                                   0.0f, 0.0f, 1.0f, 0.0f,
                                   0.0f, 0.0f, 0.0f, 1.0f };
float Video::viewRotation[16]  = { 1.0f, 0.0f, 0.0f, 0.0f,
                                   0.0f, 1.0f, 0.0f, 0.0f,
                                   0.0f, 0.0f, 1.0f, 0.0f,
                                   0.0f, 0.0f, 0.0f, 1.0f };

bool  Video::lightEnabled[MAX_LIGHTS];
Video::Light Video::lights[MAX_LIGHTS];

bool Video::shadersSupported     = false;
bool Video::normalMappingEnabled = false;
bool Video::texturesEnabled      = true;


void Video::initVideo() {
  // initialize SDL's video support
  int result = SDL_InitSubSystem(SDL_INIT_VIDEO);

  // was SDL initialization successful?
  if ( -1 == result )
    throw std::runtime_error("unable to initialize SDL video");

  // we want a double buffer
  if ( SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1) == -1 )
    throw std::runtime_error("unable to use double buffering");

  // attempt to set video mode
  if ( !SDL_SetVideoMode(WINDOW_WIDTH, WINDOW_HEIGHT, BPP, SDL_OPENGL) )
    throw std::runtime_error("unable to set requested video mode");

  // set window title
  SDL_WM_SetCaption(APP_TITLE, APP_TITLE);

  // hide mouse
  SDL_ShowCursor(SDL_FALSE);

  // set projection matrix/view frustum
  resize(WINDOW_WIDTH, WINDOW_HEIGHT);
  
  // initialize shader support
  shadersSupported = initShaders();

  glEnable(GL_DEPTH_TEST);
  glEnable(GL_TEXTURE_2D);
  glEnable(GL_CULL_FACE);
  glEnable(GL_TEXTURE_2D);

  // no lighting when using fixed function pipeline
  glDisable(GL_LIGHTING);

  glFrontFace(GL_CCW);

  glEnableClientState(GL_VERTEX_ARRAY);
  glEnableClientState(GL_NORMAL_ARRAY);
  glEnableClientState(GL_TEXTURE_COORD_ARRAY);

  // no global ambient light
  GLfloat globalAmbient[4] = { 0.0f, 0.0f, 0.0f, 0.0f };
  glLightModelfv(GL_LIGHT_MODEL_AMBIENT, globalAmbient);

  if ( shadersSupported ) {
    // enable vertex arrays on texture unit 2 also
    glClientActiveTextureARB(GL_TEXTURE2_ARB);
    glEnableClientState(GL_TEXTURE_COORD_ARRAY);
    glClientActiveTextureARB(GL_TEXTURE0_ARB);
  }

  // all lights initially disabled
  for ( int i=0; i < MAX_LIGHTS; i++ )
    lightEnabled[i] = false;

  // TODO: get rid of this
  Material mat;
  mat.ambient[0] = mat.ambient[1] = mat.ambient[2] = 1.0f;
  mat.diffuse[0] = mat.diffuse[1] = mat.diffuse[2] = 1.0f;
  mat.specular[0] = mat.specular[1] = mat.specular[2] = 1.0;
  mat.ambient[3] = mat.diffuse[3] = mat.specular[3] = 1.0f;
  mat.shininess = 8.0f;
  setMaterial(mat);

  checkGLError();
}


void Video::shutdownVideo() {
  SDL_QuitSubSystem(SDL_INIT_VIDEO);
}


// resize view (set projection matrix and viewport)
void Video::resize(int w, int h) {
  // sanity check
  if ( w < 0 )
    w = 0;

  // avoid div by 0
  if (h <= 0)
     h = 1;

  // set viewport
  glViewport(0, 0, w, h);

  glMatrixMode(GL_PROJECTION); // work on projection matrix
  glLoadIdentity(); // reset projection matrix

  // create perspective projection matrix
  gluPerspective(60.0, GLdouble(w)/GLdouble(h), 1.0, 10000.0);

  glMatrixMode(GL_MODELVIEW); // work on modelview matrix again
  glLoadIdentity();

  checkGLError();
}


void Video::clearZBuffer() {
  glClear(GL_DEPTH_BUFFER_BIT);
}


void Video::clearBackBuffer() {
  glClear(GL_COLOR_BUFFER_BIT);
}


void Video::showBackBuffer() {
  SDL_GL_SwapBuffers();
}


// generates a texture from an image file loaded from disk,
// only supports TGA files at this point
int Video::loadTexture(const char *filename, bool normalMap, bool useMipMaps) {
  // sanity check  
  if ( NULL == filename )
    throw std::runtime_error("Video::loadTexture(): filename is NULL");

  TGAFile tgaFile;

  // attempt to load image
  if ( !tgaFile.load(filename) ) {
    std::string msg("Video::loadTexture(): unable to load ");
    msg += filename;
    throw std::runtime_error(msg);
  }

  // another sanity check
  if ( tgaFile.getWidth() <= 0 || tgaFile.getHeight() <= 0 )
    throw std::runtime_error("Video::loadTexture(): image dimensions <= 0");

  // and yet another sanity check...
  if ( tgaFile.getData() == NULL )
    throw std::runtime_error("Video::loadTexture(): image is empty");

  // map to gl format
  GLenum formatGL;
  switch ( tgaFile.getBPP() ) {
  case 24: formatGL = GL_RGB;  break;
  case 32: formatGL = GL_RGBA; break;
  default:
    throw std::runtime_error("Video::createTexture(): unsupported format");
  }

  // map to gl type
  // (only one type at this point)
  GLenum typeGL = GL_UNSIGNED_BYTE;

  // generate a texture id
  GLuint texID = 0;
  glGenTextures(1, &texID);

  if ( texID > 0 ) {
    // bind texture id just generated
    glBindTexture(GL_TEXTURE_2D, texID);

    // set wrapping parameters
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    
    if ( true == useMipMaps ) {
      // set filtering parameters for mip mapping
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR_MIPMAP_LINEAR);

      // generate mip maps
      gluBuild2DMipmaps( GL_TEXTURE_2D,
                         formatGL,
                         tgaFile.getWidth(),
                         tgaFile.getHeight(),
                         formatGL,
                         typeGL,
                         tgaFile.getData() );
    }
    else {
      // not using mip mapping
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  
      // store image data in texture
      glTexImage2D( GL_TEXTURE_2D, 0, formatGL, tgaFile.getWidth(),
                   tgaFile.getHeight(), 0, formatGL, typeGL, tgaFile.getData() );
    }
  }

  // check for OpenGL errors
  checkGLError();
  
  if ( texID <= 0 )
    return -1; // could not generate a texture ID for
               // some reason

  // return texture id
  return int(texID);
}


void Video::deleteTexture(int texID) {
  // OpenGL texture IDs are > 0
  if ( texID > 0 ) {
    GLuint texIDGL[1] = { GLuint(texID) };
    glDeleteTextures(1, texIDGL);
    checkGLError();
  }
}


void Video::useTexture(int texID) {
  // if texture ID is invalid, don't do anything
  if ( texID < 0 )
    return;

  glActiveTextureARB(GL_TEXTURE0_ARB);
  glBindTexture(GL_TEXTURE_2D, texID);
}


void Video::useNormalMap(int texID) {
  if ( false == shadersSupported || getNormalMappingEnabled() == false )
    return;

  // if texture ID is invalid, don't do anything
  if ( texID < 0 )
    return;

  glActiveTextureARB(GL_TEXTURE1_ARB);
  glBindTexture(GL_TEXTURE_2D, texID);

  glActiveTextureARB(GL_TEXTURE0_ARB);
}


void Video::setModelTranslation(float *trans) {
  // sanity check
  if ( NULL == trans )
    return;

  modelTranslation[0] = trans[0];
  modelTranslation[1] = trans[1];
  modelTranslation[2] = trans[2];
}


void Video::setModelTranslation(const Point3 &trans) {
  modelTranslation[0] = trans[0];
  modelTranslation[1] = trans[1];
  modelTranslation[2] = trans[2];
}


void Video::setViewTranslation(float *trans) {
  // sanity check
  if ( NULL == trans )
    return;

  viewTranslation[0] = trans[0];
  viewTranslation[1] = trans[1];
  viewTranslation[2] = trans[2];
}


void Video::setViewTranslation(const Point3 &trans) {
  viewTranslation[0] = trans[0];
  viewTranslation[1] = trans[1];
  viewTranslation[2] = trans[2];
}


void Video::setModelRotation(const Quat &q) {
  Matrix3 matrix = q.toMatrix3();

  for ( int i=0; i < 3; i++ )
    for ( int j=0; j < 3; j++ )
      modelRotation[i*4 + j] = matrix(i, j);
}


// sets view rotation transformation using Euler angles
void Video::setViewRotation(const Quat &q) {
  Matrix3 matrix = q.toMatrix3();

  for ( int i=0; i < 3; i++ )
    for ( int j=0; j < 3; j++ )
      viewRotation[i*4 + j] = matrix(i, j);
}


void Video::applyTransforms() {
  glPushAttrib(GL_TRANSFORM_BIT);
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
  
  glMultMatrixf(viewRotation);
  glTranslatef(viewTranslation[0],
               viewTranslation[1],
               viewTranslation[2]);

  // now that view transformation has been done, set lights
  // since they will be transformed by current modelview matrix,
  // and we do not want model transformations being applied to
  // light positions
  //setLights();

  glTranslatef(modelTranslation[0],
               modelTranslation[1],
               modelTranslation[2]);
  glMultMatrixf(modelRotation);
  glPopAttrib();
}


void Video::toggleLight(int lightIndex, bool on) {
  if ( lightIndex >= 0 && lightIndex < MAX_LIGHTS ) {
    lightEnabled[lightIndex] = on;

    if ( lightIndex < 8 ) {
      if ( true == on )
        glEnable( lightGLEnum[lightIndex] );
      else
        glDisable( lightGLEnum[lightIndex] );
    }
  }
}


void Video::setLight(int lightIndex, const Video::Light &l) {
  if ( lightIndex >= 0 && lightIndex < MAX_LIGHTS )
    lights[lightIndex] = l;
}


void Video::setupLights() {
  for ( int i=0; i < MAX_LIGHTS; i++ ) {
    if ( true == lightEnabled[i] ) {
      GLenum lightEnum = lightGLEnum[i];
      glLightfv(lightEnum, GL_POSITION, lights[i].pos);
      glLightfv(lightEnum, GL_AMBIENT, lights[i].ambient);
      glLightfv(lightEnum, GL_DIFFUSE, lights[i].diffuse);
      glLightfv(lightEnum, GL_SPECULAR, lights[i].specular);
      glLightf (lightEnum, GL_CONSTANT_ATTENUATION, lights[i].constAtt);
      glLightf (lightEnum, GL_LINEAR_ATTENUATION, lights[i].linearAtt);
      glLightf (lightEnum, GL_QUADRATIC_ATTENUATION, lights[i].quadAtt);
    }
  }
}


void Video::setMaterial(const Video::Material &mat) {
  glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, mat.ambient);
  glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, mat.diffuse);
  glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, mat.specular);
  glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, mat.shininess);
}


// checks OpenGL's error log and throws an exception if an error
// has occured
void Video::checkGLError() {
  GLenum err = glGetError();

  // if there hasn't been any OpenGL errors, return
  if ( GL_NO_ERROR == err )
    return;

  // there has been an OpenGL error, so find out which kind it was
  std::string msg("Video::checkGLError(): ");
  switch( err ) {
    case GL_INVALID_ENUM:
      msg += "GL_INVALID_ENUM";
      break;
    case GL_INVALID_OPERATION:
      msg += "GL_INVALID_OPERATION";
      break;
    case GL_STACK_OVERFLOW:
      msg += "GL_STACK_OVERFLOW";
      break;
    case GL_STACK_UNDERFLOW:
      msg += "GL_STACK_UNDERFLOW";
      break;
    case GL_OUT_OF_MEMORY:
      msg += "GL_OUT_OF_MEMORY";
      break;
  } // switch

  // report error by throwing an exception
  throw std::runtime_error(msg);
}


Frustum Video::getFrustum() {
  Frustum frustum;

  // make sure modelview matrix is up to date
  //setGLModelview();

  GLfloat proj[16]; // to store projection matrix
  GLfloat modl[16]; // to store modelview matrix
  GLfloat clip[16]; // to store product of model view and projection matrices

  // get modelview matrix
  glGetFloatv(GL_MODELVIEW_MATRIX, modl);

  // get projection matrix
  glGetFloatv(GL_PROJECTION_MATRIX, proj);

  // check for OpenGL errors (might happen if this is called in a
  // glBegin()..glEnd() block)
  checkGLError();

  // multiply modelview and projection matrices
  clip[ 0] = modl[ 0] * proj[ 0] + modl[ 1] * proj[ 4] + modl[ 2] * proj[ 8] + modl[ 3] * proj[12];
  clip[ 1] = modl[ 0] * proj[ 1] + modl[ 1] * proj[ 5] + modl[ 2] * proj[ 9] + modl[ 3] * proj[13];
  clip[ 2] = modl[ 0] * proj[ 2] + modl[ 1] * proj[ 6] + modl[ 2] * proj[10] + modl[ 3] * proj[14];
  clip[ 3] = modl[ 0] * proj[ 3] + modl[ 1] * proj[ 7] + modl[ 2] * proj[11] + modl[ 3] * proj[15];

  clip[ 4] = modl[ 4] * proj[ 0] + modl[ 5] * proj[ 4] + modl[ 6] * proj[ 8] + modl[ 7] * proj[12];
  clip[ 5] = modl[ 4] * proj[ 1] + modl[ 5] * proj[ 5] + modl[ 6] * proj[ 9] + modl[ 7] * proj[13];
  clip[ 6] = modl[ 4] * proj[ 2] + modl[ 5] * proj[ 6] + modl[ 6] * proj[10] + modl[ 7] * proj[14];
  clip[ 7] = modl[ 4] * proj[ 3] + modl[ 5] * proj[ 7] + modl[ 6] * proj[11] + modl[ 7] * proj[15];

  clip[ 8] = modl[ 8] * proj[ 0] + modl[ 9] * proj[ 4] + modl[10] * proj[ 8] + modl[11] * proj[12];
  clip[ 9] = modl[ 8] * proj[ 1] + modl[ 9] * proj[ 5] + modl[10] * proj[ 9] + modl[11] * proj[13];
  clip[10] = modl[ 8] * proj[ 2] + modl[ 9] * proj[ 6] + modl[10] * proj[10] + modl[11] * proj[14];
  clip[11] = modl[ 8] * proj[ 3] + modl[ 9] * proj[ 7] + modl[10] * proj[11] + modl[11] * proj[15];

  clip[12] = modl[12] * proj[ 0] + modl[13] * proj[ 4] + modl[14] * proj[ 8] + modl[15] * proj[12];
  clip[13] = modl[12] * proj[ 1] + modl[13] * proj[ 5] + modl[14] * proj[ 9] + modl[15] * proj[13];
  clip[14] = modl[12] * proj[ 2] + modl[13] * proj[ 6] + modl[14] * proj[10] + modl[15] * proj[14];
  clip[15] = modl[12] * proj[ 3] + modl[13] * proj[ 7] + modl[14] * proj[11] + modl[15] * proj[15];

  // calculate right clipping plane
  Vector3 right;
  right[0] = float(clip[3]  - clip[0]);
  right[1] = float(clip[7]  - clip[4]);
  right[2] = float(clip[11] - clip[8]);
  float rightD = float(clip[15] - clip[12]),
         mag = right.mag();
  right  /= mag;
  rightD /= -mag;
  frustum.rightP = Plane(right, rightD);

  // calculate left clipping plane
  Vector3 left;
  left[0] = float(clip[ 3] + clip[ 0]);
  left[1] = float(clip[ 7] + clip[ 4]);
  left[2] = float(clip[11] + clip[ 8]);
  float leftD = float(clip[15] + clip[12]);
  mag = left.mag();
  left  /= mag;
  leftD /= -mag;
  frustum.leftP = Plane(left, leftD);

  // calculate bottom clipping plane
  Vector3 bottom;
  bottom[0] = float(clip[ 3] + clip[ 1]);
  bottom[1] = float(clip[ 7] + clip[ 5]);
  bottom[2] = float(clip[11] + clip[ 9]);
  float bottomD = float(clip[15] + clip[13]);
  mag = bottom.mag();
  bottom  /= mag;
  bottomD /= -mag;
  frustum.bottomP = Plane(bottom, bottomD);

  // calculate top clipping plane
  Vector3 top;
  top[0] = float(clip[ 3] - clip[ 1]);
  top[1] = float(clip[ 7] - clip[ 5]);
  top[2] = float(clip[11] - clip[ 9]);
  float topD = float(clip[15] - clip[13]);
  mag = top.mag();
  top  /= mag;
  topD /= -mag;
  frustum.topP = Plane(top, topD);

  // calculate far clipping plane
  Vector3 farP;
  farP[0] = float(clip[ 3] - clip[ 2]);
  farP[1] = float(clip[ 7] - clip[ 6]);
  farP[2] = float(clip[11] - clip[10]);
  float farD = float(clip[15] - clip[14]);
  mag = farP.mag();
  farP  /= mag;
  farD /= -mag;
  frustum.farP = Plane(farP, farD);
  
  // calculate near clipping plane
  Vector3 nearP;
  nearP[0] = float(clip[ 3] + clip[ 2]);
  nearP[1] = float(clip[ 7] + clip[ 6]);
  nearP[2] = float(clip[11] + clip[10]);
  float nearD = float(clip[15] + clip[14]);
  mag = nearP.mag();
  nearP  /= mag;
  nearD /= -mag;
  frustum.nearP = Plane(nearP, nearD);

  return frustum;
}


void Video::renderTris(const Vertex *verts, int numTris) {
  if ( NULL == verts || numTris <= 0 )
    return;

  glVertexPointer(3, GL_FLOAT, (GLsizei)sizeof(Vertex), verts[0].pos.m);
  glNormalPointer(GL_FLOAT, (GLsizei)sizeof(Vertex), verts[0].faceNormal.m);

  if ( shadersSupported ) {
    glClientActiveTextureARB(GL_TEXTURE0_ARB);
    glTexCoordPointer(2, GL_FLOAT, (GLsizei)sizeof(Vertex), verts[0].tc);

    glClientActiveTextureARB(GL_TEXTURE2_ARB);
    glTexCoordPointer(3, GL_FLOAT, (GLsizei)sizeof(Vertex), verts[0].faceTangent.m);
  }
  else {
    glTexCoordPointer(2, GL_FLOAT, (GLsizei)sizeof(Vertex), verts[0].tc);
  }

  glDrawArrays( GL_TRIANGLES, 0, GLsizei(numTris*3) );
}


void Video::renderIndexedTris(const Video::Vertex *verts,
                              const unsigned int *indices,
                              int numTris) {
  if ( NULL == verts || NULL == indices || numTris <= 0 )
    return;

  glVertexPointer(3, GL_FLOAT, (GLsizei)sizeof(Vertex), verts[0].pos.m);

  if ( true == normalMappingEnabled )
    glNormalPointer(GL_FLOAT, (GLsizei)sizeof(Vertex), verts[0].faceNormal.m);
  else
    glNormalPointer(GL_FLOAT, (GLsizei)sizeof(Vertex), verts[0].vertNormal.m);

  if ( true == shadersSupported ) {
    glClientActiveTextureARB(GL_TEXTURE0_ARB);
    glTexCoordPointer(2, GL_FLOAT, (GLsizei)sizeof(Vertex), verts[0].tc);

    glClientActiveTextureARB(GL_TEXTURE2_ARB);
    glTexCoordPointer(3, GL_FLOAT, (GLsizei)sizeof(Vertex), verts[0].faceTangent.m);
  }
  else {
    glTexCoordPointer(2, GL_FLOAT, (GLsizei)sizeof(Vertex), verts[0].tc);
  }

  glDrawElements(GL_TRIANGLES, GLsizei(numTris*3), GL_UNSIGNED_INT, indices);
}


// I probably should not have made one function to render
// both triangles and quads... oh well
void Video::renderQuads(const Vertex *verts, int numQuads) {
  if ( NULL == verts || numQuads <= 0 )
    return;

  glVertexPointer(3, GL_FLOAT, (GLsizei)sizeof(Vertex), verts[0].pos.m);
  glNormalPointer(GL_FLOAT, (GLsizei)sizeof(Vertex), verts[0].faceNormal.m);

  if ( shadersSupported ) {
    glClientActiveTextureARB(GL_TEXTURE0_ARB);
    glTexCoordPointer(2, GL_FLOAT, (GLsizei)sizeof(Vertex), verts[0].tc);

    glClientActiveTextureARB(GL_TEXTURE2_ARB);
    glTexCoordPointer(3, GL_FLOAT, (GLsizei)sizeof(Vertex), verts[0].faceTangent.m);
  }
  else {
    glTexCoordPointer(2, GL_FLOAT, (GLsizei)sizeof(Vertex), verts[0].tc);
  }

  glDrawArrays( GL_QUADS, 0, GLsizei(numQuads*4) );
}


bool Video::initShaders() {
  // get list of OpenGL extensions
	const char *ext = (const char *)glGetString(GL_EXTENSIONS); 

  // check if required shader functionality is available
  if ( !strstr(ext, "GL_ARB_shading_language_100") ||
       !strstr(ext, "GL_ARB_vertex_shader")        ||
       !strstr(ext, "GL_ARB_fragment_shader")      ||
       !strstr(ext, "GL_ARB_shader_objects") )
     return false;
     //throw std::runtime_error("Video::initShaders(): GLSL not supported");

  // get function pointers for OpenGL extensions
  glCreateProgramObjectARB      = (PFNGLCREATEPROGRAMOBJECTARBPROC)SDL_GL_GetProcAddress("glCreateProgramObjectARB");
  glDeleteObjectARB             = (PFNGLDELETEOBJECTARBPROC)SDL_GL_GetProcAddress("glDeleteObjectARB");
  glUseProgramObjectARB         = (PFNGLUSEPROGRAMOBJECTARBPROC)SDL_GL_GetProcAddress("glUseProgramObjectARB");
  glCreateShaderObjectARB       = (PFNGLCREATESHADEROBJECTARBPROC)SDL_GL_GetProcAddress("glCreateShaderObjectARB");
  glShaderSourceARB             = (PFNGLSHADERSOURCEARBPROC)SDL_GL_GetProcAddress("glShaderSourceARB");
  glCompileShaderARB            = (PFNGLCOMPILESHADERARBPROC)SDL_GL_GetProcAddress("glCompileShaderARB");
  glGetObjectParameterivARB     = (PFNGLGETOBJECTPARAMETERIVARBPROC)SDL_GL_GetProcAddress("glGetObjectParameterivARB");
  glAttachObjectARB             = (PFNGLATTACHOBJECTARBPROC)SDL_GL_GetProcAddress("glAttachObjectARB");
  glGetInfoLogARB               = (PFNGLGETINFOLOGARBPROC)SDL_GL_GetProcAddress("glGetInfoLogARB");
  glLinkProgramARB              = (PFNGLLINKPROGRAMARBPROC)SDL_GL_GetProcAddress("glLinkProgramARB");
  glGetUniformLocationARB       = (PFNGLGETUNIFORMLOCATIONARBPROC)SDL_GL_GetProcAddress("glGetUniformLocationARB");
  glGetAttribLocationARB        = (PFNGLGETATTRIBLOCATIONARBPROC)SDL_GL_GetProcAddress("glGetAttribLocationARB");
  glVertexAttrib3fARB           = (PFNGLVERTEXATTRIB3FARBPROC)SDL_GL_GetProcAddress("glVertexAttrib3fARB");
  glVertexAttrib4fARB           = (PFNGLVERTEXATTRIB4FARBPROC)SDL_GL_GetProcAddress("glVertexAttrib4fARB");
  glVertexAttrib1fARB           = (PFNGLVERTEXATTRIB1FARBPROC)SDL_GL_GetProcAddress("glVertexAttrib1fARB");
  glUniform3fARB                = (PFNGLUNIFORM3FARBPROC)SDL_GL_GetProcAddress("glUniform3fARB");
  glUniform4fARB                = (PFNGLUNIFORM4FARBPROC)SDL_GL_GetProcAddress("glUniform4fARB");
  glUniform1iARB                = (PFNGLUNIFORM1IARBPROC)SDL_GL_GetProcAddress("glUniform1iARB");
  glEnableVertexAttribArrayARB  = (PFNGLENABLEVERTEXATTRIBARRAYPROC)SDL_GL_GetProcAddress("glEnableVertexAttribArrayARB");
  glDisableVertexAttribArrayARB = (PFNGLDISABLEVERTEXATTRIBARRAYARBPROC)SDL_GL_GetProcAddress("glDisableVertexAttribArrayARB");
  glVertexAttribPointerARB      = (PFNGLVERTEXATTRIBPOINTERARBPROC)SDL_GL_GetProcAddress("glVertexAttribPointerARB");
  glActiveTextureARB            = (PFNGLACTIVETEXTUREARBPROC)SDL_GL_GetProcAddress("glActiveTextureARB");
  glClientActiveTextureARB      = (PFNGLCLIENTACTIVETEXTUREARBPROC)SDL_GL_GetProcAddress("glClientActiveTextureARB");

   // make sure all of the shader functions were found
   if ( !glCreateProgramObjectARB      || !glDeleteObjectARB            || !glUseProgramObjectARB    ||
        !glCreateShaderObjectARB       || !glCreateShaderObjectARB      || !glCompileShaderARB       || 
        !glGetObjectParameterivARB     || !glAttachObjectARB            || !glGetInfoLogARB          || 
        !glLinkProgramARB              || !glGetUniformLocationARB      || !glUniform3fARB           ||
        !glGetAttribLocationARB        || !glVertexAttrib3fARB          || !glVertexAttrib1fARB      ||
        !glVertexAttrib4fARB           || !glEnableVertexAttribArrayARB || !glVertexAttribPointerARB ||
        !glUniform4fARB                || !glUniform1iARB               || !glActiveTextureARB       ||
        !glDisableVertexAttribArrayARB || !glClientActiveTextureARB )
      return false;
      //throw std::runtime_error("Video::initShaders(): could not obtain one "
      //                         "function pointers for one or more GLSL "
      //                         "functions");

  // create shader objects
  GLhandleARB vertShader = glCreateShaderObjectARB(GL_VERTEX_SHADER_ARB),
              fragShader = glCreateShaderObjectARB(GL_FRAGMENT_SHADER_ARB);
  
  // set source code for vertex shader
  GLint len = (GLint)strlen(vertShaderSrc);
  glShaderSourceARB(vertShader, 1, &vertShaderSrc, &len);

  // set source code for fragment shader
  len = (GLint)strlen(fragShaderSrc);
  glShaderSourceARB(fragShader, 1, &fragShaderSrc, &len);

  // compile the shaders
  glCompileShaderARB(vertShader);
  glCompileShaderARB(fragShader);

  // make sure compile of vertex shader was successful
  int status = 1;
  glGetObjectParameterivARB(vertShader, GL_OBJECT_COMPILE_STATUS_ARB, &status);

  if ( 0 == status ) {
    // there were compile errors
    char buffer[256];
    glGetInfoLogARB(vertShader, 256, NULL, buffer);
    throw std::runtime_error(buffer);
  }

   // make sure compile of fragment shader was successful
  status = 1;
  glGetObjectParameterivARB(fragShader, GL_OBJECT_COMPILE_STATUS_ARB, &status);

  if ( 0 == status ) {
    // there were compile errors
    char buffer[256];
    glGetInfoLogARB(fragShader, 256, NULL, buffer);
    throw std::runtime_error(buffer);
  }

  // create a shader program
  GLhandleARB shaderProgram = glCreateProgramObjectARB();

  // attach shaders to program before linking
  glAttachObjectARB(shaderProgram, vertShader);
  glAttachObjectARB(shaderProgram, fragShader);

  // link the program
  glLinkProgramARB(shaderProgram);

  // make sure link was successfull
  status = 1;
  glGetObjectParameterivARB(shaderProgram, GL_OBJECT_LINK_STATUS_ARB, &status);

  if ( 0 == status ) {
    // there were compile errors
    char buffer[256];
    glGetInfoLogARB(shaderProgram, 256, NULL, buffer);
    throw std::runtime_error(buffer);
  }

  // activate the program
  glUseProgramObjectARB(shaderProgram);

  // get location of uniform variable in shader program
  GLint loc = glGetUniformLocationARB(shaderProgram, "normalMap");

  // was uniform variable found in shader program?
  if ( loc < 0 )
    throw std::runtime_error("uniform variable normalMap not found");

  glUniform1iARB(loc, 1);

  // clean up vertex and fragment shader objects since they are no longer
  // needed (only program object is needed)
  glDeleteObjectARB(vertShader);
  glDeleteObjectARB(fragShader);

  normalMapShader = (int)shaderProgram;
  normalMappingEnabled = true;

  // return true to indicate shaders are supported
  return true;
}


void Video::enableNormalMapping() {
  if ( !shadersSupported || normalMappingEnabled )
    return;

  normalMappingEnabled = true;

  glUseProgramObjectARB( GLhandleARB(normalMapShader) );

  checkGLError();
}


void Video::disableNormalMapping() {
  if ( !shadersSupported || !normalMappingEnabled )
    return;

  normalMappingEnabled = false;

  // used fixed function pipeline
  glUseProgramObjectARB(0);

  checkGLError();
}


void Video::enableTextures() {
  if ( getTexturesEnabled() == true )
    return;

  glEnable(GL_TEXTURE_2D);
  texturesEnabled = true;
}


void Video::disableTextures() {
  if ( getTexturesEnabled() == false )
    return;

  glDisable(GL_TEXTURE_2D);
  glColor3f(1.0f, 1.0f, 1.0f);
  texturesEnabled = false;
}


// computes face normal and tangent of a triangle defined by
// v1, v2, v3 in counter-clockwise order.  Vertex positions
// and texture coordinates must already be set before this method
// is called.
void Video::computeTangentAndNormal(Video::Vertex &v1,
                                    Video::Vertex &v2,
                                    Video::Vertex &v3) {
  Math3D::Vector3<float> v2v1 = v2.pos - v1.pos,
                         v3v1 = v3.pos - v1.pos;

  float c2c1T = v2.tc[0] - v1.tc[0],
        c2c1B = v2.tc[1] - v1.tc[1],
        c3c1T = v3.tc[0] - v1.tc[0],
        c3c1B = v3.tc[1] - v1.tc[1];
  float invDet = 1.0f/(c2c1T*c3c1B - c3c1T*c2c1B);

  // for brevity
  Math3D::Vector3<float> t, n;

  // normalize basis vectors
  t = (v2v1*c3c1B  - v3v1*c2c1B)*invDet;
  n = Math3D::Vector3<float>::cross(v3v1, v2v1);

  // normalize tangent space vectors
  t.normalize();
  n.normalize();

  v1.faceNormal  = v2.faceNormal  = v3.faceNormal  = n;
  v1.faceTangent = v2.faceTangent = v3.faceTangent = t;
}


// default material constructor, just sets all reflectivities to zero
Video::Material::Material() {
  for ( int i=0; i < 4; i++ )
    ambient[i] = diffuse[i] = specular[i] = 0.0f;

  shininess = 0.0f;
}
