#include <cassert>
#include <cstring>
#define _USE_MATH_DEFINES
#include <cmath>
#include <iostream>
#include <vector>
#include <map>

#define GLEW_STATIC
#include <GL/glew.h>
#include <GLFW/glfw3.h>

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

#include "tiny_obj_loader.h"

#include "GLSL.h"
#include "MatrixStack.h"
#include "Shape.h"
#include "Program.h"
#include "Component.h"

#define PI 3.14159

using namespace std;
using namespace glm;

GLFWwindow *window; // Main application window
string RESOURCE_DIR = "./"; // Where the resources are loaded from
shared_ptr<Program> prog;
shared_ptr<Shape> shape;


GLuint progID;
map<string,GLint> attrIDs;
map<string,GLint> unifIDs;
map<string,GLuint> bufIDs;
int indCount;

// Root component global
Component root;

// Selected component
Component* selected = &root;

// This function is called when a char is input
static void char_callback(GLFWwindow *window, unsigned int c)
{
  char cp = (char)c;

  if(cp == ',')
  {
    // select...
    selected->selected = false;
    selected = &selected->getPrevious(NULL);
    selected->selected = true;
  }
  else if(cp == '.')
  {
    // select...
    selected->selected = false;
    selected = &selected->getNext(NULL);
    selected->selected = true;
  }

  if(cp == 'x')
  {
    // rotate...
    selected->r.x += 0.1;
  }
  else if(cp == 'X')
  {
    // rotate...
    selected->r.x -= 0.1;
  }

  if(cp == 'y')
  {
    // rotate...
    selected->r.y += 0.1;
  }
  else if(cp == 'Y')
  {
    // rotate...
    selected->r.y -= 0.1;
  }
  
  if(cp == 'z')
  {
    // rotate...
    selected->r.z += 0.1;
  }
  else if(cp == 'Z')
  {
    // rotate...
    selected->r.z -= 0.1;
  }
}

// This function is called when a GLFW error occurs
static void error_callback(int error, const char *description)
{
  cerr << description << endl;
}

// This function is called when a key is pressed
static void key_callback(GLFWwindow *window, int key, int scancode, int action, int mods)
{
  if(key == GLFW_KEY_ESCAPE && action == GLFW_PRESS) {
    glfwSetWindowShouldClose(window, GL_TRUE);
  }
}

// This function is called when the mouse is clicked
static void mouse_callback(GLFWwindow *window, int button, int action, int mods)
{
  // Do nothing
}

// If the window is resized, capture the new size and reset the viewport
static void resize_callback(GLFWwindow *window, int width, int height)
{
  glViewport(0, 0, width, height);
}

static void setParents(Component& elem)
{
  for (unsigned int i = 0; i < elem.children.size(); ++i)
  {
    setParents(elem.children[i]);
    elem.children[i].parent = &elem;
  }
}

