/***************************************************************************
 *   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.                                   *
 *                                                                         *
 *   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, write to the                         *
 *   Free Software Foundation, Inc.,                                       *
 *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
 ***************************************************************************/

#include <iostream>
#include <string>
#include <cmath>
#include <cstring>
#include <cstddef>
#include <cctype>

#include "MD5Model.h"

#define IS_WHITESPACE(c) (' ' == c || '\t' == c || '\r' ==c || '\n' == c )

Quat md5Rot = Quat::fromEulerAngles(-90.0f, 0.0f, 0.0f);


MD5Model::MD5Model():
     numJoints(0),
     numMeshes(0),
     currAnim(-1),
     currFrame(0),
     animTime(0.0f) {

}


MD5Model::~MD5Model() {
  clear();
}


void MD5Model::clear() {
  // delete meshes
  for ( size_t i=0; i < meshes.size(); i++ )
    if ( meshes[i] )
      delete meshes[i];

  // delete animations
  for ( size_t i=0; i < anims.size(); i++ )
    if ( anims[i] )
      delete anims[i];

  meshes.clear();
  anims.clear();
  joints.clear();
}


int MD5Model::getNumAnimFrames(int animIndex) const {
  // make sure animation index is valid
  if ( animIndex >= getNumAnims() )
    throw Exception("MD5Model::getNumAnimFrames(): animation "
                    "index is invalid");

  Anim *anim = anims[animIndex];

  // if animation is non-NULL, return number of frames in it
  if ( anim != NULL )
    return anim->numFrames;

  return 0;
}


int MD5Model::getAnimFrameRate(int animIndex) const {
  // make sure animation index is valid
  if ( animIndex >= getNumAnims() )
    throw Exception("MD5Model::getAnimFrameRate(): animation"
                    " index is invalid");

  Anim *anim = anims[animIndex];

  if ( NULL == anim )
    throw Exception("MD5Model::getAnimFrameRate(): animation is NULL");

  // if animation is non-NULL, return frame rate
  return anim->frameRate;
}


// returns bounding box of model's current frame
void MD5Model::getBoundingBox(float *min, float *max) const {
  // make sure there is an animation
  if ( anims.size() == 0 || currAnim < 0 )
    throw Exception("MD5Model::getBoundingBox(): no animation has been set");

  // make sure current animation is valid
  if ( currAnim < 0 || currAnim >= int(anims.size()) || !anims[currAnim] )
    throw Exception("MD5Model::animate(): currAnim is invalid");

  // make sure current frame is valid
  if ( currFrame < 0 || currFrame >= int(anims[currAnim]->numFrames) )
    throw Exception("MD5Model::animate(): currFrame is invalid");

  const Frame &frame = anims[currAnim]->frames[currFrame];

  // finally... get min and max coords of bounding box
  if ( min != NULL ) {
    min[0] = frame.min[0];
    min[1] = frame.min[1];
    min[2] = frame.min[2];
  }

  if ( max != NULL ) {
    max[0] = frame.max[0];
    max[1] = frame.max[1];
    max[2] = frame.max[2];
  }
}


AABB MD5Model::getBoundingBox() const {
  float aabbMin[3], aabbMax[3];
  getBoundingBox(aabbMin, aabbMax);
  return AABB( Point3(aabbMin[0], aabbMin[1], aabbMin[2]),
               Point3(aabbMax[0], aabbMax[1], aabbMax[2]) );
}


// removes any translation of the root node
void MD5Model::removeRootTranslation() {
  // iterate through each animation
  for ( size_t i=0; i < anims.size(); i++ ) {
    Anim *anim = anims[i];

    // iterate through each frame of animation
    for ( size_t j=0; j < anim->frames.size(); j++ ) {
      Frame &frame = anim->frames[j];

      // remove translation from root joint
      if ( !frame.joints.empty() ) {
        frame.joints[0].pos[0] = 0.0f;
        frame.joints[0].pos[1] = 0.0f;
        frame.joints[0].pos[2] = 0.0f;
      }
    } // for (frames)
  } // for (animations)
}


void MD5Model::animate(float dt) {
  // sanity check #1
  if ( currAnim < 0 || currAnim >= int(anims.size()) || !anims[currAnim] )
    throw Exception("MD5Model::animate(): currAnim is invalid");

  Anim *anim = anims[currAnim];

  // sanity check #2
  if ( currFrame < 0 || currFrame >= int(anim->numFrames) )
    throw Exception("MD5Model::animate(): currFrame is invalid");

  // compute index of next frame
  int nextFrameIndex = currFrame >= anim->numFrames - 1 ? 0 : currFrame + 1;

  // update animation time
  animTime += dt*float(anim->frameRate);
  if ( animTime > 1.0f ) {
    while ( animTime > 1.0f )
      animTime -= 1.0f;
    //setFrame(nextFrameIndex);
    currFrame = nextFrameIndex;
    nextFrameIndex = currFrame >= anim->numFrames - 1 ? 0 : currFrame + 1;
  }

  // make sure size of storage for interpolated frame is correct
  if ( int(interpFrame.joints.size()) != numJoints )
    interpFrame.joints.resize(numJoints);

  ///// now interpolate between the two frames /////

  Frame &frame     = anim->frames[currFrame],
        &nextFrame = anim->frames[nextFrameIndex];

  // interpolate between the joints of the current frame and those of the next
  // frame and store them in interpFrame
  for ( int i=0; i < numJoints; i++ ) {
    Joint &interpJoint = interpFrame.joints[i];

    // linearly interpolate between joint positions
    float *pos1 = frame.joints[i].pos,
          *pos2 = nextFrame.joints[i].pos;
    interpJoint.pos[0] = pos1[0] + animTime*(pos2[0] - pos1[0]);
    interpJoint.pos[1] = pos1[1] + animTime*(pos2[1] - pos1[1]);
    interpJoint.pos[2] = pos1[2] + animTime*(pos2[2] - pos1[2]);

    interpJoint.quat = slerp(frame.joints[i].quat,
                             nextFrame.joints[i].quat,
                             animTime);
  }

  buildVerts(interpFrame);
  buildTBN();
}


