/**
 * @author	Andrew Robert Owens, Philmo Gu
 * @date 	2019-10-15.
 * @details Organization: Biological Modeling and Visualization research group
 * University of Calgary, Calgary, AB, Canada
 *
 * Contact:	arowens [at] ucalgary.ca
 * @copyright Copyright (c) 2019 ___ALGORITHMIC_BOTANY___. All rights reserved.
 *
 * @brief
 */

#include <array>
#include <iostream>
#include <vector>
#include <sstream>
#include <cmath>
#include <cassert>	//assert

// glad beforw glfw
#include "glad/glad.h"
//
#include "GLFW/glfw3.h"

#include "common_matrices.hpp"
#include "obj_mesh_file_io.hpp"
#include "obj_mesh.hpp"
#include "mat4f.hpp"
#include "mat3f.hpp"
#include "shader.hpp"
#include "program.hpp"
#include "triangle.hpp"
#include "vec3f.hpp"
#include "vec2f.hpp"
#include "shader_file_io.hpp"
#include "buffer_object.hpp"
#include "vertex_array_object.hpp"
#include "vbo_tools.hpp"
#include "texture.hpp"
#include "image.hpp"

using namespace math;
using namespace geometry;
using namespace opengl;

// GLOBAL Variables
Mat4f g_M = Mat4f::identity();
Mat4f g_V = Mat4f::identity();
Mat4f g_P = Mat4f::identity();

GLuint g_width = 1000, g_height = 1000;

// function declaration
using namespace std;

void setFrameBufferSize(GLFWwindow *window, int width, int height) {
  g_width = width;
  g_height = height;
  glViewport(0, 0, g_width, g_height);
  g_P = perspectiveProjection(30, float(g_width) / g_height, 0.01, 100.f);
}

void setKeyboard(GLFWwindow *window, int key, int scancode, int action,
                 int mods) {

  // Students should try and fix:
  // g_M holds the transformation of the model
  // we keep multiplying on the left or right with new matrices
  // What effect does this have on translation/rotation/scale

  if (GLFW_KEY_LEFT == key) {
    if (GLFW_REPEAT == action || GLFW_PRESS == action) {
      g_M = rotateAboutYMatrix(5) * g_M;
    }
  } else if (GLFW_KEY_RIGHT == key) {
    if (GLFW_REPEAT == action || GLFW_PRESS == action) {
      g_M = rotateAboutYMatrix(-5) * g_M;
    }
  } else if (GLFW_KEY_UP == key) {
    if (GLFW_REPEAT == action || GLFW_PRESS == action) {
      g_M = uniformScaleMatrix(1.1) * g_M;
    }
  } else if (GLFW_KEY_DOWN == key) {
    if (GLFW_REPEAT == action || GLFW_PRESS == action) {
      g_M = uniformScaleMatrix(1. / 1.1) * g_M;
    }
  } else if (GLFW_KEY_W == key) {
    if (GLFW_REPEAT == action || GLFW_PRESS == action) {
      g_M = translateMatrix(0, 0.1, 0) * g_M;
    }
  } else if (GLFW_KEY_S == key) {
    if (GLFW_REPEAT == action || GLFW_PRESS == action) {
      g_M = translateMatrix(0, -0.1, 0) * g_M;
    }
  } else if (GLFW_KEY_D == key) {
    if (GLFW_REPEAT == action || GLFW_PRESS == action) {
      g_M = translateMatrix(0.1, 0, 0) * g_M;
    }
  } else if (GLFW_KEY_A == key) {
    if (GLFW_REPEAT == action || GLFW_PRESS == action) {
      g_M = translateMatrix(-0.1, 0, 0) * g_M;
    }
  } else if (GLFW_KEY_ESCAPE == key) {
    glfwSetWindowShouldClose(window, GLFW_TRUE);
  }
}