void createRobot(Component& body)
{

  body.selected = true;

  //Make Body
  
  body.s.x = 1.5;
  body.s.y = 2.0;
  body.s.z = 1.0;


    // Make Head

    Component head;
    head.parent = &body;
   
    head.tp.x = 0.0;
    head.tp.y = body.s.y/2;
    head.tp.z = 0.0;

    head.t.x = 0.0;
    head.t.y = 0.5;
    head.t.z = 0.0;

  body.children.push_back(head);

    // Make Left Upper Arm
 
    Component luarm;
    luarm.parent = &body;
   
    luarm.tp.x = body.s.x/2;
    luarm.tp.y = body.s.y/4;
    luarm.tp.z = 0.0;

    luarm.s.x = 1.0;
    luarm.s.y = 0.5;
    luarm.s.z = 0.5;

    luarm.t.x = luarm.s.x/2;
    luarm.t.y = 0.0;
    luarm.t.z = 0.0;

      // Make Left Lower Arm
   
      Component llarm;
      llarm.parent = &luarm;
     
      llarm.tp.x = luarm.s.x/2;
      llarm.tp.y = 0.0;
      llarm.tp.z = 0.0;

      llarm.s.x = 1.5;
      llarm.s.y = 0.4;
      llarm.s.z = 0.4;

      llarm.t.x = llarm.s.x/2;
      llarm.t.y = 0.0;
      llarm.t.z = 0.0;

    luarm.children.push_back(llarm);
   
  body.children.push_back(luarm);

    // Make Left Upper Leg
 
    Component luleg;
    luleg.parent = &body;
   
    luleg.tp.x = body.s.x/4;
    luleg.tp.y = -body.s.y/2;
    luleg.tp.z = 0.0;

    luleg.s.x = 0.5;
    luleg.s.y = 1.0;
    luleg.s.z = 0.5;

    luleg.t.x = 0.0;
    luleg.t.y = -luleg.s.y/2;
    luleg.t.z = 0.0;
   
      // Make Left Lower Leg
   
      Component llleg;
      llleg.parent = &luleg;
     
      llleg.tp.x = 0.0;
      llleg.tp.y = -luleg.s.y/2;
      llleg.tp.z = 0.0;

      llleg.s.x = 0.4;
      llleg.s.y = 1.5;
      llleg.s.z = 0.4;


      llleg.t.x = 0.0;
      llleg.t.y = -llleg.s.y/2;
      llleg.t.z = 0.0;
    
    luleg.children.push_back(llleg);

  body.children.push_back(luleg);
  
    // Make Right Upper Leg
 
    Component ruleg;
    ruleg.parent = &body;
   
    ruleg.tp.x = -body.s.x/4;
    ruleg.tp.y = -body.s.y/2;
    ruleg.tp.z = 0.0;

    ruleg.s.x = 0.5;
    ruleg.s.y = 1.0;
    ruleg.s.z = 0.5;

    ruleg.t.x = 0.0;
    ruleg.t.y = -ruleg.s.y/2;
    ruleg.t.z = 0.0;
   
      // Make Right Lower Leg
   
      Component rlleg;
      rlleg.parent = &ruleg;
     
      rlleg.tp.x = 0.0;
      rlleg.tp.y = -ruleg.s.y/2;
      rlleg.tp.z = 0.0;

      rlleg.s.x = 0.4;
      rlleg.s.y = 1.5;
      rlleg.s.z = 0.4;


      rlleg.t.x = 0.0;
      rlleg.t.y = -rlleg.s.y/2;
      rlleg.t.z = 0.0;
    
    ruleg.children.push_back(rlleg);

  body.children.push_back(ruleg);
  
    // Make Right Upper Arm
 
    Component ruarm;
    ruarm.parent = &body;
   
    ruarm.tp.x = -body.s.x/2;
    ruarm.tp.y = body.s.y/4;
    ruarm.tp.z = 0.0;

    ruarm.s.x = 1.0;
    ruarm.s.y = 0.5;
    ruarm.s.z = 0.5;

    ruarm.t.x = -ruarm.s.x/2;
    ruarm.t.y = 0.0;
    ruarm.t.z = 0.0;

      // Make Right Lower Arm
   
      Component rlarm;
      rlarm.parent = &ruarm;
     
      rlarm.tp.x = -ruarm.s.x/2;
      rlarm.tp.y = 0.0;
      rlarm.tp.z = 0.0;

      rlarm.s.x = 1.5;
      rlarm.s.y = 0.4;
      rlarm.s.z = 0.4;

      rlarm.t.x = -rlarm.s.x/2;
      rlarm.t.y = 0.0;
      rlarm.t.z = 0.0;

    ruarm.children.push_back(rlarm);
   
  body.children.push_back(ruarm);

  setParents(body);
}