// sets current animation, frame, and animation time
void MD5Model::setAnim(int animIndex, int frameIndex, float t) {
  // sanity check #1
  if ( animIndex < 0 || animIndex >= int(anims.size()) ||
       NULL == anims[animIndex] )
    throw Exception("MD5Model::setAnim(): invalid animation index");

  // sanity check #2
  if ( frameIndex < 0 || anims[animIndex]->numFrames <= frameIndex )
    throw Exception("MD5Model::setAnim(): frame index is invalid");

  if ( currAnim != animIndex || currFrame != frameIndex ) {
    currAnim  = animIndex;
    currFrame = frameIndex;
    animTime = t;
    animate(0.0f); // builds mesh
  }
}


void MD5Model::setFrame(int frameIndex) {
  // sanity check #1
  if ( anims.size() == 0 || currAnim < 0 )
    throw Exception("MD5Model::setFrame(): no animation has been set");

  // sanity check #2
  if ( frameIndex < 0 || !anims[currAnim] ||
       anims[currAnim]->numFrames <= 0 ||
       anims[currAnim]->numFrames <= frameIndex )
    throw Exception("MD5Model::setFrame(): frame index is invalid");

  buildVerts(anims[currAnim]->frames[frameIndex]);
  buildTBN();
  currFrame = frameIndex;
  animTime = 0.0f;
}


void MD5Model::loadMesh(const char *filename) {
  // sanity check
  if ( !filename )
    throw Exception("MD5Model::loadMesh(): filename is NULL");

  // attempt to open file for reading
  std::ifstream fin(filename, std::ifstream::in);

  // was open successful?
  if ( !fin.is_open() ) {
    std::string msg = std::string("MD5Model::loadMesh(): unable to open ") +
                      std::string(filename) + std::string(" for reading");
    throw Exception(msg);
  }

  // read in file version
  std::string str;
  getNextToken(fin, &str);

  // token must read "MD5Version"
  if ( str != "MD5Version" )
    throw Exception("MD5Model::loadMesh(): expected 'MD5Version'");

  // get version #
  int ver = readInt(fin);

  // must be version 10
  if ( ver != 10 )
    throw Exception("MD5Model::loadMesh(): MD5Version must be 10");

  // clear any data before reading file
  clear();

  // read in all of the MD5Model elements
  readElements(fin);

  // close input file (should be done destructor anyway...)
  fin.close();

  // allocate storage for vertices
  size_t numTris = 0;
  for ( size_t i=0; i < meshes.size(); i++ )
    if ( meshes[i] != NULL )
      numTris += meshes[i]->tris.size();
  //verts.resize(numTris*3);
  vertsInfo.resize(numTris*3);

  // calculate vertex positions and normals from information in joints
  buildVerts();
  buildTBN();
}


int MD5Model::loadAnim(const char *filename) {
  // attempt to open file for reading
  std::ifstream fin(filename, std::ifstream::in);

  if ( !fin.is_open() ) {
    std::string msg = std::string("MD5Model::loadAnim(): unable to open ") +
                      std::string(filename) + std::string(" for reading");
    throw Exception(msg);
  }

  // read in file version
  std::string str;
  getNextToken(fin, &str);

  // token must read "MD5Version"
  if ( str != "MD5Version" )
    throw Exception("MD5Model::loadAnim(): expected 'MD5Version'");

  // get version #
  int ver = readInt(fin);

  // must be version 10
  if ( ver != 10 )
    throw Exception("MD5Model::loadAnim(): MD5Version must be 10");

  Anim *anim = new Anim;
  if ( !anim )
    throw Exception("MD5Model::loadAnim(): could not allocate storage"
                    " for animation");

  readAnimElements(fin, *anim);

  // close file (should be done by destructor anyway)
  fin.close();

  // add to array of animations
  int animIndex = (int)anims.size();
  anims.push_back(anim);

  // build frames for this animation
  buildFrames(*anim);

  // make this the current animation
  setAnim(animIndex, 0);

  return animIndex;
}


