// ==============================================================
//	This file is part of MegaGlest (www.glest.org)
//
//	Copyright (C) 2011-  by Mark Vejvoda
//
// This code is licensed under the MIT license:
// http://www.opensource.org/licenses/mit-license.php
//
// Open Source Initiative OSI - The MIT License (MIT):Licensing
//
// The MIT License (MIT)
// Copyright (c) <year> <copyright holders>
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.//
//
// ==============================================================

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

#include <iostream>
#include <iomanip>
#include <fstream>
#include <vector>
#include <string>
#include <GL/glew.h>

#include "GlErrors.h"
#include "Mathlib.h"
#include "Md5Model.h"
#include "TextureManager.h"
#include "ArbProgram.h"
#include "ShaderManager.h"
#include "md5util.h"

namespace Shared { namespace Graphics { namespace md5 {


using std::cout;
using std::cerr;
using std::endl;
using std::string;
using std::vector;

// All vertex and fragment programs
ArbVertexProgram *vp_bump = NULL;
ArbVertexProgram *vp_bump_parallax = NULL;

ArbFragmentProgram *fp_diffuse = NULL;
ArbFragmentProgram *fp_diffuse_specular = NULL;
ArbFragmentProgram *fp_ds_parallax = NULL;

// Tangent uniform's location
GLint tangentLoc = -1;

int renderFlags = Md5Object::kDrawModel;

bool bAnimate = true;
bool bTextured = true;
bool bCullFace = true;
bool bBounds = false;
bool bParallax = false;
bool bLight = true;
bool bSmooth = true;
bool bWireframe = false;
bool bDrawNormals = false;

vector<string> animations;

// Camera
Vector3f rot, eye;

// -------------------------------------------------------------------------
// cleanupMD5OpenGL
//
// Application cleanup.
// -------------------------------------------------------------------------
void cleanupMD5OpenGL() {
  //delete model;
  //delete object;
  //delete font;
  //delete shader;

  delete Md5Model::shader;
  Md5Model::shader=NULL;
  delete vp_bump;
  delete vp_bump_parallax;
  delete fp_diffuse;
  delete fp_diffuse_specular;
  delete fp_ds_parallax;

  Texture2DManager::kill ();
}

// -------------------------------------------------------------------------
// initShader
//
// Shader's uniform variables initialization.
// -------------------------------------------------------------------------
void initShader () {
  if (NULL == Md5Model::shader)
    return;

  Md5Model::shader->use();

  if (GLEW_VERSION_2_0) {
      GLuint prog = Md5Model::shader->handle ();

      // Set uniform parameters
      glUniform1i (glGetUniformLocation (prog, "decalMap"), 0);
      glUniform1i (glGetUniformLocation (prog, "glossMap"), 1);
      glUniform1i (glGetUniformLocation (prog, "normalMap"), 2);
      glUniform1i (glGetUniformLocation (prog, "heightMap"), 3);
      glUniform1i (glGetUniformLocation (prog, "parallaxMapping"), bParallax);

      // Get attribute location
      Md5Model::tangentLoc = glGetAttribLocation (prog, "tangent");
  }
  else {
      GLhandleARB prog = Md5Model::shader->handle();

      // Set uniform parameters
      glUniform1iARB (glGetUniformLocationARB (prog, "decalMap"), 0);
      glUniform1iARB (glGetUniformLocationARB (prog, "glossMap"), 1);
      glUniform1iARB (glGetUniformLocationARB (prog, "normalMap"), 2);
      glUniform1iARB (glGetUniformLocationARB (prog, "heightMap"), 3);
      glUniform1iARB (glGetUniformLocationARB (prog, "parallaxMapping"), bParallax);

      // Get attribute location
      Md5Model::tangentLoc = glGetAttribLocationARB (prog, "tangent");
  }

  Md5Model::shader->unuse();

  // Warn ff we fail to get tangent location... We'll can still use
  // the shader, but without tangents
  if(Md5Model::tangentLoc == -1)
    cerr << "Warning! No \"tangent\" uniform found in shader!" << endl;
}

// -------------------------------------------------------------------------
// announceRenderPath
//
// Print info about a render path.
// -------------------------------------------------------------------------
void announceRenderPath (render_path_e path) {
  cout << "Render path: ";
  switch (path)
    {
    case R_normal:
      cout << "no bump mapping (fixed pipeline)" << endl;
      break;

    case R_ARBfp_diffuse:
      cout << "bump mapping, diffuse only "
	   << "(ARB vp & fp)" << endl;
      break;

    case R_ARBfp_diffuse_specular:
      cout << "bump mapping, diffuse and specular "
	   << "(ARB vp & fp)" << endl;
      break;

    case R_ARBfp_ds_parallax:
      cout << "bump mapping with parallax "
	   << "(ARB fp & fp)" << endl;
      break;

    case R_shader:
      cout << "bump mapping with parallax "
	   << "(GLSL)" << endl;
      break;
    }
}

// -------------------------------------------------------------------------
// initMD5OpenGL
//
// OpenGL initialization.
// -------------------------------------------------------------------------
void initMD5OpenGL(string shaderPath) {
  //glClearColor (0.5f, 0.5f, 0.5f, 0.0f);
  //glShadeModel (GL_SMOOTH);
  //glCullFace (GL_BACK);
  //glEnable (GL_DEPTH_TEST);

  // Initialize GLEW
  GLenum err = glewInit ();
  if(GLEW_OK != err) {
      // Problem: glewInit failed, something is seriously wrong.
      cerr << "Error: " << glewGetErrorString (err) << endl;
      cleanupMD5OpenGL();
  }

  // Print some infos about user's OpenGL implementation
  cout << "OpenGL Version String: " << glGetString (GL_VERSION) << endl;
  cout << "GLU Version String: " << gluGetString (GLU_VERSION) << endl;
  cout << "GLEW Version String: " << glewGetString (GLEW_VERSION) << endl;

  // Initialize ARB vertex/fragment program support
  initArbProgramHandling();

  // Initialize GLSL shader support
  initShaderHandling();

  if(hasArbVertexProgramSupport () &&
      hasArbFragmentProgramSupport ()) {
      // Load ARB programs
      vp_bump = new ArbVertexProgram(shaderPath + "bump.vp");
      vp_bump_parallax = new ArbVertexProgram(shaderPath + "bumpparallax.vp");

      fp_diffuse = new ArbFragmentProgram(shaderPath + "bumpd.fp");
      fp_diffuse_specular = new ArbFragmentProgram(shaderPath + "bumpds.fp");
      fp_ds_parallax = new ArbFragmentProgram(shaderPath + "bumpdsp.fp");

      // Current ARB programs will be bump mapping with diffuse
      // and specular components
      Md5Model::vp = vp_bump;
      Md5Model::fp = fp_diffuse_specular;
  }

  if(hasShaderSupport ()) {
      // Load shader
      VertexShader vs(shaderPath + "bump.vert");
      FragmentShader fs(shaderPath + "bump.frag");
      Md5Model::shader = new ShaderProgram(shaderPath + "bump mapping", vs, fs);

      // Initialize shader's uniforms
      initShader();
  }

  // Announce avalaible render paths, select the best
  cout << endl << "Available render paths:" << endl;

  cout << " [F3] - No bump mapping (fixed pipeline)" << endl;
  Md5Model::renderPath = R_normal;

  if (vp_bump && fp_diffuse) {
      cout << " [F4] - Bump mapping, diffuse only "
	   << "(ARB vp & fp)" << endl;
      Md5Model::renderPath = R_ARBfp_diffuse;
  }

  if (vp_bump && fp_diffuse_specular) {
      cout << " [F5] - Bump mapping, diffuse and specular "
	   << "(ARB vp & fp)" << endl;
      Md5Model::renderPath = R_ARBfp_diffuse_specular;
  }

  if (vp_bump_parallax && fp_ds_parallax) {
      cout << " [F6] - Bump mapping with parallax "
	   << "(ARB vp & fp)" << endl;
  }

  if (Md5Model::shader) {
      cout << " [F7] - Bump mapping with parallax "
	   << "(GLSL)" << endl;
      Md5Model::renderPath = R_shader;
  }

  // Announce which path has been chosen by default
  cout << endl;
  announceRenderPath(Md5Model::renderPath);

  checkOpenGLErrors (__FILE__, __LINE__);
}

// -------------------------------------------------------------------------
// extractFromQuotes
//
// Extract a string from quotes.
// -------------------------------------------------------------------------
inline const string extractFromQuotes (const string &str) {
  string::size_type start = str.find_first_of ('\"') + 1;
  string::size_type end = str.find_first_of ('\"', start) - 2;
  return str.substr (start, end);
}

// -------------------------------------------------------------------------
// getMD5ObjectFromLoaderScript
//
// Parse a script file for loading md5mesh and animations.
// -------------------------------------------------------------------------
Md5Object * getMD5ObjectFromLoaderScript(const string &filename) {
  // Open the file to parse
  std::ifstream file (filename.c_str(), std::ios::in);

  if (file.fail ()) {
      cerr << "Couldn't open " << filename << endl;
      cleanupMD5OpenGL();
  }

  // Get texture manager
  Texture2DManager *texMgr = Texture2DManager::getInstance();

  Md5Model *model = NULL;
  Md5Object *object = NULL;

  while (!file.eof ()) {
      string token, buffer;
      string meshFile, animFile, textureFile;
      string meshName, animName;

      // Peek next token
      file >> token;

      if (token == "model") {
		  std::getline (file, buffer);
		  meshFile = extractFromQuotes (buffer);

		  // Delete previous model and object if existing
		  delete model;
		  delete object;

		  // Load mesh model
		  model = new Md5Model(meshFile);
		  object = new Md5Object(model);
      }
      else if (token == "anim") {
		  std::getline (file, buffer);
		  animFile = extractFromQuotes (buffer);

		  try {
			  // Load animation
			  if (model) {
				  model->addAnim(animFile);
			  }
		  }
		  catch (Md5Exception &err) {
			  cerr << "Failed to load animation "
			   << animFile << endl;
			  cerr << "Reason: " << err.what ()
			   << " (" << err.which () << ")" << endl;
		  }
      }
      else if (token == "hide")	{
		  std::getline (file, buffer);
		  meshName = extractFromQuotes (buffer);

		  // Set mesh's render state
		  if (model) {
			  model->setMeshRenderState (meshName, Md5Mesh::kHide);
		  }
      }
      else if ((token == "decalMap") ||
	       (token == "specularMap") ||
	       (token == "normalMap") ||
	       (token == "heightMap")) {
		  // Get the next token and extract the mesh name
		  file >> buffer;
		  long start = buffer.find_first_of ('\"') + 1;
		  long end = buffer.find_first_of ('\"', start) - 1;
		  meshName = buffer.substr (start, end);

		  // Get the rest of line and extract texture's filename
		  std::getline (file, buffer);
		  textureFile = extractFromQuotes (buffer);

		  // If the model has been loaded, setup
		  // the texture to the desired mesh
		  if (model) {
			  Texture2D *tex = texMgr->load (textureFile);
			  if (tex->fail ())
				  cerr << "failed to load " << textureFile << endl;

			  if (token == "decalMap")
				  model->setMeshDecalMap (meshName, tex);
			  else if (token == "specularMap")
				  model->setMeshSpecularMap (meshName, tex);
			  else if (token == "normalMap")
				  model->setMeshNormalMap (meshName, tex);
			  else if (token == "heightMap")
				  model->setMeshHeightMap (meshName, tex);
		  }
      }
      else if (token == "setAnim") {
		  std::getline (file, buffer);
		  animName = extractFromQuotes (buffer);

		  // Set object's default animation
		  object->setAnim (animName);
      }
    }

  	file.close ();

  	if (!model || !object)
  		throw Md5Exception ("No mesh found!", filename);

  	return object;
}

// -------------------------------------------------------------------------
// setupLight
//
// Setup light position and enable light0.
// -------------------------------------------------------------------------
void setupLight(GLfloat x, GLfloat y, GLfloat z) {
  GLfloat lightPos[4];
  lightPos[0] = x;
  lightPos[1] = y;
  lightPos[2] = z;
  lightPos[3] = 1.0f;

  glDisable (GL_LIGHTING);
  glDisable (GL_LIGHT0);

  if (bLight)
    {
      glPushMatrix ();
	glLoadIdentity ();
	glLightfv (GL_LIGHT0, GL_POSITION, lightPos);
      glPopMatrix ();

      glEnable (GL_LIGHTING);
      glEnable (GL_LIGHT0);
    }
}


// -------------------------------------------------------------------------
// drawObb
//
// Draw an Oriented Bouding Box.
// -------------------------------------------------------------------------
void drawObb(const OBBox_t &obb) {
  Vector3f corners[8];

  corners[0] = Vector3f (-obb.extent._x, -obb.extent._y, -obb.extent._z);
  corners[1] = Vector3f ( obb.extent._x, -obb.extent._y, -obb.extent._z);
  corners[2] = Vector3f ( obb.extent._x,  obb.extent._y, -obb.extent._z);
  corners[3] = Vector3f (-obb.extent._x,  obb.extent._y, -obb.extent._z);
  corners[4] = Vector3f (-obb.extent._x, -obb.extent._y,  obb.extent._z);
  corners[5] = Vector3f ( obb.extent._x, -obb.extent._y,  obb.extent._z);
  corners[6] = Vector3f ( obb.extent._x,  obb.extent._y,  obb.extent._z);
  corners[7] = Vector3f (-obb.extent._x,  obb.extent._y,  obb.extent._z);

  glPushAttrib (GL_ENABLE_BIT);
  glDisable (GL_TEXTURE_2D);
  glDisable (GL_LIGHTING);

  for (int i = 0; i < 8; ++i) {
      corners[i] += obb.center;
      obb.world.transform (corners[i]);
  }

  GLuint indices[24] =
    {
      0, 1, 1, 2, 2, 3, 3, 0,
      4, 5, 5, 6, 6, 7, 7, 4,
      0, 4, 1, 5, 2, 6, 3, 7
    };

  glColor3f (1.0, 0.0, 0.0);

  glEnableClientState (GL_VERTEX_ARRAY);
  glVertexPointer (3, GL_FLOAT, 0, corners);
  glDrawElements (GL_LINES, 24, GL_UNSIGNED_INT, indices);
  glDisableClientState (GL_VERTEX_ARRAY);

  // GL_ENABLE_BIT
  glPopAttrib();
}

// -------------------------------------------------------------------------
// drawAxes
//
// Draw the X, Y and Z axes at the center of world.
// -------------------------------------------------------------------------
void drawAxes(const Matrix4x4f &modelView) {
  // Setup world model view matrix
  glLoadIdentity ();
  glMultMatrixf (modelView._m);

  // Draw the three axes
  glBegin (GL_LINES);
    // X-axis in red
    glColor3f (1.0f, 0.0f, 0.0f);
    glVertex3fv (kZeroVectorf._v);
    glVertex3fv (kZeroVectorf + Vector3f (10.0f, 0.0f, 0.0));

    // Y-axis in green
    glColor3f (0.0f, 1.0f, 0.0f);
    glVertex3fv (kZeroVectorf._v);
    glVertex3fv (kZeroVectorf + Vector3f (0.0f, 10.0f, 0.0));

    // Z-axis in blue
    glColor3f (0.0f, 0.0f, 1.0f);
    glVertex3fv (kZeroVectorf._v);
    glVertex3fv (kZeroVectorf + Vector3f (0.0f, 0.0f, 10.0));
  glEnd ();
}

// -------------------------------------------------------------------------
// renderMD5Object
//
// Render the 3D part of the scene.
// -------------------------------------------------------------------------
void renderMD5Object(Md5Object *object, double anim, Matrix4x4f *modelViewMatrix) {
  if(!object) {
	  return;
  }

  // Clear the window
  //glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  glMatrixMode (GL_MODELVIEW);
  glLoadIdentity ();

  // Camera rotation
  Matrix4x4f camera;

#if 0
  camera.identity ();

  glTranslated (-eye._x, -eye._y, -eye._z);
  glRotated (rot._x, 1.0f, 0.0f, 0.0f);
  glRotated (rot._y, 0.0f, 1.0f, 0.0f);
  glRotated (rot._z, 0.0f, 0.0f, 1.0f);
#else
  camera.fromEulerAngles (degToRad (rot._x),
			  degToRad (rot._y),
			  degToRad (rot._z));
  camera.setTranslation (-eye);
#endif

  Matrix4x4f axisRotation
    = RotationMatrix (kXaxis, -kPiOver2)
    * RotationMatrix (kZaxis, -kPiOver2);

  Matrix4x4f final = camera * axisRotation;
  //glMultMatrixf (final._m);
  if(modelViewMatrix) {
	  final = *modelViewMatrix;
  }

  // Setup scene lighting
  setupLight (0.0f, 20.0f, 100.0f);

  // Enable/disable texture mapping (fixed pipeline)
  if (bTextured)
    glEnable (GL_TEXTURE_2D);
  else
    glDisable (GL_TEXTURE_2D);

  // Enable/disable backface culling
  if (bCullFace)
    glEnable (GL_CULL_FACE);
  else
    glDisable (GL_CULL_FACE);

  // Setup polygon mode and shade model
  glPolygonMode (GL_FRONT_AND_BACK, bWireframe ? GL_LINE : GL_FILL);
  glShadeModel (bSmooth ? GL_SMOOTH : GL_FLAT);

  // Draw object
  object->setModelViewMatrix (final);
  object->setRenderFlags (renderFlags);
  //object->animate (bAnimate ? timer.deltaTime () : 0.0f);
  object->animate (anim);
  object->computeBoundingBox ();
  object->prepare (false);
  object->render ();

  if (bBounds)
    drawObb (object->boundingBox ());

  glDisable (GL_LIGHTING);
  glDisable (GL_TEXTURE_2D);

  drawAxes (final);
}

}}} //end namespace
