#include <iostream>
#include <fstream>

#define GLM_FORCE_RADIANS
#include <glm/glm.hpp>
#include <glm/gtc/type_ptr.hpp>

#define TINYOBJLOADER_IMPLEMENTATION
#include "tiny_obj_loader.h"

#include "ShapeSkin.h"
#include "GLSL.h"
#include "Program.h"

using namespace std;

ShapeSkin::ShapeSkin() :
	prog(NULL),
	elemBufID(0),
	posBufID(0),
	norBufID(0),
	texBufID(0)
{
}

ShapeSkin::~ShapeSkin()
{
}

void ShapeSkin::loadMesh(const string &meshName)
{
	// Load geometry
	tinyobj::attrib_t attrib;
	std::vector<tinyobj::shape_t> shapes;
	std::vector<tinyobj::material_t> materials;
	string errStr;
	bool rc = tinyobj::LoadObj(&attrib, &shapes, &materials, &errStr, meshName.c_str());
	if(!rc) {
		cerr << errStr << endl;
	} else {
		posBuf = attrib.vertices;
		norBuf = attrib.normals;
		texBuf = attrib.texcoords;
		assert(posBuf.size() == norBuf.size());
		// Loop over shapes
		for(size_t s = 0; s < shapes.size(); s++) {
			// Loop over faces (polygons)
			const tinyobj::mesh_t &mesh = shapes[s].mesh;
			size_t index_offset = 0;
			for(size_t f = 0; f < mesh.num_face_vertices.size(); f++) {
				size_t fv = mesh.num_face_vertices[f];
				// Loop over vertices in the face.
				for(size_t v = 0; v < fv; v++) {
					// access to vertex
					tinyobj::index_t idx = mesh.indices[index_offset + v];
					elemBuf.push_back(idx.vertex_index);
				}
				index_offset += fv;
				// per-face material (IGNORE)
				shapes[s].mesh.material_ids[f];
			}
		}
    posBufInit = posBuf;
	}
}

void ShapeSkin::loadAttachment(const std::string &filename)
{
	unsigned int nverts, nbones;
	ifstream in;
	in.open(filename);
	if(!in.good()) {
		cout << "Cannot read " << filename << endl;
		return;
	}
	string line;
	getline(in, line); // comment
	getline(in, line); // comment
	getline(in, line);
	stringstream ss0(line);
	ss0 >> nverts;
	ss0 >> nbones;
	assert((unsigned int)nverts == posBuf.size()/3);
	while(1) {
		getline(in, line);
		if(in.eof()) {
			break;
		}
		// Parse line
		stringstream ss(line);
    float temp;
    std::vector<float> tempVec;
    std::vector<unsigned int> tempIndexes;
    for(unsigned int i = 0; i < nbones; i++)
    {
      ss >> temp;
      tempVec.push_back(temp);
      if(temp > 0.0f)
      {
        tempIndexes.push_back((unsigned int) i);
      }
    }
    indexes.push_back(tempIndexes);
    boneInfluence.push_back(tempVec);
	}
	in.close();
}

void ShapeSkin::loadSkeleton(const std::string &filename)
{
	unsigned int nframes, nbones;
	ifstream in;
	in.open(filename);
	if(!in.good()) {
		cout << "Cannot read " << filename << endl;
		return;
	}
	string line;
	getline(in, line); // comment
	getline(in, line); // comment
	getline(in, line); // comment
	getline(in, line);
	stringstream ss0(line);
	ss0 >> nframes;
	ss0 >> nbones;
	while(1) {
		getline(in, line);
		if(in.eof()) {
			break;
		}
		// Parse line
		stringstream ss(line);
    vert temp;
    
    float tempx;
    float tempy;
    float tempz;
    float tempw;

    std::vector<vert> tempVec;
    tempVec.clear();
    for(unsigned int i = 0; i < nbones; i++)
    {
      tempVec.push_back(temp);
      ss >> tempx;
      ss >> tempy;
      ss >> tempz;
      ss >> tempw;
      tempVec.at(tempVec.size() - 1).q = glm::quat(tempw, tempx, tempy, tempz);

      ss >> tempx;
      ss >> tempy;
      ss >> tempz;
      tempVec.at(tempVec.size() - 1).p = glm::vec3(tempx, tempy, tempz);
    }
    vertInfo.push_back(tempVec);
	}
	in.close();

  glm::mat4 E;

  std::vector<glm::mat4> tempMatVec;

  for(unsigned int j = 0; j < vertInfo.size(); j++)
  {
    for(unsigned int k = 0; k < nbones; k++)
    {
      E = glm::mat4_cast(vertInfo[j][k].q);
      E[3] = glm::vec4(vertInfo[j][k].p, 1.0f);
      if(j == 0)
      {
        tempMatVec.push_back(glm::inverse(E));
      }
      else
        tempMatVec.push_back(E);
    }
      transMats.push_back(tempMatVec);
    tempMatVec.clear();
  }
}