void MD5Model::readElements(std::ifstream &fin) {
  while ( !fin.eof() ) {
    std::string str;
    TOKEN tok = getNextToken(fin, &str);
  
    // token is TOKEN_INVALID when end of file is reached
    if ( TOKEN_INVALID == tok )
      break;
  
    if ( str == "commandline" )
      readCommandLineEl(fin);
    else if ( str == "numJoints" )
      readNumJointsEl(fin);
    else if ( str == "numMeshes" )
      readNumMeshesEl(fin);
    else if ( str == "joints" )
      readJointsEl(fin);
    else if ( str == "mesh" )
      readMeshEl(fin);
    else {
      // invalid element
      throw Exception( std::string("MD5Model::readElements(): unknown"
                                   " element type '") + str + "'");
    }
  } // while ( not EOF )
}


void MD5Model::readAnimElements(std::ifstream &fin, Anim &anim) {
  while ( !fin.eof() ) {
    std::string str;
    TOKEN tok = getNextToken(fin, &str);

    // token is TOKEN_INVALID when end of file is reached
    if ( TOKEN_INVALID == tok )
      break;

    if ( str == "commandline" )
      readCommandLineEl(fin);
    else if ( str == "numJoints" ) {
      // just make sure that the number of joints is the same as the number
      // specified in the mesh file
      int n = readInt(fin);

      if ( n != numJoints )
        throw Exception("MD5Model::readAnimElements(): anim file "
                        "does not match mesh");
    }
    else if ( str == "numMeshes" ) {
      // just make sure that the number of meshes is the same as the number
      // specified in the mesh file
      int n = readInt(fin);

      if ( n != numMeshes )
        throw Exception("MD5Model::readAnimElements(): anim file does"
                        " not match mesh");
    }
    else if ( str == "numFrames" )
      readNumFramesEl(fin, anim);
    else if ( str == "frameRate" )
      readFrameRateEl(fin, anim);
    else if ( str == "numAnimatedComponents" )
      readNumAnimatedComponentsEl(fin, anim);
    else if ( str == "hierarchy" )
      readHierarchyEl(fin, anim);
    else if ( str == "bounds" )
      readBoundsEl(fin, anim);
    else if ( str == "baseframe" )
      readBaseframeEl(fin, anim);
    else if ( str == "frame" )
      readFrameEl(fin, anim);
    else {
      // invalid element
      throw Exception( std::string("MD5Model::readAnimElements(): "
                                   "unknown element type '") + str + "'");
    }
  }
}


void MD5Model::readCommandLineEl(std::ifstream &fin) {
  // commandline elements can be ignored, but make sure the syntax is correct
  if ( getNextToken(fin) != TOKEN_STRING )
    throw Exception("MD5Model::readCommandLineEl(): expected string");
}


void MD5Model::readNumJointsEl(std::ifstream &fin) {
  // if number of joints has already been set, can't set it again
  if ( numJoints != 0 )
    throw Exception("MD5Model::readNumJointsEl(): numJoints has"
                    " already been set");

  // read in next token, should be an integer
  int n = readInt(fin);

  if ( n <= 0 )
    throw Exception("MD5Model::readNumJointsEl(): numJoints must"
                    " be greater than 0");

  numJoints =  n;
  //joints.resize(numJoints);
}


void MD5Model::readNumMeshesEl(std::ifstream &fin) {
  // if number of meshes has already been set, can't set it again
  if ( numMeshes != 0 )
    throw Exception("MD5Model::readNumMeshesEl(): numMeshes"
                    " has already been set");

  // read in next token, should be an integer
  int n = readInt(fin);

  if ( n <= 0 )
    throw Exception("MD5Model::readNumMeshesEl(): numMeshes"
                    " must be greater than 0");

  numMeshes =  n;
  //meshes.resize(numMeshes);
}


void MD5Model::readNumFramesEl(std::ifstream &fin, Anim &anim) {
  // if number of frames has already been set, can't set it again
  if ( anim.numFrames != 0 )
    throw Exception("MD5Model::readNumFramesEl(): "
                    "numFrames has already been set");

  // read in next token, should be an integer
  int n = readInt(fin);

  if ( n <= 0 )
    throw Exception("MD5Model::readNumFramesEl(): "
                    " numFrames must be greater than 0");

  anim.numFrames =  n;
  anim.frames.resize(n);
}


void MD5Model::readFrameRateEl(std::ifstream &fin, Anim &anim) {
  // if framerate has already been set, can't set it again
  if ( anim.frameRate != 0 )
    throw Exception("MD5Model::readFrameRateEl(): "
                    "frameRate has already been set");

  // read in next token, should be an integer
  int n = readInt(fin);

  if ( n <= 0 )
    throw Exception("MD5Model::readFrameRateEl(): "
                    "frameRate must be a positive integer");

  anim.frameRate =  n;
}


void MD5Model::readNumAnimatedComponentsEl(std::ifstream &fin, Anim &anim) {
  // make sure parameter hasn't been set, as has been done with all of the
  // others
  if ( anim.numAnimatedComponents != 0 )
    throw Exception("MD5Model::readNumAnimatedComponentsEl(): "
                    " numAnimatedComponents has already been set");

  // read in next token, should be an integer
  int n = readInt(fin);

  if ( n <= 0 )
    throw Exception("MD5Model::readNumAnimatedComponentsEl(): "
                    "numAnimatedComponents must be a positive integer");

  anim.numAnimatedComponents = n;
}


