/***************************************************************************
 *   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 <iostream>
#include <stdexcept>
#include <SDL/SDL.h>
#include "FPSDemo.h"
#include "Sound.h"
#include "Q3Map.h"
#include "Physics.h"
#include "MD5Entity.h"

#define MOUSE_SENSITIVITY  25.0f
#define MOVE_SPEED         350.0f


FPSDemo::FPSDemo() {
  Video::initVideo();

  if ( Video::getShadersSupported() == false )
    throw std::runtime_error("shaders not supported");

  Sound::initSound();

  loadMap();
  loadModels();
  loadSounds();

  initSkyBox();

  // initialize head bob state
  headBobPhase = 0.0f;
  headOffset = Vector3(0.0f, 0.0f, 0.0f);

  // hard-code initial camera orientation
  camera.setYRot(180.0f);
}


FPSDemo::~FPSDemo() {
  if ( player != NULL && player->getCleanUp() == false )
    delete player;

  Video::deleteTexture(skyBoxTextureFront);
  Video::deleteTexture(skyBoxTextureBack);
  Video::deleteTexture(skyBoxTextureLeft);
  Video::deleteTexture(skyBoxTextureRight);
  Video::deleteTexture(skyBoxTextureTop);

  // Clear BSP before Video::shutdownVideo() is called because clearing the
  // BSP entails deleting textures, if this happens after
  // Video::shutdownVideo() then there will be an error.  Also note that
  // BSP::clear() is called in the destructor of BSP, so this MUST be called
  // before Video::shutdownVideo() to avoid errors.
  bsp.clear();

  Video::shutdownVideo();
  Sound::shutdownSound();
}


void FPSDemo::loadModels() {
  md5Models[0].loadMesh("models/qshambler.md5mesh");
  md5Models[0].loadAnim("models/qshambler_idle02.md5anim");
  //md5Models[0].loadAnim("models/qshambler_walk.md5anim");

  MD5Entity *md5Ent = new MD5Entity(&md5Models[0], true);
  md5Ent->setPos( Point3(0.0f, 128.0f, -2250.0f) );
  md5Ent->setMass(1.0f);
  md5Ent->setRot( Quat::fromEulerAngles(0.0f, 0.0f, 180.0f)*
                  Quat::fromEulerAngles(0.0f, 0.0f, -90.0f) );
  bsp.insertEntity(md5Ent);

  //if ( mesh.load("models/chaingun.msh") == false )
  //  throw std::runtime_error("could not load weapon!");

  //int weaponTex = Video::loadTexture("textures/models/chaingun.tga");

  //weapon = new TriangleMeshEntity(&mesh, weaponTex, -1, true);

  Entity *meshEnt = NULL;

  if ( meshes[0].load("models/teapot.msh") == false )
    throw std::runtime_error("could not load models/teapot.msh");

  int texID = Video::loadTexture("textures/models/teapot.tga"),
      nmID  = Video::loadTexture("textures/models/teapot_local.tga", true);
  meshEnt = new TriangleMeshEntity(&meshes[0], texID, nmID, true);
  meshEnt->setPos( Point3(510.0f, 256.0f, -512.0f) );
  meshEnt->setMass(1.0f);
  bsp.insertEntity(meshEnt);

  // add another teapot
  meshEnt = new TriangleMeshEntity(&meshes[0], texID, nmID, true);
  meshEnt->setPos( Point3(-510.0f, 256.0f, -512.0f) );
  meshEnt->setMass(1.0f);
  meshEnt->setRot( Quat::fromEulerAngles(0.0f, 180.0f, 0.0f) );
  bsp.insertEntity(meshEnt);

  ///// chess pieces /////

  int darkChessTexture  = Video::loadTexture("textures/wood03.tga"),
      darkChessNM       = Video::loadTexture("textures/wood03_local.tga", true),
      lightChessTexture = Video::loadTexture("textures/wood11.tga"),
      lightChessNM      = Video::loadTexture("textures/wood11_local.tga", true);

  if ( meshes[1].load("models/pawn.msh") == false )
    throw std::runtime_error("could not load pawn");

  if ( meshes[2].load("models/rook.msh") == false )
    throw std::runtime_error("could not load rook");

  if ( meshes[3].load("models/knight.msh") == false )
    throw std::runtime_error("could not load knight");

  if ( meshes[4].load("models/bishop.msh") == false )
    throw std::runtime_error("could not load bishop");

  if ( meshes[5].load("models/king.msh") == false )
    throw std::runtime_error("could not load king");

  if ( meshes[6].load("models/queen.msh") == false )
    throw std::runtime_error("could not load queen");

  TriangleMesh *row[8] = { &meshes[2], &meshes[3], &meshes[4], &meshes[5],
                           &meshes[6], &meshes[4], &meshes[3], &meshes[2] };

  // add rows of chess pieces
  for ( int i=0; i < 8; i++ ) {
    // light pawns
    meshEnt = new TriangleMeshEntity(&meshes[1], lightChessTexture, lightChessNM, true);
    meshEnt->setPos( Point3(-320.0f, 150.0f, -2275.0f + float(i*-128)) );
    meshEnt->setMass(1.0f);
    bsp.insertEntity(meshEnt);

    // dark pawns
    meshEnt = new TriangleMeshEntity(&meshes[1], darkChessTexture, darkChessNM, true);
    meshEnt->setPos( Point3(320.0f, 150.0f, -2275.0f + float(i*-128)) );
    meshEnt->setMass(1.0f);
    bsp.insertEntity(meshEnt);

    meshEnt = new TriangleMeshEntity(row[i], lightChessTexture, lightChessNM, true);
    meshEnt->setPos( Point3(-450.0f, 150.0f, -2275.0f + float(i*-128)) );
    meshEnt->setRot( Quat::fromEulerAngles(0.0f, 180.0f, 0.0f) );
    meshEnt->setMass(1.0f);
    bsp.insertEntity(meshEnt);

    meshEnt = new TriangleMeshEntity(row[i], darkChessTexture, darkChessNM, true);
    meshEnt->setPos( Point3(450.0f, 150.0f, -2275.0f + float(i*-128)) );
    meshEnt->setMass(1.0f);
    bsp.insertEntity(meshEnt);
  }
}


void FPSDemo::loadSounds() {
  //stepSoundID = Sound::loadSound("sounds/steps.wav");
  //jumpGruntSoundID = Sound::loadSound("sounds/jump_grunt.wav");
  stepSoundStartTime = 0;
}


void FPSDemo::loadMap() {
  // load Quake 3 map
  Q3Map q3map;
  if ( !q3map.load("maps/map.bsp") )
    throw std::runtime_error("unable to load map");

  bsp.loadQ3Map(q3map);

  // extract entities from map
  Q3Map::EntMaps entMaps;
  if ( q3map.parseEnts(entMaps) == false )
    throw std::runtime_error("FPSDemo::loadMap(): unable to parse entities");

  // find player start location in entities
  Point3 playerStartPos(0.0f, 0.0f, 0.0f);
  for ( size_t i=0; i < entMaps.size(); i++ ) {
    Q3Map::EntAttrMap::const_iterator itr = entMaps[i].find("classname");
    
    if ( itr != entMaps[i].end() && itr->second == "info_player_start" ) {
      itr = entMaps[i].find("origin");
      if ( itr != entMaps[i].end() ) {
        if ( sscanf(itr->second.c_str(),
                    "%f %f %f",
                    &playerStartPos[0],
                    &playerStartPos[2],
                    &playerStartPos[1]) != 3 )
          throw std::runtime_error("FPSDemo::loadMap(): invalid "
                                   "info_player_start in bsp");

          playerStartPos[2] *= -1.0f;
          break; // found start position
      }
    }
  }

  // insert player entity into world
  player = new Player(false);
  player->setPos(playerStartPos);
  bsp.insertEntity(player);
}


// initialize sky box vertices and load sky box textures
// (sky box has 20 vertices rather than 24 because it has
//  no bottom face)
void FPSDemo::initSkyBox() {
  // the positions of the sky box corners
  static const Point3 boxVerts[8] = { Point3( 5000.0f,  5000.0f,  5000.0f),   // 0
                                      Point3(-5000.0f,  5000.0f,  5000.0f),   // 1
                                      Point3( 5000.0f, -5000.0f,  5000.0f),   // 2
                                      Point3( 5000.0f,  5000.0f, -5000.0f),   // 3
                                      Point3(-5000.0f, -5000.0f,  5000.0f),   // 4
                                      Point3( 5000.0f, -5000.0f, -5000.0f),   // 5
                                      Point3(-5000.0f,  5000.0f, -5000.0f),   // 6
                                      Point3(-5000.0f, -5000.0f, -5000.0f) }; // 7

  ///// initialize sky box vertices /////

  // front face
  skyBox[0].pos=boxVerts[0]; skyBox[0].tc[0]=1.0f; skyBox[0].tc[1]=1.0f;
  skyBox[1].pos=boxVerts[2]; skyBox[1].tc[0]=1.0f; skyBox[1].tc[1]=0.0f;
  skyBox[2].pos=boxVerts[4]; skyBox[2].tc[0]=0.0f; skyBox[2].tc[1]=0.0f;
  skyBox[3].pos=boxVerts[1]; skyBox[3].tc[0]=0.0f; skyBox[3].tc[1]=1.0f;

  // back face
  skyBox[4].pos=boxVerts[6]; skyBox[4].tc[0]=1.0f; skyBox[4].tc[1]=1.0f;
  skyBox[5].pos=boxVerts[7]; skyBox[5].tc[0]=1.0f; skyBox[5].tc[1]=0.0f;
  skyBox[6].pos=boxVerts[5]; skyBox[6].tc[0]=0.0f; skyBox[6].tc[1]=0.0f;
  skyBox[7].pos=boxVerts[3]; skyBox[7].tc[0]=0.0f; skyBox[7].tc[1]=1.0f;

  // left face
  skyBox[8].pos=boxVerts[1]; skyBox[8].tc[0]=1.0f; skyBox[8].tc[1]=1.0f;
  skyBox[9].pos=boxVerts[4]; skyBox[9].tc[0]=1.0f; skyBox[9].tc[1]=0.0f;
  skyBox[10].pos=boxVerts[7]; skyBox[10].tc[0]=0.0f; skyBox[10].tc[1]=0.0f;
  skyBox[11].pos=boxVerts[6]; skyBox[11].tc[0]=0.0f; skyBox[11].tc[1]=1.0f;

  // right face
  skyBox[12].pos=boxVerts[3]; skyBox[12].tc[0]=1.0f; skyBox[12].tc[1]=1.0f;
  skyBox[13].pos=boxVerts[5]; skyBox[13].tc[0]=1.0f; skyBox[13].tc[1]=0.0f;
  skyBox[14].pos=boxVerts[2]; skyBox[14].tc[0]=0.0f; skyBox[14].tc[1]=0.0f;
  skyBox[15].pos=boxVerts[0]; skyBox[15].tc[0]=0.0f; skyBox[15].tc[1]=1.0f;

  // top face
  skyBox[16].pos=boxVerts[3]; skyBox[16].tc[0]=1.0f; skyBox[16].tc[1]=1.0f;
  skyBox[17].pos=boxVerts[0]; skyBox[17].tc[0]=1.0f; skyBox[17].tc[1]=0.0f;
  skyBox[18].pos=boxVerts[1]; skyBox[18].tc[0]=0.0f; skyBox[18].tc[1]=0.0f;
  skyBox[19].pos=boxVerts[6]; skyBox[19].tc[0]=0.0f; skyBox[19].tc[1]=1.0f;

  ///// load sky box textures /////

  skyBoxTextureLeft = skyBoxTextureRight = skyBoxTextureFront = 
  skyBoxTextureBack = skyBoxTextureTop   = -1;

  skyBoxTextureLeft  = Video::loadTexture("textures/sky1_left.tga",  false, false);
  skyBoxTextureRight = Video::loadTexture("textures/sky1_right.tga", false, false);
  skyBoxTextureFront = Video::loadTexture("textures/sky1_front.tga", false, false);
  skyBoxTextureBack  = Video::loadTexture("textures/sky1_back.tga",  false, false);
  skyBoxTextureTop   = Video::loadTexture("textures/sky1_top.tga",   false, false);
}


bool FPSDemo::shouldExit() {
  // get keyboard state
  Uint8 *kbd = SDL_GetKeyState(NULL);

  // sanity check
  if ( NULL == kbd )
    return false;

  return 1 == kbd[SDLK_ESCAPE];
}


void FPSDemo::run() {
  bool done = false;
  SDL_Event event;
  float dt = 0.0f;
  unsigned int lastTime = SDL_GetTicks();

  while ( shouldExit() == false && done == false ) {
    unsigned int currTime = SDL_GetTicks();

    // make sure at least one ms passes
    if ( currTime == lastTime )
      continue;

    if ( currTime > lastTime )
      dt = float(currTime-lastTime)/1000.0f;
    else
      dt = 0.0f;

    // if too much time has passed, cap dt since tunneling might occur
    if ( dt > 0.1f )
      dt = 0.1f;

    lastTime = currTime;

    // handle events
    while ( SDL_PollEvent( &event ) ) {
      switch ( event.type ) {
        case SDL_QUIT:
          done = true;
          break;
        default:
          break;
      }
    } // while (events)

    // update game state
    frame(dt);

    // render scene
    render(dt);
  } // while (main loop)
}


void FPSDemo::frame(float dt) {
  Physics::beginFrame(bsp);
  
  // poll for keyboard input
  handleKeyboard(dt);

  // poll for mouse input
  handleMouseMotion(dt);

  bsp.updateEntities(dt);

  Physics::endFrame(bsp, dt);
}


void FPSDemo::handleKeyboard(float dt) {
  // get keyboard state
  Uint8 *kbd = SDL_GetKeyState(NULL);

  // sanity check
  if ( NULL == kbd )
    throw std::runtime_error("SDLDriver::getKeyboardInput(): unable to "
                             "retrieve keyboard state");

  if ( player->isGrounded() == false )
    return;

  // is player trying to jump?
  bool jumping = false;
  if ( 1 == kbd[SDLK_SPACE] ) {
    // stop footstep sound
    Sound::stopChannel(0);

    // play jumping sound
    //Sound::playSound(jumpGruntSoundID, 0, 0);

    // make player jump
    player->setVel( player->getVel() + Vector3(0.0f, 400.0f, 0.0f) );

    jumping = true;
  }
  
  Vector3 v(0.0f, 0.0f, 0.0f);

  // check arrow keys
  if ( 1 == kbd[SDLK_RIGHT] || 1 == kbd[SDLK_d] )
    v[0] += 1.0f;
  if ( 1 == kbd[SDLK_LEFT] || 1 == kbd[SDLK_a] )
    v[0] += -1.0f;
  if ( 1 == kbd[SDLK_UP] || 1 == kbd[SDLK_w] )
    v[2] += -1.0f;
  if ( 1 == kbd[SDLK_DOWN] || 1 == kbd[SDLK_s] )
    v[2] += 1.0f;

  // if user is pressing shift, then set walking to true,
  // this will make player move slower
  bool walking = (kbd[SDLK_LSHIFT] == 1 || kbd[SDLK_RSHIFT] == 1) ? true : false;

  if ( v.sqrMag() < EPSILON )
    return;

  // remove horizontal component of velocity and force
  player->setVel( Vector3(0.0f, player->getVel()[1], 0.0f) );
  player->setForce( Vector3(0.0f, player->getForce()[1], 0.0f) );

  // use camera matrix to transform displacement vector
  v = camera.getQuat().toMatrix4().transform(v);

  // get rid of vertical component of displacement vector
  v[1] = 0.0f;

  if ( Math3D::rabs(v[0]) >= EPSILON ||
       Math3D::rabs(v[2]) >= EPSILON ) {
    // play footstep sound if it is not already playing
    if ( !jumping && !Sound::isChannelPlaying(0) ) {
      stepSoundStartTime = SDL_GetTicks();
      //Sound::playSound(stepSoundID, 0, 0);
    }

    // normalize displacement vector
    v.normalize();

    Vector3 vel = v*MOVE_SPEED;
    if ( true == walking )
      vel *= 0.5f;
     
    player->setVel(player->getVel() + vel);
    headBob(vel*dt);
  }
  else {
    // if the footstep sound is playing but player is not moving,
    // then stop the footstep sound from playing
    if ( Sound::isChannelPlaying(0) ) {
      if ( SDL_GetTicks() > stepSoundStartTime &&
           SDL_GetTicks() - stepSoundStartTime > 400 )
        Sound::stopChannel(0);
    }
  }
}


void FPSDemo::handleMouseMotion(float dt) {
  // to store x, y coordinates of mouse cursor
  int x, y;

  // get mouse button state
  SDL_GetMouseState(&x, &y);

  ///// handle mouselook /////

  // angle differences
  float dTheta = 0.0f,
        dPhi   = 0.0f;

  // horizontal and vertical differences in cursor position
  float dx = float( x - (WINDOW_WIDTH  >> 1) ),
        dy = float( y - (WINDOW_HEIGHT >> 1) );

  // if cursor has moved horizontally
  if ( dx )
    dTheta = dx/float(WINDOW_WIDTH >> 1);

  // if cursor has moved vertically
  if ( dy )
    dPhi = dy/float(WINDOW_HEIGHT >> 1);

  if ( dTheta != 0.0f || dPhi != 0.0f ) {
    // update camera orientation
    camera.updateXRot(dPhi*MOUSE_SENSITIVITY);
    camera.updateYRot(dTheta*MOUSE_SENSITIVITY);
    
    // restrict camera orientation
    if ( camera.getXRot() > 85.0f )
      camera.setXRot(85.0f);
    else if ( camera.getXRot() < -85.0f )
      camera.setXRot(-85.0f);
  }

  SDL_WarpMouse(WINDOW_WIDTH >> 1, WINDOW_HEIGHT >> 1);
}


void FPSDemo::render(float dt) {
  Video::clearBackBuffer();
  Video::clearZBuffer();

  ///// temporary lighting /////
  Video::Light l;

  l.pos[0] = camera.getPos()[0];
  l.pos[1] = camera.getPos()[1];
  l.pos[2] = camera.getPos()[2];
  l.ambient[0] = l.ambient[1] = l.ambient[2] = 0.1f;
  l.diffuse[0] = l.diffuse[1] = l.diffuse[2] = 1.0f;
  l.specular[0] = l.specular[1] = l.specular[2] = 0.8f;
  l.linearAtt = 0.001f;
  Video::toggleLight(0, true);
  Video::setLight(0, l);

  // set camera position
  camera.setPos( player->getPos() + headOffset );

  // apply view rotation transformation and then render sky box
  // (view translation not applied until after sky box is rendered)
  Video::setModelTranslation( Point3(0.0f, 0.0f, 0.0f) );
  Video::setModelRotation( Quat() );
  Video::setViewTranslation( Point3(0.0f, 0.0f, 0.0f) );
  Video::setViewRotation( camera.getQuat() );
  Video::applyTransforms();

  // render sky box
  renderSky();

  // clear z-buffer so world geometry always gets drawn over sky box
  Video::clearZBuffer();

  // now apply view translation
  Video::setViewTranslation( camera.getPos()*-1.0f );
  Video::applyTransforms();

  // render world geometry
  bsp.render(camera);

  // render weapon
  //renderWeapon();

  // show back buffer
  Video::showBackBuffer();
}


// This function renders the sky box.  The sky box is missing the bottom face,
// since it is assumed that the viewer will never be able to see it.
void FPSDemo::renderSky() {
  // disable normal mapping if it is enabled
  bool normalMappingEnabled = Video::getNormalMappingEnabled();
  if ( true == normalMappingEnabled )
    Video::disableNormalMapping();  

  // front face
  Video::useTexture(skyBoxTextureFront);
  Video::renderQuads(&skyBox[0], 1);

  // back face
  Video::useTexture(skyBoxTextureBack);
  Video::renderQuads(&skyBox[4], 1);

  // left face
  Video::useTexture(skyBoxTextureLeft);
  Video::renderQuads(&skyBox[8], 1);

  // right face
  Video::useTexture(skyBoxTextureRight);
  Video::renderQuads(&skyBox[12], 1);

  // top face
  Video::useTexture(skyBoxTextureTop);
  Video::renderQuads(&skyBox[16], 1);

  // re-enable normal mapping if it was enabled before we disabled it
  if ( true == normalMappingEnabled )
    Video::enableNormalMapping();
}


void FPSDemo::renderWeapon() {
/*
  if ( NULL != weapon ) {
    Video::clearZBuffer();

    // set weapon material
    Video::Material mat;
    mat.ambient[0] = mat.ambient[1] = mat.ambient[2] = 0.8f;
    mat.diffuse[0] = mat.diffuse[1] = mat.diffuse[2] = 0.8f;
    mat.specular[0] = mat.specular[1] = mat.specular[2] = 0.7f;
    mat.ambient[3] = mat.diffuse[3] = mat.specular[3] = 1.0f;
    mat.shininess = 8.0f;
    Video::setMaterial(mat);

    // compute offset along y axis, sin(head bob phase)*(head bob amplitude)
    Vector3 offset = Vector3(0.0f, 0.0f, Math3D::rsin(headBobPhase)*3.0f);

    Video::setModelTranslation( Point3(0.0f, 0.0f, 0.0f) );
    Video::setModelRotation( Quat() );
    Video::setViewTranslation( camera.getPos()*-1.0f );
    Video::setViewRotation( Quat() );
    Video::applyTransforms();
    weapon->setPos(camera.getPos() + Vector3(25.0f, -25.0f, -35.0f) + offset);
    weapon->render();
  }
*/
}


void FPSDemo::headBob(const Vector3 &displacement) {
  headBobPhase += (displacement.mag()/MOVE_SPEED)*10.0f;

  // compute offset along y axis, sin(head bob phase)*(head bob amplitude)
  headOffset = Vector3(0.0f, Math3D::rsin(headBobPhase)*3.0f, 0.0f);
  camera.setPos(camera.getPos() + headOffset);

  if ( headBobPhase > 6.28318f )
    headBobPhase = 0.0f;
}