static void init()
{
  GLSL::checkVersion();

  // Set background color.
  glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
  // Enable z-buffer test.
  glEnable(GL_DEPTH_TEST);

  // Initialize mesh.
  shape = make_shared<Shape>();
  shape->loadMesh(RESOURCE_DIR + "cube.obj");
  shape->init();

  // Initialize the GLSL program.
  prog = make_shared<Program>();
  prog->setVerbose(false); // Set this to true when debugging.
  prog->setShaderNames(RESOURCE_DIR + "vert.glsl", RESOURCE_DIR + "frag.glsl");
  prog->init();
  prog->addUniform("P");
  prog->addUniform("MV");
  prog->addAttribute("aPos");
  prog->addAttribute("aNor");

  createRobot(root);
  selected = &root;

  // If there were any OpenGL errors, this will print something.
  // You can intersperse this line in your code to find the exact location
  // of your OpenGL error.
  GLSL::checkError(GET_FILE_LINE);
}

static void render()
{
  // Get current frame buffer size.
  int width, height;
  glfwGetFramebufferSize(window, &width, &height);
  float aspect = width/(float)height;
  glViewport(0, 0, width, height);

  // Clear framebuffer.
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  // Create matrix stacks.
  auto P = make_shared<MatrixStack>();
  auto MV = make_shared<MatrixStack>();
  // Apply projection.
  P->pushMatrix();
  P->multMatrix(glm::perspective((float)(45.0*M_PI/180.0), aspect, 0.01f, 100.0f));
  // Apply camera transform.
  MV->pushMatrix();
  MV->translate(glm::vec3(0, -0.5, -10));

  // Draw mesh using GLSL.
  prog->bind();
  //	glUniformMatrix4fv(prog->getUniform("P"), 1, GL_FALSE, &P->topMatrix()[0][0]);
  //	glUniformMatrix4fv(prog->getUniform("MV"), 1, GL_FALSE, &MV->topMatrix()[0][0]);
  //	shape->draw(prog);

  root.draw(MV, P, shape, prog);

  prog->unbind();

  // Pop matrix stacks.
  MV->popMatrix();
  P->popMatrix();

  GLSL::checkError(GET_FILE_LINE);
}

int main(int argc, char **argv)
{
  if(argc < 2) {
    cout << "Please specify the resource directory." << endl;
    return 0;
  }
  RESOURCE_DIR = argv[1] + string("/");

  // Set error callback.
  glfwSetErrorCallback(error_callback);
  // Initialize the library.
  if(!glfwInit()) {
    return -1;
  }
  // Create a windowed mode window and its OpenGL context.
  window = glfwCreateWindow(640, 480, "Alexander Huddleston", NULL, NULL);
  if(!window) {
    glfwTerminate();
    return -1;
  }
  // Make the window's context current.
  glfwMakeContextCurrent(window);
  // Initialize GLEW.
  glewExperimental = true;
  if(glewInit() != GLEW_OK) {
    cerr << "Failed to initialize GLEW" << endl;
    return -1;
  }
  glGetError(); // A bug in glewInit() causes an error that we can safely ignore.
  cout << "OpenGL version: " << glGetString(GL_VERSION) << endl;
  cout << "GLSL version: " << glGetString(GL_SHADING_LANGUAGE_VERSION) << endl;
  GLSL::checkVersion();
  // Set vsync.
  glfwSwapInterval(1);
  // Set char callback.
  glfwSetCharCallback(window, char_callback);
  // Set keyboard callback.
  glfwSetKeyCallback(window, key_callback);
  // Set the mouse call back.
  glfwSetMouseButtonCallback(window, mouse_callback);
  // Set the window resize call back.
  glfwSetFramebufferSizeCallback(window, resize_callback);
  // Initialize scene.
  init();
  // Loop until the user closes the window.
  while(!glfwWindowShouldClose(window)) {
    // Render scene.
    render();
    // Swap front and back buffers.
    glfwSwapBuffers(window);
    // Poll for and process events.
    glfwPollEvents();
  }
  // Quit program.
  glfwDestroyWindow(window);
  glfwTerminate();
  return 0;
}