void MD5Model::readJointsEl(std::ifstream &fin) {
  // make sure numJoints has been set
  if ( numJoints <= 0 )
    throw Exception("MD5Model::readJointsEl(): numJoints needs to be "
                    "set before 'joints' block");

  TOKEN t = getNextToken(fin);

  // expect an opening brace { to begin block of joints
  if ( t != TOKEN_LBRACE )
    throw Exception("MD5Model::readJointsEl(): expected { to follow 'joints'");

  // read in each joint in block until '}' is hit
  // (if EOF is reached before this, then the read*() methods
  //  will close the file and throw an exception)
  std::string str;
  t = getNextToken(fin, &str);
  while ( t != TOKEN_RBRACE ) {
    Joint joint;

    // token should be name of joint (a string)
    if ( t != TOKEN_STRING )
      throw Exception("MD5Model::readJointsEl(): "
                      "expected joint name (string)'");

    // read in index of parent joint
    int parentIndex = readInt(fin);
  
    // read in joint position
    readVec(fin, joint.pos, 3);

    // swap y and z
    //float temp = joint.pos[1];
    //joint.pos[1] = joint.pos[2];
    //joint.pos[2] = temp;
  
    // read in first 3 components of quaternion (must compute 4th)
    float quat[4];
    readVec(fin, quat, 3);

    joint.quat = buildQuat(quat[0], quat[1], quat[2]);

    // add index to parent's list of child joints (if it has a parent,
    // root joints have parent index -1)
    if ( parentIndex >= 0 ) {
      if ( size_t(parentIndex) >= joints.size() )
        throw Exception("MD5Model::readJointsEl(): parent index is invalid");

      joints[parentIndex].children.push_back( int(joints.size()) );
    }

    // add joint to vector of joints
    joints.push_back(joint);

    // get next token
    t = getNextToken(fin, &str);
  }
}


void MD5Model::readMeshEl(std::ifstream &fin) {
  // make sure numMeshes has been set
  if ( numMeshes <= 0 )
    throw Exception("MD5Model::readMeshesEl(): numMeshes needs "
                    "to be set before 'mesh' block");

  TOKEN t = getNextToken(fin);

  // expect an opening brace { to begin block of joints
  if ( t != TOKEN_LBRACE )
    throw Exception("MD5Model::readMeshEl(): expected { to follow 'mesh'");

  Mesh *mesh = new Mesh;

  // read in all mesh information in block until '}' is hit
  // (if EOF is reached before this, then the read*() methods
  //  will close the file and throw an exception)
  std::string str;
  t = getNextToken(fin, &str);
  while ( t != TOKEN_RBRACE ) {
    if ( str == "vert" ) {
      // syntax: vert (vertex index) '(' (u) (v) ')' (weight index) (weight count)
      Video::Vertex vert;
      VertexInfo vertInfo;

      int index = readInt(fin);
      readVec(fin, vert.tc, 2);
      // flip texture coordinates
      vert.tc[1] = 1.0f - vert.tc[1];
      vertInfo.weightIndex = readInt(fin);
      vertInfo.weightCount = readInt(fin);

      // make sure index is within bounds of vector of vertices
      if ( size_t(index) >= mesh->verts.size() )
        throw Exception("MD5Model::readMeshEl(): vertex index >= numverts");

      mesh->verts[index] = vert;
      mesh->vertsInfo[index] = vertInfo;
    }
    else if ( str == "tri" ) {
      // syntax: tri (triangle index) (v0 index) (v1 index) (v2 index)
      Tri tri;

      int index = readInt(fin);

      // make sure index is within bounds of vector of triangles
      if ( size_t(index) >= mesh->tris.size() )
        throw Exception("MD5Model::readMeshEl(): triangle index >= numtris");

      // read in triangles.  reverse order because MD5 uses
      // clockwise vertex ordering, but this engine uses
      // counter-clockwise vertex ordering.
      tri.v[2] = (unsigned int)readInt(fin);
      tri.v[1] = (unsigned int)readInt(fin);
      tri.v[0] = (unsigned int)readInt(fin);
      mesh->tris[index] = tri;
    }
    else if ( str == "weight" ) {
      Weight weight;

      int weightIndex = readInt(fin);
      weight.joint = readInt(fin);
      weight.w = readFloat(fin);
      readVec(fin, weight.pos, 3);

      // swap y and z
      //float temp = weight.pos[1];
      //weight.pos[1] = weight.pos[2];
      //weight.pos[2] = temp;

      if ( size_t(weightIndex) >= mesh->weights.size() )
        throw Exception("MD5Model::readMeshEl(): weight "
                        "index >= numweights");

      mesh->weights[weightIndex] = weight;
    }
    else if ( str == "shader" ) {
      std::string shader;
      if ( getNextToken(fin, &shader) != TOKEN_STRING )
        throw Exception("MD5Model::readMeshEl(): expected string"
                        " to follow 'shader'");

      // look for material name
      TexIDMap::const_iterator itr = texIDs.find(shader);
      if ( texIDs.find(shader) != texIDs.end() ) {
        // texture has already been loaded
        std::pair<int, int> texIDPair = itr->second;
        mesh->texID = texIDPair.first;
        mesh->nmID  = texIDPair.second;
      }
      else {
        // texture has not been loaded yet, so load it now
        std::pair<int, int> texIDPair;
        std::string textureName   = shader,
                    normalMapName = shader;
        textureName   += ".tga";
        normalMapName += "_nm.tga";

        // load texture and normal map
        texIDPair.first  = Video::loadTexture(textureName.c_str(), false);
        texIDPair.second = Video::loadTexture(normalMapName.c_str(), true);

        mesh->texID = texIDPair.first;
        mesh->nmID  = texIDPair.second;
      }
    }
    else if ( str == "numverts" ) {
      if ( mesh->verts.size() > 0 )
        throw Exception("MD5Model::readMeshEl(): numverts has "
                        "already been set");

      int n = readInt(fin);

      if ( n <= 0 )
        throw Exception("MD5Model::readMeshEl(): numverts must"
                        " be greater than 0");

      mesh->verts.resize(n);
      mesh->vertsInfo.resize(n);
    }
    else if ( str == "numtris" ) {
      if ( mesh->tris.size() > 0 )
        throw Exception("MD5Model::readMeshEl(): numtris has already "
                        "been set");

      int n = readInt(fin);

      if ( n <= 0 )
        throw Exception("MD5Model::readMeshEl(): numtris must be greater"
                        " than 0");

      mesh->tris.resize(n);
    }
    else if ( str == "numweights" ) {
      if ( mesh->weights.size() > 0 )
        throw Exception("MD5Model::readMeshEl(): numweights has "
                        "already been set");

      int n = readInt(fin);

      if ( n <= 0 )
        throw Exception("MD5Model::readMeshEl(): numweights"
                        " must be greater than 0");

      mesh->weights.resize(n);
    }

    // read next token
    t = getNextToken(fin, &str);
  }

  meshes.push_back(mesh);
}