// user defined alias
opengl::Program createShaderProgram(std::string const &vertexShaderFile,
                                    std::string const &fragmentShaderFile) {
  using namespace opengl;
  auto vertexShaderSource = loadShaderStringFromFile(vertexShaderFile);
  auto fragmentShaderSource = loadShaderStringFromFile(fragmentShaderFile);

  std::cout << "[Log] compiling program " << vertexShaderFile << ' '
            << fragmentShaderFile << '\n';
  return opengl::makeProgram(vertexShaderSource, fragmentShaderSource);
}

opengl::Program createShaderProgram(std::string const &vertexShaderFile,
                                    std::string const &geometryShaderFile,
                                    std::string const &fragmentShaderFile) {
  using namespace opengl;
  auto vertexShaderSource = loadShaderStringFromFile(vertexShaderFile);
  auto geometryShaderSource = loadShaderStringFromFile(geometryShaderFile);
  auto fragmentShaderSource = loadShaderStringFromFile(fragmentShaderFile);

  std::cout << "[Log] compiling program " << vertexShaderFile << ' '
            << geometryShaderFile << ' ' << fragmentShaderFile << '\n';
  return opengl::makeProgram(vertexShaderSource, geometryShaderSource,
                             fragmentShaderSource);
}

std::string glfwVersion() {
  std::ostringstream s("GLFW version: ", std::ios::in | std::ios::ate);
  // print version
  int glfwMajor, glfwMinor, glfwRevision;
  glfwGetVersion(&glfwMajor, &glfwMinor, &glfwRevision);
  s << glfwMajor << '.' << glfwMinor << '.' << glfwRevision;
  return s.str();
}

GLFWwindow *initWindow() {
  GLFWwindow *window = nullptr;

  if (!glfwInit()) {
    exit(EXIT_FAILURE);
  }

  std::cout << glfwVersion() << '\n';

  // set opengl version
  glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
  glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
  glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
  glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE);
  glfwWindowHint(GLFW_SAMPLES, 4);

  window = glfwCreateWindow(1000,           // width
                            1000,           // height
                            "Mesh Loading", // title
                            NULL,           // unused
                            NULL);
  if (!window) {
    glfwTerminate();
    exit(EXIT_FAILURE);
  }

  glfwMakeContextCurrent(window);
  gladLoadGLLoader((GLADloadproc)glfwGetProcAddress);

  glfwSwapInterval(1); // vsync
  glEnable(GL_MULTISAMPLE);
  glEnable(GL_DEPTH_TEST);
  // glEnable(GL_CULL_FACE);
  // glCullFace(GL_BACK);

  //Polygon fill mode
  glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
  //glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

  // setup callbacks
  glfwSetFramebufferSizeCallback(window, setFrameBufferSize);
  glfwSetKeyCallback(window, setKeyboard);

  return window;
}

void bindTexture(opengl::Texture const &texture, opengl::Program const &program, std::string s, GLuint activeTex) {
	assert(activeTex < 32);
	glActiveTexture(GL_TEXTURE0 + activeTex); // Activate texture in position "activeTex"
	texture.bind();		//Bind texture object to active texture
	
	//Set bound texture to sampler2D uniform in shader program in position "activeTex" 
	program.use();
	glUniform1i(program.uniformLocation(s), activeTex);	
}