void ShapeSkin::init()
{
	// Send the position array to the GPU
	glGenBuffers(1, &posBufID);
	glBindBuffer(GL_ARRAY_BUFFER, posBufID);
	glBufferData(GL_ARRAY_BUFFER, posBuf.size()*sizeof(float), &posBuf[0], GL_STATIC_DRAW);
	
	// Send the normal array to the GPU
	glGenBuffers(1, &norBufID);
	glBindBuffer(GL_ARRAY_BUFFER, norBufID);
	glBufferData(GL_ARRAY_BUFFER, norBuf.size()*sizeof(float), &norBuf[0], GL_STATIC_DRAW);

	// No texture info
	texBufID = 0;
	
	// Send the element array to the GPU
	glGenBuffers(1, &elemBufID);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, elemBufID);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, elemBuf.size()*sizeof(unsigned int), &elemBuf[0], GL_STATIC_DRAW);
	
	// Unbind the arrays
	glBindBuffer(GL_ARRAY_BUFFER, 0);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
	
	assert(glGetError() == GL_NO_ERROR);
}

void ShapeSkin::draw(float t, std::shared_ptr<MatrixStack> MV) const
{
	assert(prog);
	
	int h_pos = prog->getAttribute("aPos");
	glEnableVertexAttribArray(h_pos);
	glBindBuffer(GL_ARRAY_BUFFER, posBufID);
	glVertexAttribPointer(h_pos, 3, GL_FLOAT, GL_FALSE, 0, (const void *)0);
	
	int h_nor = prog->getAttribute("aNor");
	glEnableVertexAttribArray(h_nor);
	glBindBuffer(GL_ARRAY_BUFFER, norBufID);
	glVertexAttribPointer(h_nor, 3, GL_FLOAT, GL_FALSE, 0, (const void *)0);
	
	// Draw
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, elemBufID);
	glDrawElements(GL_TRIANGLES, (int)elemBuf.size(), GL_UNSIGNED_INT, (const void *)0);
	
	glDisableVertexAttribArray(h_nor);
	glDisableVertexAttribArray(h_pos);
	glBindBuffer(GL_ARRAY_BUFFER, 0);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

	float alpha = std::fmod(60.0f*t, (float)transMats.size() - 2);
  float u = std::fmod(alpha, 1.0f);
  int frame = std::ceil(alpha);

  for(unsigned int j = 0; j < transMats[frame].size(); j++)
  {
    glm::mat4 p0 = transMats[frame][j];
    glm::mat4 p1 = transMats[frame + 1][j];


	  MV->pushMatrix();
      glm::mat4 temp((1 - u)*p0 + u*p1);
      MV->multMatrix(glm::inverse(transMats[0][j]) + temp);
      glm::vec3 pp = glm::vec3(0.0f, 0.0f, 0.0f);
 
	  glUniformMatrix4fv(prog->getUniform("MV"), 1, GL_FALSE, glm::value_ptr(MV->topMatrix()));
    // Draw frenet frame
    glBegin(GL_LINE_STRIP);
      glColor3f(1.0, 0.0, 0.0);
      glVertex3f(pp.x, pp.y, pp.z);
      glVertex3f(pp.x + 0.1f, pp.y + 0.0f, pp.z + 0.0f);
      glColor3f(0.0, 1.0, 0.0);
      glVertex3f(pp.x, pp.y, pp.z);
      glVertex3f(pp.x + 0.0f, pp.y + 0.1f, pp.z + 0.0f);
      glColor3f(0.0, 0.0, 1.0);
      glVertex3f(pp.x, pp.y, pp.z);
      glVertex3f(pp.x + 0.0f, pp.y + 0.0f, pp.z + 0.1f);
    glEnd();
	  MV->popMatrix();
  }
  
  //
}

void ShapeSkin::skin(float t)
{
	float alpha = std::fmod(60.0f*t, (float)transMats.size() - 2);
  float u = std::fmod(alpha, 1.0f);
  int frame = std::ceil(alpha);

  std::vector<glm::mat4> tempInterpMats;

  for(unsigned int j = 0 ; j < boneInfluence[0].size(); j++)
  {
    glm::mat4 p0 = transMats[frame][j];
    glm::mat4 p1 = transMats[frame + 1][j];
    glm::mat4 temp((1 - u)*p0 + u*p1);
    tempInterpMats.push_back(temp);
  }


  for(unsigned int i = 0; i < indexes.size(); i++)
  {
    glm::vec4 temp(posBufInit[i*3], posBufInit[i*3 + 1], posBufInit[i*3 + 2], 1.0f);
    glm::vec4 tempOutput(0.0f, 0.0f, 0.0f, 0.0f);
    for(unsigned int j = 0; j < indexes[i].size(); j++)
    {
      glm::mat4 tempMat = transMats[0][indexes[i][j]];
      glm::mat4 tempTransMat = tempInterpMats[indexes[i][j]];
      float w = boneInfluence[i][indexes[i][j]];
      
      glm::vec4 temp1 = tempMat*temp;
      glm::vec4 temp2 = tempTransMat*temp1;
      glm::vec4 temp3 = w*temp2;

      tempOutput = tempOutput + temp3;
    }
    posBuf[i*3] = tempOutput.x;
    posBuf[i*3 + 1] = tempOutput.y;
    posBuf[i*3 + 2] = tempOutput.z;
  }
	glBindBuffer(GL_ARRAY_BUFFER, posBufID);
	glBufferData(GL_ARRAY_BUFFER, posBuf.size()*sizeof(float), &posBuf[0], GL_STATIC_DRAW);
}

void ShapeSkin::setMaterial(Material mat)
{
  m = mat;
}

Material ShapeSkin::getMaterial()
{
  return m;
}