// reads in hierarchy block from anim file
void MD5Model::readHierarchyEl(std::ifstream &fin, Anim &anim) {
  TOKEN t = getNextToken(fin);

  // expect an opening brace { to begin hierarchy block
  if ( t != TOKEN_LBRACE )
    throw Exception("MD5Model::readHierarchyEl(): expected "
                    "{ to follow 'hierarchy'");

  anim.jointInfo.clear();

  // read in each joint in block until '}' is hit
  // (if EOF is reached before this, then the read*() methods
  //  will close the file and throw an exception)
  std::string str;
  t = getNextToken(fin, &str);
  while ( t != TOKEN_RBRACE ) {
    JointInfo info;

    // token should be name of a joint (a string)
    if ( t != TOKEN_STRING )
      throw Exception("MD5Model::readHierarchyEl(): expected name (string)");

    info.name        = str;
    info.parentIndex = readInt(fin);
    info.flags       = readInt(fin);
    info.startIndex  = readInt(fin);

    anim.jointInfo.push_back(info);

    // get next token
    t = getNextToken(fin, &str);
  }

  if ( int(anim.jointInfo.size()) != numJoints )
    throw Exception("MD5Model::readHierarchyEl(): number of entires"
                    " in hierarchy block does not match numJoints");
}


// reads in bounds block from anim file
void MD5Model::readBoundsEl(std::ifstream &fin, Anim &anim) {
  TOKEN t = getNextToken(fin);

  // expect an opening brace { to begin block
  if ( t != TOKEN_LBRACE )
    throw Exception("MD5Model::readBoundsEl(): expected { to follow 'bounds'");

  if ( anim.numFrames != int(anim.frames.size()) )
    throw Exception("MD5Model::readBoundsEl(): frames must be allocated "
                    "before setting bounds");

  // read in bound for each frame
  for ( int i=0; i < anim.numFrames; i++ ) {
    readVec(fin, anim.frames[i].min, 3);
    readVec(fin, anim.frames[i].max, 3);
  }

  // next token must be a closing brace }
  t = getNextToken(fin);

  if ( TOKEN_LPAREN == t )
    throw Exception("MD5Model::readBoundsEl(): number of bounds exceeds "
                    "number of frames");

  // expect a closing brace } to end block
  if ( t != TOKEN_RBRACE )
    throw Exception("MD5Model::readBoundsEl(): expected }");
}


void MD5Model::readBaseframeEl(std::ifstream &fin, Anim &anim) {
  TOKEN t = getNextToken(fin);

  // expect an opening brace { to begin block
  if ( t != TOKEN_LBRACE )
    throw Exception("MD5Model::readBaseframeEl(): expected { to"
                    " follow 'baseframe'");

  anim.baseJoints.resize(numJoints);

  int i;
  for ( i=0; i < numJoints; i++ ) {
    // read in joint position
    readVec(fin, anim.baseJoints[i].pos, 3);

    // swap y and z
    //float temp = anim.baseJoints[i].pos[1];
    //anim.baseJoints[i].pos[1] = anim.baseJoints[i].pos[2];
    //anim.baseJoints[i].pos[2] = temp;

    // read in first 3 components of quaternion (must compute 4th)
    float quat[3];
    readVec(fin, quat, 3);

    anim.baseJoints[i].quat = buildQuat(quat[0], quat[1], quat[2]);
  }

  if ( i < numJoints - 1 )
    throw Exception("MD5Model::readBaseframeEl(): not enough joints");

  // next token must be a closing brace }
  t = getNextToken(fin);

  if ( TOKEN_LPAREN == t )
    throw Exception("MD5Model::readBaseframeEl(): too many joints");

  // expect a closing brace } to end block
  if ( t != TOKEN_RBRACE )
    throw Exception("MD5Model::readBaseframeEl(): expected }");
}