int main() {
  GLFWwindow *window = initWindow();

  auto vao = makeVertexArrayObject();
  auto indexBuffer = makeBufferObject();
  auto vertexBuffer = makeBufferObject();
  GLuint totalIndices = 0;

  auto tex = generateTexture();

  {
    geometry::OBJMesh meshData;
    //std::string filePath = "../models/simple/ico_sphere_w_normals.obj";
	//std::string filePath = "../models/simple/simple_bunny_ccw.obj";
	std::string filePath = "../models/simple/square/square_w_texCoord.obj";
     

    if (!geometry::loadOBJMeshFromFile(filePath, meshData)) {
      std::cerr << "[Error] Cannot load .obj\n";
    }

	//Calculate vertex normals if they are not included in .obj file
	if (meshData.normals.size() == 0) {
		auto normals =
			geometry::calculateVertexNormals(meshData.triangles, meshData.vertices);
		
		auto vboData = opengl::makeConsistentVertexNormalIndices(meshData, normals);

		totalIndices =
			opengl::setup_vao_and_buffers(vao, indexBuffer, vertexBuffer, vboData);
	}

	//Pass in vertex normals if they are already included in .obj file
	else {		
		if (meshData.textureCoords.empty()) {
			auto vboData = opengl::makeConsistentVertexNormalIndices(meshData);

			totalIndices =
				opengl::setup_vao_and_buffers(vao, indexBuffer, vertexBuffer, vboData);
		}
		else {
			auto vboData = opengl::makeConsistentVertexTextureCoordNormalIndices(meshData);

			totalIndices =
				opengl::setup_vao_and_buffers(vao, indexBuffer, vertexBuffer, vboData);
		}
	}

  } // ... mesh is deconstruced here (freeing memory)

  {
	  auto image = raster::read_image_from_file(
		  "../models/simple/square/SquareNormalMap.png");

	  glActiveTexture(GL_TEXTURE0);	
	  tex.bind();
	  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
	  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
	  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

	  auto channels = GL_RGBA;
	  if (image.channels() == 3)
		  channels = GL_RGB;
	  
	  glTexImage2D(GL_TEXTURE_2D,  //target
		0,                 		//level
		channels,         		//internalformat
		image.width(),    		//width
		image.height(),   		//height
		0,                		//border
		channels,           	//format
		GL_UNSIGNED_BYTE, 		//type
		image.data());			//data
	  
	  glGenerateMipmap(GL_TEXTURE_2D);
  }

  Vec3f lightPosition(1, 0, 0);
  Vec3f viewPosition(0, 0, 3);

  g_V = lookAtMatrix(viewPosition,    // eye position
                     {0.f, 0.f, 0.f}, // look at
                     {0.f, 1.f, 0.f}  // up vector
                     );
  g_P = perspectiveProjection(30.,  // fov
                              1.f,  // ratio
                              0.1f, // distance to near plane
                              100.f // distance to far plane
                              );

  auto depthShader = createShaderProgram("../shaders/depth_vs.glsl",
										"../shaders/depth_fs.glsl");
  auto normalShader = createShaderProgram("../shaders/normal_vs.glsl",
                                        "../shaders/normal_fs.glsl");
  auto textureShader = createShaderProgram("../shaders/texture_vs.glsl",
										"../shaders/texture_gs.glsl",
										"../shaders/texture_fs.glsl");

  assert(depthShader && normalShader && textureShader);	//Check console for program compilation error

  depthShader.use();
  setUniformVec3f(depthShader.uniformLocation("lightPosition"), lightPosition);
  
  //Set to one shader program 
  //opengl::Program *program = &depthShader;
  //opengl::Program *program = &normalShader;
  opengl::Program *program = &textureShader;

  //Load texture to OpenGL
  bindTexture(tex, *program, "tex", 0);

  while (!glfwWindowShouldClose(window)) {

    glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);

    program->use();

    vao.bind();
    setUniformMat4f(program->uniformLocation("model"), g_M, true);
    setUniformMat4f(program->uniformLocation("view"), g_V, true);
    setUniformMat4f(program->uniformLocation("projection"), g_P, true);

    glDrawElements(GL_TRIANGLES,
                   totalIndices,    // # of triangles * 3
                   GL_UNSIGNED_INT, // type of indices
                   (void *)0        // offset
                   );

    glfwSwapBuffers(window); // swaps back buffer to front for drawing to screen
    glfwPollEvents();        // will process event queue and carry on
  }

  // cleaup window, and glfw before exit
  glfwDestroyWindow(window);
  glfwTerminate();

  return EXIT_SUCCESS;
}
