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

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

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

#include "MyCamera.h"
#include "GLSL.h"
#include "MatrixStack.h"
#include "Program.h"
#include "Shape.h"
#include "Material.h"
#include "Light.h"
#include "Component.h"

using namespace std;

GLFWwindow *window; // Main application window
string RESOURCE_DIR = "./"; // Where the resources are loaded from

shared_ptr<Camera> camera;
shared_ptr<Program> prog;
shared_ptr<Program> sprog;
shared_ptr<Shape> shape;
shared_ptr<Shape> sphere;
shared_ptr<Shape> cube;
shared_ptr<Shape> teapot;

bool keyToggles[256] = {false}; // only for English keyboards!

vector<Material> materials;
Light l[2];
int ind = 0;
int lind = 0;
int sind = 0;

unsigned int pid0;
unsigned int pid1;

//Variables affecting the assortment of shapes.
unsigned int n = 10;
float s = 1.0;
float tx = 1.0;
float ty = 1.0;
float tz = 1.0;
vector<Component> shapes;

// 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_button_callback(GLFWwindow *window, int button, int action, int mods)
{
	// Get the current mouse position.
	double xmouse, ymouse;
	glfwGetCursorPos(window, &xmouse, &ymouse);
	// Get current window size.
	int width, height;
	glfwGetWindowSize(window, &width, &height);
	if(action == GLFW_PRESS) {
		bool shift = (mods & GLFW_MOD_SHIFT) != 0;
		bool ctrl  = (mods & GLFW_MOD_CONTROL) != 0;
		bool alt   = (mods & GLFW_MOD_ALT) != 0;
		camera->mouseClicked((float)xmouse, (float)ymouse, shift, ctrl, alt);
	}
}

// This function is called when the mouse moves
static void cursor_position_callback(GLFWwindow* window, double xmouse, double ymouse)
{
	int state = glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_LEFT);
	if(state == GLFW_PRESS) {
		camera->mouseMoved((float)xmouse, (float)ymouse);
	}
}

static void char_callback(GLFWwindow *window, unsigned int key)
{
	keyToggles[key] = !keyToggles[key];
  char cp = (char)key;

  if(cp == 'W' || cp == 'w')
  {
    vec3 temp(0.0f, 0.0f, -0.1f);
    camera->transpose(temp);
  }
  if(cp == 'A' || cp == 'a')
  {
    vec3 temp(-0.1f, 0.0f, 0.0f);
    camera->transpose(temp);
  }
  if(cp == 'S' || cp == 's')
  {
    vec3 temp(0.0f, 0.0f, 0.1f);
    camera->transpose(temp);
  }
  if(cp == 'D' || cp == 'd')
  {
    vec3 temp(0.1f, 0.0f, 0.0f);
    camera->transpose(temp);
  }
  if(cp == 'X')
  {
          glm::vec3 temp = l[lind].getPos();
          l[lind].setPos(glm::vec3(temp.x + 0.1f, temp.y, temp.z));
  }
  if(cp == 'x')
  {
          glm::vec3 temp = l[lind].getPos();
          l[lind].setPos(glm::vec3(temp.x - 0.1f, temp.y, temp.z));
  }
  
  if(cp == 'Y')
  {
          glm::vec3 temp = l[lind].getPos();
          l[lind].setPos(glm::vec3(temp.x, temp.y + 0.1f, temp.z));
  }
  if(cp == 'y')
  {
          glm::vec3 temp = l[lind].getPos();
          l[lind].setPos(glm::vec3(temp.x, temp.y - 0.1f, temp.z));
  }
  
  if(cp == 'Z')
  {
          glm::vec3 temp = l[lind].getPos();
          l[lind].setPos(glm::vec3(temp.x, temp.y, temp.z + 0.1f));
  }
  if(cp == 'z')
  {
          glm::vec3 temp = l[lind].getPos();
          l[lind].setPos(glm::vec3(temp.x, temp.y, temp.z - 0.1f));
  }
}

// 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 createComponents()
{
  for(unsigned int i = 0; i < n; i++)
  {
          Component temp;
          
          temp.s.x = s;
          temp.s.y = s;
          temp.s.z = s;
         
          temp.tp.x = 0;
          temp.tp.y = 0;
          temp.tp.z = -tz*((float)i);

          temp.t.x = 1.0/2.0;
          temp.t.y = 0;
          temp.t.z = -1.0/2.0;

          temp.sid = rand()%3;

          shapes.push_back(temp);
  }
}