void MD5Model::readFrameEl(std::ifstream &fin, Anim &anim) {
  // numAnimatedComponents has to have been set before frame element
  if ( 0 == anim.numAnimatedComponents )
    throw Exception("MD5Model::readFrameEl(): numAnimatedComponents"
                    " must be set before 'frame' block");

  // read frame index
  int frameIndex = readInt(fin);

  if ( frameIndex < 0 || frameIndex >= anim.numFrames )
    throw Exception("MD5Model::readFrameEl(): invalid frame index");

  // get reference to frame and set number of animated components
  Frame &frame = anim.frames[frameIndex];
  frame.animatedComponents.resize(anim.numAnimatedComponents);

  TOKEN t = getNextToken(fin);

  // expect an opening brace { to begin block
  if ( t != TOKEN_LBRACE )
    throw Exception("MD5Model::readFrameEl(): expected { to"
                    " follow frame index");

  for ( int i=0; i < anim.numAnimatedComponents; i++ )
    frame.animatedComponents[i] = readFloat(fin);

  t = getNextToken(fin);

  // expect a closing brace } to end block
  if ( t != TOKEN_RBRACE )
    throw Exception("MD5Model::readFrameEl(): expected }");
}


// reads in a string terminal and stores it in str
// (assumes opening " has already been read in)
void MD5Model::readString(std::ifstream &fin, std::string &str) {
  str = std::string();

  // read characters until closing " is found
  char c = '\0';
  do {
    fin.get(c);

    if ( fin.eof() )
      throw Exception("MD5Model::readString(): reached end"
                      " of file before \" was found");

    if ( c != '"')
      str += c;
  } while ( c != '"' );
}


// reads in an integer terminal and returns its value
int MD5Model::readInt(std::ifstream &fin) {
  std::string str;
  TOKEN t = getNextToken(fin, &str);

  if ( t != TOKEN_INT )
    throw Exception("MD5Model::readInt(): expected integer");

  return atoi( str.c_str() );
}


// reads in a float terminal and returns its value
float MD5Model::readFloat(std::ifstream &fin) {
  std::string str;
  TOKEN t = getNextToken(fin, &str);

  // integer tokens are just numbers with out a decimal point, so they'll
  // suffice here as well
  if ( t != TOKEN_FLOAT && t != TOKEN_INT )
    throw Exception("MD5Model::readFloat(): expected float");

  float f = 0.0f;
  sscanf(str.c_str(), "%f", &f);

  return f;
}


// reads in sequence consisting of n floats enclosed by parentheses
void MD5Model::readVec(std::ifstream &fin, float *v, int n) {
  if ( getNextToken(fin) != TOKEN_LPAREN )
    throw Exception("MD5Model::readVec(): expected '('");

  for ( int i=0; i < n; i++ )
    v[i] = readFloat(fin);

  if ( getNextToken(fin) != TOKEN_RPAREN )
    throw Exception("MD5Model::readVec(): expected ')'");
}


void MD5Model::skipComments(std::ifstream &fin) {
  char c;
  fin.get(c);

  if ( c != '/' )
    throw Exception("MD5Model::skipComments(): invalid comment, expected //");

  while ( !fin.eof() && c != '\n' )
    fin.get(c);

  // put back last character read
  fin.putback(c);
}


// reads until first non-whitespace character
void MD5Model::skipWhitespace(std::ifstream &fin) {
  char c = '\0';
  while ( !fin.eof() ) {
    fin.get(c);

    if ( !IS_WHITESPACE(c) ) {
      fin.putback(c);
      break;
    }
  }
}