static void createMaterials()
{
  Material m1;
  
  materials.push_back(m1);
  materials.push_back(m1);
  materials.push_back(m1);
  materials.push_back(m1);
  materials.push_back(m1);
  materials.push_back(m1);
  materials.push_back(m1);
  materials.push_back(m1);
  materials.push_back(m1);
  materials.push_back(m1);
  materials.push_back(m1);

  materials[0].setMaterial(glm::vec3(0.0f, 0.0f, 0.4f), glm::vec3(0.2f, 0.1f, 0.7f), glm::vec3(0.5f, 0.5f, 0.5f), 200.0f);
  materials[1].setMaterial(glm::vec3(0.3f, 0.3f, 0.4f), glm::vec3(0.2f, 0.2f, 0.3f), glm::vec3(0.1f, 0.1f, 0.1f), 10.0f);
  materials[2].setMaterial(glm::vec3(0.2f, 0.2f, 0.2f), glm::vec3(0.8f, 0.7f, 0.7f), glm::vec3(1.0f, 0.9f, 0.8f), 200.0f);
  materials[3].setMaterial(glm::vec3(0.2f, 0.8f, 0.2f), glm::vec3(0.1f, 0.7f, 0.2f), glm::vec3(0.2f, 0.2f, 0.2f), 100.0f);
  materials[4].setMaterial(glm::vec3(0.8f, 0.2f, 0.2f), glm::vec3(0.7f, 0.1f, 0.1f), glm::vec3(0.3f, 0.3f, 0.3f), 100.0f);
  materials[5].setMaterial(glm::vec3(0.7f, 0.7f, 0.1f), glm::vec3(0.6f, 0.5f, 0.2f), glm::vec3(0.1f, 0.1f, 0.1f), 50.0f);
  materials[6].setMaterial(glm::vec3(0.5f, 0.1f, 0.1f), glm::vec3(0.7f, 0.1f, 0.1f), glm::vec3(0.2f, 0.2f, 0.2f), 50.0f);
  materials[7].setMaterial(glm::vec3(0.2f, 0.8f, 0.8f), glm::vec3(0.1f, 0.7f, 0.6f), glm::vec3(0.5f, 0.5f, 0.5f), 120.0f);
  materials[8].setMaterial(glm::vec3(0.3f, 0.6f, 0.3f), glm::vec3(0.3f, 0.6f, 0.3f), glm::vec3(0.7f, 0.7f, 0.7f), 20.0f);
  materials[9].setMaterial(glm::vec3(0.8f, 0.2f, 0.7f), glm::vec3(0.7f, 0.2f, 0.8f), glm::vec3(0.5f, 0.5f, 0.5f), 200.0f);
  materials[10].setMaterial(glm::vec3(0.2f, 0.5f, 0.2f), glm::vec3(0.2f, 0.5f, 0.2f), glm::vec3(0.5f, 0.5f, 0.5f), 10.0f);
}

static void makeGround()
{
  Component temp;
  
  temp.tp.x = 0.0f;
  temp.tp.y = -1.0f;
  temp.tp.z = 0.0f;

  temp.s.x = 100.0f;
  temp.s.y = 0.0f;
  temp.s.z = 100.0f;

  temp.t.x = 0.0f;
  temp.t.y = 0.0f;
  temp.t.z = 0.0f;

  temp.sid = 2;

  shapes.push_back(temp);
}

// This function is called once to initialize the scene and OpenGL
static void init()
{
	// Initialize time.
	glfwSetTime(0.0);
	
	// Set background color.
	glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
	// Enable z-buffer test.
	glEnable(GL_DEPTH_TEST);

  sprog = make_shared<Program>();
  sprog->setShaderNames(RESOURCE_DIR + "vert.glsl", RESOURCE_DIR + "sil.glsl");
  sprog->setVerbose(false);
  sprog->init();
  sprog->addAttribute("aPos");
  sprog->addAttribute("aNor");
  sprog->addUniform("MV");
  sprog->addUniform("P");
  
  prog = make_shared<Program>();
  prog->setShaderNames(RESOURCE_DIR + "vert.glsl", RESOURCE_DIR + "frag.glsl");
  prog->setVerbose(false);
  prog->init();
  prog->addAttribute("aPos");
  prog->addAttribute("aNor");
  prog->addUniform("MV");
  prog->addUniform("P");
  prog->addUniform("MVL");
  prog->addUniform("lightPos1");
  prog->addUniform("lightPos2");
  prog->addUniform("ka");
 	prog->addUniform("kd");
 	prog->addUniform("ks");
 	prog->addUniform("s");
  prog->addUniform("i1");
  prog->addUniform("i2");

	camera = make_shared<Camera>();
	camera->setInitDistance(2.0f);
	
	shape = make_shared<Shape>();
	shape->loadMesh(RESOURCE_DIR + "bunny.obj");
	shape->fitToUnitBox();
	shape->init();
  
  sphere = make_shared<Shape>();
	sphere->loadMesh(RESOURCE_DIR + "sphere.obj");
	sphere->fitToUnitBox();
	sphere->init();
 
  cube = make_shared<Shape>();
	cube->loadMesh(RESOURCE_DIR + "cube.obj");
	cube->fitToUnitBox();
	cube->init();

  teapot = make_shared<Shape>();
	teapot->loadMesh(RESOURCE_DIR + "teapot.obj");
	teapot->fitToUnitBox();
	teapot->init();

  createMaterials();
  
  Light l1(glm::vec3(1.0f, 1.0f, 1.0f), 0.8f);
  Light l2(glm::vec3(-1.0f, 1.0f, 1.0f), 0.2f);
  l[0] = l1;
  l[1] = l2;

  // separate function to create shapes.
  createComponents();

  makeGround();

  GLSL::checkError(GET_FILE_LINE);
}