// reads in next token from file and matches it to a token type,
// if tokStr is non-NULL then it will be set to the text of the token
MD5Model::TOKEN MD5Model::getNextToken(std::ifstream &fin, std::string *tokStr) {
  skipWhitespace(fin);
  std::string str;

  TOKEN t = TOKEN_INVALID;

  while ( !fin.eof() ) {
    char c = '\0';
    fin.get(c);

    // single character tokens
    if ( '{' == c || '}' == c || '(' == c || ')' == c ) {
      // if already reading in a token, treat this as a delimiter
      if ( t != TOKEN_INVALID ) {
        fin.putback(c);
        if ( tokStr != NULL )
          (*tokStr) = str;
      }
 
      if ( '{' == c )
        t = TOKEN_LBRACE;
      if ( '}' == c )
        t = TOKEN_RBRACE;
      if ( '(' == c )
        t = TOKEN_LPAREN;
      if ( ')' == c )
        t = TOKEN_RPAREN;

      if ( tokStr) {
        (*tokStr) = std::string();
        (*tokStr) += c;
      }
      return t;
    }
    if ( isdigit(c) ) {
      str += c;
      if ( TOKEN_INVALID == t )
        t = TOKEN_INT;
      else if ( t != TOKEN_INT && t != TOKEN_FLOAT && t != TOKEN_KEYWORD ) {
        std::string msg("MD5Model::getNextToken(): invalid token '");
        msg += str + "'";
        throw Exception(msg);
      }
    }
    if ( '-' == c ) {
      str += c;
      if ( TOKEN_INVALID == t )
        t = TOKEN_INT;
      else {
        std::string msg("MD5Model::getNextToken(): invalid token '");
        msg += str + "'";
        throw Exception(msg);
      }
    }
    if ( isalpha(c) ) {
      str += c;
      if ( TOKEN_INVALID == t )
        t = TOKEN_KEYWORD;
      else if ( t != TOKEN_KEYWORD ) {
        std::string msg("MD5Model::getNextToken(): invalid token '");
        msg += str + "'";
        throw Exception(msg);
      }
    }
    if ( '"' == c ) {
      // treat as a delimeter if already reading in a token
      if ( t != TOKEN_INVALID ) {
        fin.putback(c);
        if ( tokStr != NULL )
          (*tokStr) = str;
        return t;
      }
      readString(fin, str);

      if ( tokStr != NULL )
        (*tokStr) = str;

      return TOKEN_STRING;
    }
    if ( '.' == c ) {
      str += c;
      if ( t != TOKEN_INT ) {
        std::string msg("MD5Model::getNextToken(): invalid token '");
        msg += str + "'";
        throw Exception(msg);
      }
      t = TOKEN_FLOAT;
    }
    if ( '/' == c ) {
      // treat as a delimeter if already reading in a token
      if ( t != TOKEN_INVALID ) {
        if ( tokStr != NULL )
          (*tokStr) = str;
        return t;
      }

      skipComments(fin);
      skipWhitespace(fin);
      continue;
    }

    // treat whitespace as a delimeter
    if ( IS_WHITESPACE(c) ) {
      if ( tokStr != NULL )
        (*tokStr) = str;
      return t;
    }

    // at this point token type should be set, if it hasn't been then
    // token is invalid
    if ( TOKEN_INVALID == t ) {
        std::string msg("MD5Model::getNextToken(): invalid token '");
        str += c;
        msg += str + "'";
        throw Exception(msg);
    }
  }

  return TOKEN_INVALID;
}


// builds mesh when no animation has been set
void MD5Model::buildVerts() {
  for ( size_t i=0; i < meshes.size(); i++ ) {
    for ( size_t j=0; j < meshes[i]->verts.size(); j++ ) {
      Video::Vertex &v = meshes[i]->verts[j];
      const VertexInfo &vertInfo = meshes[i]->vertsInfo[j];
      v.pos[0] = v.pos[1] = v.pos[2] = 0.0;

      for ( int k=0; k < vertInfo.weightCount; k++ ) {
        Weight &w = meshes[i]->weights[vertInfo.weightIndex + k];
        Joint &joint = joints[w.joint];

        Math3D::Quat<float> q(w.pos[0], w.pos[1], w.pos[2], 0.0f);
        Math3D::Quat<float> result = joint.quat*q*joint.quat.conjugate();
        v.pos[0] += (joint.pos[0] + result[0])*w.w;
        v.pos[1] += (joint.pos[1] + result[1])*w.w;
        v.pos[2] += (joint.pos[2] + result[2])*w.w;
      } // for (weights)
    } // for (mesh vertices)

/*
    // copy into master vertex list
    for ( size_t j=0; j < meshes[i]->tris.size(); j++ ) {
      verts[vertIndex++] = meshes[i]->verts[ meshes[i]->tris[j].v[2] ];
      verts[vertIndex++] = meshes[i]->verts[ meshes[i]->tris[j].v[1] ];
      verts[vertIndex++] = meshes[i]->verts[ meshes[i]->tris[j].v[0] ];
    }
*/
  } // for (meshes)

}


void MD5Model::buildVerts(Frame &frame) {
  for ( size_t i=0; i < meshes.size(); i++ ) {
    for ( size_t j=0; j < meshes[i]->verts.size(); j++ ) {
      Video::Vertex &v = meshes[i]->verts[j];
      const VertexInfo &vertInfo = meshes[i]->vertsInfo[j];
      v.pos[0] = v.pos[1] = v.pos[2] = 0.0;

      for ( int k=0; k < vertInfo.weightCount; k++ ) {
        Weight &w = meshes[i]->weights[vertInfo.weightIndex + k];
        Joint &joint = frame.joints[w.joint];

        Math3D::Quat<float> q(w.pos[0], w.pos[1], w.pos[2], 0.0f);
        Math3D::Quat<float> result = joint.quat*q*joint.quat.conjugate();
        v.pos[0] += (joint.pos[0] + result[0])*w.w;
        v.pos[1] += (joint.pos[1] + result[1])*w.w;
        v.pos[2] += (joint.pos[2] + result[2])*w.w;
      } // for (weights)
    } // for (mesh vertices)

/*
    // copy into master vertex list
    for ( size_t j=0; j < meshes[i]->tris.size(); j++ ) {
      verts[vertIndex++] = meshes[i]->verts[ meshes[i]->tris[j].v[2] ];
      verts[vertIndex++] = meshes[i]->verts[ meshes[i]->tris[j].v[1] ];
      verts[vertIndex++] = meshes[i]->verts[ meshes[i]->tris[j].v[0] ];
    }
*/
  } // for (meshes)
}