// This function is called every frame to draw the scene.
static void render()
{
	// Clear framebuffer.
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	if(keyToggles[(unsigned)'c']) {
		glEnable(GL_CULL_FACE);
	} else {
		glDisable(GL_CULL_FACE);
	}
 	// Get current frame buffer size.
	int width, height;
	glfwGetFramebufferSize(window, &width, &height);
	camera->setAspect((float)width/(float)height);
	
	// Matrix stacks
	auto P = make_shared<MatrixStack>();
	auto MV = make_shared<MatrixStack>();
	
	// Apply camera transforms
	P->pushMatrix();
	camera->applyProjectionMatrix(P);
	MV->pushMatrix();
	camera->applyViewMatrix(MV);

  prog->bind();
  
  mat4 templ;
  templ[0] = vec4(1.0, 0, 0, 0);
  templ[1] = vec4(0, 1.0, 0, 0);
  templ[2] = vec4(0, 0, 1.0, 0);
  templ[3] = vec4(l[0].getPos().x, l[0].getPos().y, l[0].getPos().z, 1.0);
  templ = MV->topMatrix()*templ;
  glUniformMatrix4fv(prog->getUniform("MVL"), 1, GL_FALSE, glm::value_ptr(templ));
  
  Component light;
  
  light.t.x = l[0].getPos().x;
  light.t.y = l[0].getPos().y;
  light.t.z = l[0].getPos().z;
    
  glUniform3f(prog->getUniform("lightPos1"), l[0].getPos().x, l[0].getPos().y, l[0].getPos().z);
  glUniform3f(prog->getUniform("ka"), 0.95, 1.0, 0.35);
  glUniform3f(prog->getUniform("kd"), 1.0, 1.0, 1.0);
  glUniform3f(prog->getUniform("ks"), 1.0, 1.0, 1.0);
  glUniform1f(prog->getUniform("s"), 1.0);
 
  light.draw(MV, P, sphere, prog);
  prog->unbind();
  
  glm::vec3 ambient = materials.at(0).getAmbient();
  glm::vec3 diffuse = materials.at(0).getDiffuse();
  glm::vec3 specular = materials.at(0).getSpecular();
  float shine = materials.at(0).getShiny();
  for(unsigned int i = 0; i < n + 1; i++)
  {
    prog->bind();
    ambient = materials.at(i).getAmbient();
    diffuse = materials.at(i).getDiffuse();
    specular = materials.at(i).getSpecular();
    shine = materials.at(i).getShiny();
   
    glUniform3f(prog->getUniform("ka"), ambient.r, ambient.g, ambient.b);
    glUniform3f(prog->getUniform("kd"), diffuse.r, diffuse.g, diffuse.b);
    glUniform3f(prog->getUniform("ks"), specular.r, specular.g, specular.b);
    glUniform1f(prog->getUniform("s"), shine);
    glUniform1f(prog->getUniform("i1"), l[0].getIntensity());
    glUniform1f(prog->getUniform("i2"), l[1].getIntensity());
    
    switch(shapes.at(i).sid)
    {
      case 0:
        shapes.at(i).draw(MV, P, shape, prog);
        break;
      case 1:
        shapes.at(i).draw(MV, P, teapot, prog);
        break;
      case 2:
        shapes.at(i).draw(MV, P, cube, prog);
        break;
      default:
        shapes.at(i).draw(MV, P, shape, prog);
        break;
    };
    prog->unbind();
  }
	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, "Alex Huddleston Assignment 4", 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 keyboard callback.
	glfwSetKeyCallback(window, key_callback);
	// Set char callback.
	glfwSetCharCallback(window, char_callback);
	// Set cursor position callback.
	glfwSetCursorPosCallback(window, cursor_position_callback);
	// Set mouse button callback.
	glfwSetMouseButtonCallback(window, mouse_button_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;
}