void MD5Model::buildTBN() {
  size_t vertIndex = 0;

  for ( size_t i=0; i < meshes.size(); i++) {
    for ( size_t j=0; j < meshes[i]->tris.size(); j++ ) {
      Video::Vertex &meshVert1 = meshes[i]->verts[ meshes[i]->tris[j].v[2] ],
                    &meshVert2 = meshes[i]->verts[ meshes[i]->tris[j].v[1] ],
                    &meshVert3 = meshes[i]->verts[ meshes[i]->tris[j].v[0] ];

      // subtract vertex positions to compute edges of triangle
      Math3D::Vector3<float> v2v1 = meshVert2.pos - meshVert1.pos,
                             v3v1 = meshVert3.pos - meshVert1.pos;

      // subtract texture coordinates
      Math3D::Vector2<float> c2c1(meshVert2.tc[0] - meshVert1.tc[0],
                                  meshVert2.tc[1] - meshVert1.tc[1]),
                             c3c1(meshVert3.tc[0] - meshVert1.tc[0],
                                  meshVert3.tc[1] - meshVert1.tc[1]);
      float invDet = 1.0f/(c2c1[0]*c3c1[1] - c3c1[0]*c2c1[1]);

      // compute normal of triangle from edges
      Math3D::Vector3<float> normal  = Math3D::Vector3<float>::cross(v2v1, v3v1),
                             tangent = (v2v1*c3c1[1] - v3v1*c2c1[1])*invDet;
      normal.normalize();
      tangent.normalize();

      meshVert1.faceTangent = meshVert2.faceTangent = meshVert3.faceTangent = tangent;
      meshVert1.faceNormal  = meshVert2.faceNormal  = meshVert3.faceNormal  = normal;

      vertIndex += 3;
    }
  } // for (meshes)
}


void MD5Model::buildFrames(Anim &anim) {
  for ( int i=0; i < anim.numFrames; i++ ) {
    // allocate storage for joints for this frame
    anim.frames[i].joints.resize(numJoints);

    for ( int j=0; j < numJoints; j++ ) {
      const Joint &joint = anim.baseJoints[j];

      float pos[3]    = { joint.pos[0],  joint.pos[1],  joint.pos[2]  };
      float orient[3] = { joint.quat[0], joint.quat[1], joint.quat[2] };

      int n = 0;
      for ( int k=0; k < 3; k++ )
        if ( anim.jointInfo[j].flags & (1 << k)  ) {
          pos[k] = anim.frames[i].animatedComponents[anim.jointInfo[j].startIndex + n];
          n++;
        }

      for ( int k=0; k < 3; k++ )
        if ( anim.jointInfo[j].flags & (8 << k)  ) {
          orient[k] = anim.frames[i].animatedComponents[anim.jointInfo[j].startIndex + n];
          n++;
        }

      Math3D::Quat<float> q = buildQuat(orient[0], orient[1], orient[2]);

      Joint &frameJoint = anim.frames[i].joints[j];
      frameJoint.name   = anim.jointInfo[j].name;
      frameJoint.parent = anim.jointInfo[j].parentIndex;

      // root joint?
      if ( anim.jointInfo[j].parentIndex < 0 ) {
        frameJoint.pos[0] = pos[0];
        frameJoint.pos[1] = pos[1];
        frameJoint.pos[2] = pos[2];
        frameJoint.quat = q;
      }
      else {
        Joint &parent = anim.frames[i].joints[anim.jointInfo[j].parentIndex];

        // rotate position (qp is quaternion representation of position)
        Math3D::Quat<float> qp(pos[0], pos[1], pos[2], 0.0f);
        Math3D::Quat<float> result = parent.quat*qp*parent.quat.conjugate();

        frameJoint.pos[0] = result[0] + parent.pos[0];
        frameJoint.pos[1] = result[1] + parent.pos[1];
        frameJoint.pos[2] = result[2] + parent.pos[2];

        // store orientation of this joint
        frameJoint.quat = parent.quat*q;
        frameJoint.quat.normalize();
      } // else
    } // for
  } // for
}


Math3D::Quat<float> MD5Model::buildQuat(float x, float y, float z) const {
  // compute the 4th component of the quaternion
  float w = 1.0f - x*x - y*y - z*z;
  w = w < 0.0f ? 0.0f : (float)-sqrt( double(w) );

  Math3D::Quat<float> q(x, y, z, w);
  q.normalize();

  return q;
}


MD5Model::Mesh::Mesh():
     texID(-1),
     nmID(-1) {

}


MD5Model::Anim::Anim():
     numFrames(0),
     frameRate(0),
     numAnimatedComponents(0) {

}
