#include <thread> // http://msdn.microsoft.com/ko-kr/library/vstudio/hh920526(v=vs.120).aspx/
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <cmath>
#include "Debugger.h"
#include "GLEW/glew.h"
#include "GLFW/glfw3.h"
#include "GLM/glm.hpp"
#include "GLM/gtc/matrix_transform.hpp"
#include "GLM/gtc/type_ptr.hpp"
#include "GuiManager.h"
#include "VertexBuffer.h"
#include "IndexBuffer.h"
#include "VertexArray.h"
#include "Shader.h"
#include "CameraController.h"
#include "Texture.h"
#include "LoadOBJ.h"
#include "Generator.h"

static int window_width = 800;
static int window_height = 600;

void WindowSizeCallback(GLFWwindow* window, int new_window_width, int new_window_height);


int main(void)
{
	// Create debugger
	Debugger* debugger = Debugger::GetInstance();
	std::thread debuger_thread(&Debugger::StartHelper, debugger); // Open Debug thread | 디버깅용(콘솔) 쓰레드 열기

	// Create window
	GLFWwindow* window;
	
	// Initialize the library
	if (!glfwInit())
	{
		debugger->WriteError("GLFW initialization failed !");
		return -1;
	}

	// Create a windowed mode window and its OpenGL context
	window = glfwCreateWindow(window_width, window_height, "Procedural Tree Generator", NULL, NULL);
	if (!window)
	{
		debugger->WriteError("Creating a GLFW window failed !");
		glfwTerminate();
		return -1;
	}

	glfwMakeContextCurrent(window); // Make the window's context current

	glfwSetWindowSizeCallback(window, WindowSizeCallback); // If window size is changed, call WindowSizeCallback function
	glfwSwapInterval(1); // Activate v-sync

	// Initialize GLEW
	if (GLEW_OK == glewInit())
	{
		// GLEW initialized successfully
		debugger->WriteMessage("GLEW initialized successfully...");

		// Show OpenGL & graphics driver version info
		debugger->ShowGLInfo();
	}
	else
	{
		// GLEW initialization failed
		debugger->WriteError("GLEW initialization failed !");
	}

	// Ensure we can capture the escape key being pressed below
	glfwSetInputMode(window, GLFW_STICKY_KEYS, GL_TRUE);
	// Hide the mouse and enable unlimited mouvement
	//glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);

	// Set the mouse at the center of the screen
	//glfwPollEvents();
	//glfwSetCursorPos(window, window_size_width / 2, window_size_height / 2);

	// Create CameraController
	CameraController camera_controller(window);

	// Create CameraController (with arc ball rotation)
	//CameraController &camera_controller = CameraController::getInstance();
	//camera_controller.BindWindow(window);
	//glfwSetCursorPosCallback(window, CameraController::mousecb);

	/* - Make cumulated transformation matrix - */
	glm::mat4 model_space_matrix = glm::mat4(1.0f); // Just unit matrix
	glm::mat4 view_space_matrix;
	glm::mat4 projection_space_matrix;
	glm::mat4 mvp_matrix;


	// Generate L model (for Branch) // I changed the containers(Generator, VertexArray, VertexBuffer, IndexBuffer) to heap pointers because the value of the vertex array can be changed later by the GUI
	Generator* generator = new Generator();
	generator->generate(20);

	BFSize branch_buffer_size = generator->getBranchBufferSize();
	float* branch_vertex_buffer_data = new float[branch_buffer_size.vBuffer];
	unsigned int* branch_indices = new unsigned int[branch_buffer_size.iBuffer];

	generator->bindBranchBuffers(branch_vertex_buffer_data, branch_indices);
	generator->transferToBranchBuffer();


	// Generate L model (for Leaf)
	BFSize leaf_buffer_size = generator->getLeafBufferSize();
	float* leaf_vertex_buffer_data = new float[leaf_buffer_size.vBuffer];
	unsigned int* leaf_indices = new unsigned int[leaf_buffer_size.iBuffer];

	generator->bindLeafBuffers(leaf_vertex_buffer_data, leaf_indices);
	generator->transferToLeafBuffer();

	//glFrontFace(GL_CW); // We solved!



	// Read our sample .obj file (sample teapot)
	//float obj_vertex_buffer_data[16384][8] = { 0 }; // If an error occurs when load the OBJ file, please double up the size of array
	//unsigned int obj_number_of_triangle = 0;
	//LoadOBJ(debugger->GetProgramPath() + "\\580_Rendering_Project\\Resources\\Models\\tea.obj", obj_vertex_buffer_data, obj_number_of_triangle);
	//unsigned int default_indices[16384] = { 0 };
	//for (int i = 0; i < 16384; i++)
	//{
	//	default_indices[i] = i;
	//}

	//// Define triangle vertices positions (sample square)
	//float square_vertex_positions [] = 
	//{
	//	// Positions (x, y, z),			Vertex Normals (Nx, Ny, Nz),	Texture coordinates (u, v)
	//	-0.5f, -0.5f, +0.0f,			+0.0, +0.0, +1.0,				0.0f, 0.0f, // index = 0
	//	+0.5f, -0.5f, +0.0f,			+0.0, +0.0, +1.0,				1.0f, 0.0f, // index = 1
	//	+0.5f, +0.5f, +0.0f,			+0.0, +0.0, +1.0,				1.0f, 1.0f, // index = 2
	//	-0.5f, +0.5f, +0.0f,			+0.0, +0.0, +1.0,				0.0f, 1.0f, // index = 3
	//};

	//// Define vertex indices (sample square)
	//unsigned int indices [] =
	//{
	//	0, 1, 2, // 1st triangle
	//	2, 3, 0, // 2nd triangle
	//};


	// Create vertex array object (for Branch)
	VertexArray* vertex_array_branch = new VertexArray();
	VertexBuffer* vertex_buffer_branch = new VertexBuffer(branch_vertex_buffer_data, branch_buffer_size.vBuffer * sizeof(float));
	//VertexBuffer vertex_buffer(obj_vertex_buffer_data, 8 * obj_number_of_triangle * sizeof(float)); // for obj reader
	//VertexBuffer vertex_buffer(square_vertex_positions, 8 * 4 * sizeof(float)); // for sample square

	VertexBufferLayout buffer_layout_branch;
	buffer_layout_branch.Push<float>(3); // Iterate (x, y, z)
	buffer_layout_branch.Push<float>(3); // Iterate (Nx, Ny, Nz)
	buffer_layout_branch.Push<float>(2); // Iterate (u, v)
	vertex_array_branch->AddBuffer(*vertex_buffer_branch, buffer_layout_branch);

	IndexBuffer* index_buffer_branch = new IndexBuffer(branch_indices, branch_buffer_size.iBuffer);
	//IndexBuffer index_buffer(default_indices, obj_number_of_triangle); // for obj reader
	//IndexBuffer index_buffer(indices, 3 * 2); // for sample square


	// Create vertex array object (for Leaf)
	VertexArray* vertex_array_leaf = new VertexArray();
	VertexBuffer* vertex_buffer_leaf = new VertexBuffer(leaf_vertex_buffer_data, leaf_buffer_size.vBuffer * sizeof(float));

	VertexBufferLayout buffer_layout_leaf;
	buffer_layout_leaf.Push<float>(3); // Iterate (x, y, z)
	buffer_layout_leaf.Push<float>(3); // Iterate (Nx, Ny, Nz)
	buffer_layout_leaf.Push<float>(2); // Iterate (u, v)
	vertex_array_leaf->AddBuffer(*vertex_buffer_leaf, buffer_layout_leaf);

	IndexBuffer* index_buffer_leaf = new IndexBuffer(leaf_indices, leaf_buffer_size.iBuffer);



	Shader shader(debugger->GetProgramPath() + "\\580_Rendering_Project\\Resources\\Shaders\\Advanced.shader");
	shader.Bind();


	// Create texture
	Texture sample_texture(debugger->GetProgramPath() + "\\580_Rendering_Project\\Resources\\Textures\\sample_image.png");
	sample_texture.Bind(0);
	shader.SetUniform1i("fragment_uniform_texture_sampler", 0); // to choose texture slot (default = 0)



	// Clean all bound buffers
	vertex_array_branch->UnBind();
	vertex_buffer_branch->UnBind();
	index_buffer_branch->UnBind();
	vertex_array_leaf->UnBind();
	vertex_buffer_leaf->UnBind();
	index_buffer_leaf->UnBind();
	shader.UnBind();
	sample_texture.UnBind();



	// ImGui initalize
	GuiManager gui_manager(window);


	// Enable depth test
	glEnable(GL_DEPTH_TEST);
	// Accept fragment if it closer to the camera than the former one
	glDepthFunc(GL_LESS);
	// Cull triangles which normal is not towards the camera
	glEnable(GL_CULL_FACE);
	// Enable alpha blending
	glEnable(GL_BLEND);
	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

	// Loop start until the user closes the window
	while (!glfwWindowShouldClose(window) && (glfwGetKey(window, GLFW_KEY_ESCAPE) != GLFW_PRESS))
	{
		// Clear the screen (with depth buffer)
		GLCALL(glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT));

		// OpenGL render start
		static const float gray[] = { 0.5f, 0.5f, 0.5f };
		GLCALL(glClearBufferfv(GL_COLOR, 0, gray)); // Gray background


		// Compute new matrices by inputs from keyboard and mouse
		camera_controller.ComputeMatricesFromInputs();
		view_space_matrix = camera_controller.GetViewMatrix();
		projection_space_matrix = camera_controller.GetProjectionMatrix();
		mvp_matrix = projection_space_matrix * view_space_matrix * model_space_matrix;


		// Bind all
		shader.Bind();
		shader.SetUniformMatrix4f("vertex_uniform_MVP", mvp_matrix);
		shader.SetUniformMatrix4f("vertex_uniform_V", view_space_matrix);
		shader.SetUniformMatrix4f("vertex_uniform_M", model_space_matrix);
		shader.SetUniform3f("vertex_fragment_uniform_light_position_worldspace", gui_manager.light_position[0], gui_manager.light_position[1], gui_manager.light_position[2]);
		shader.SetUniform1f("fragment_uniform_light_intensity", gui_manager.light_intensity);
		shader.SetUniform1f("fragment_uniform_light_attenuation", gui_manager.light_attenuation);
		shader.SetUniform3f("fragment_uniform_ambient_light_color", gui_manager.ambient_color[0], gui_manager.ambient_color[1], gui_manager.ambient_color[2]);
		shader.SetUniform3f("fragment_uniform_diffuse_light_color", gui_manager.diffuse_color[0], gui_manager.diffuse_color[1], gui_manager.diffuse_color[2]);
		shader.SetUniform3f("fragment_uniform_specular_light_color", gui_manager.specular_color[0], gui_manager.specular_color[1], gui_manager.specular_color[2]);
		

		
		// Just sample objects for testing
		//glDrawElements(GL_TRIANGLES, obj_number_of_triangle, GL_UNSIGNED_INT, nullptr); // (mode, number of indices, type, index buffer pointer (we already bound it)) // for obj reader
		//glDrawElements(GL_TRIANGLES, 3 * 2, GL_UNSIGNED_INT, nullptr); // sample square (mode, number of indices, type, index buffer pointer (we already bound it)) // for sample square



		// Draw call (for Branches)
		if (gui_manager.m_bark_texture_selected_texture_ID != 0) // First, check whether other texture selected or not in the texture browser
		{
			GLCALL(glActiveTexture(GL_TEXTURE0 + 0));
			GLCALL(glBindTexture(GL_TEXTURE_2D, gui_manager.m_bark_texture_selected_texture_ID));
		}
		else
		{
			sample_texture.Bind();
		}

		vertex_array_branch->Bind();
		if (gui_manager.m_need_to_show_branch) // If "view branches" button is activated
		{
			GLCALL(glDrawElements(GL_TRIANGLES, branch_buffer_size.iBuffer, GL_UNSIGNED_INT, nullptr)); // (mode, number of indices, type, index buffer pointer (we already bound it))
		}


		// Draw call (for Leaves)
		if (gui_manager.m_leaf_texture_selected_texture_ID != 0) // First, check whether other texture selected or not in the texture browser
		{
			GLCALL(glActiveTexture(GL_TEXTURE0 + 0));
			GLCALL(glBindTexture(GL_TEXTURE_2D, gui_manager.m_leaf_texture_selected_texture_ID));
		}
		else
		{
			sample_texture.Bind();
		}

		vertex_array_leaf->Bind();
		if (gui_manager.m_need_to_show_leaf) // If "view leaves" button is activated
		{
			GLCALL(glDrawElements(GL_TRIANGLES, leaf_buffer_size.iBuffer, GL_UNSIGNED_INT, nullptr)); // (mode, number of indices, type, index buffer pointer (we already bound it))
		}





		// If we need to re-generate tree
		if (gui_manager.m_need_to_regenerate == true)
		{
			gui_manager.m_need_to_regenerate = false;
			
			debugger->WriteMessage("New L-system is generating...");

			// Get parameter from GUI & Generate L model (for Branch)
			delete generator;
			//generator = new Generator();
			generator = new Generator(gui_manager.m_tree_size, gui_manager.m_branch_tallness, gui_manager.m_branch_shrinkage, gui_manager.m_branch_max_angle, gui_manager.m_branch_angle_turbulence, gui_manager.m_branch_number_of_new_branches_spawned_by_one_trunk, gui_manager.m_branch_number_of_new_branches, gui_manager.m_branch_number_of_new_branches_fluctuation, gui_manager.m_leaf_density, gui_manager.m_leaf_height, gui_manager.m_tree_random_seed);
			generator->generate(gui_manager.m_iteration);

			branch_buffer_size = generator->getBranchBufferSize();
			branch_vertex_buffer_data = new float[branch_buffer_size.vBuffer];
			branch_indices = new unsigned int[branch_buffer_size.iBuffer];

			generator->bindBranchBuffers(branch_vertex_buffer_data, branch_indices);
			generator->transferToBranchBuffer();

			// Get parameter from GUI & Generate L model (for Leaf)
			leaf_buffer_size = generator->getLeafBufferSize();
			leaf_vertex_buffer_data = new float[leaf_buffer_size.vBuffer];
			leaf_indices = new unsigned int[leaf_buffer_size.iBuffer];

			generator->bindLeafBuffers(leaf_vertex_buffer_data, leaf_indices);
			generator->transferToLeafBuffer();


			// Create vertex array object (for Branch)
			delete vertex_array_branch;
			delete vertex_buffer_branch;
			delete index_buffer_branch;

			vertex_array_branch = new VertexArray();
			vertex_buffer_branch = new VertexBuffer(branch_vertex_buffer_data, branch_buffer_size.vBuffer * sizeof(float));
			vertex_array_branch->AddBuffer(*vertex_buffer_branch, buffer_layout_branch);
			index_buffer_branch = new IndexBuffer(branch_indices, branch_buffer_size.iBuffer);

			// Create vertex array object (for Leaf)
			delete vertex_array_leaf;
			delete vertex_buffer_leaf;
			delete index_buffer_leaf;

			vertex_array_leaf = new VertexArray();
			vertex_buffer_leaf = new VertexBuffer(leaf_vertex_buffer_data, leaf_buffer_size.vBuffer * sizeof(float));
			vertex_array_leaf->AddBuffer(*vertex_buffer_leaf, buffer_layout_leaf);
			index_buffer_leaf = new IndexBuffer(leaf_indices, leaf_buffer_size.iBuffer);
		}

		// If we need to export obj file
		if (gui_manager.m_need_to_export_obj == true)
		{
			gui_manager.m_need_to_export_obj = false;

			generator->saveAsObj();
		}

		// If we need to save project file
		if (gui_manager.m_need_to_save_project == true)
		{
			gui_manager.m_need_to_save_project = false;

			Debugger::GetInstance()->WriteMessage("Writing tree data to Project file...");

			std::ofstream file;
			file.open(debugger->GetProgramPath() + "\\580_Rendering_Project\\Resources\\Projects\\Project.tree");

			file << gui_manager.m_iteration << " ";
			file << gui_manager.m_tree_size << " ";
			file << gui_manager.m_branch_tallness << " ";
			file << gui_manager.m_branch_shrinkage << " ";
			file << gui_manager.m_branch_max_angle << " ";
			file << gui_manager.m_branch_angle_turbulence << " ";
			file << gui_manager.m_branch_number_of_new_branches_spawned_by_one_trunk << " ";
			file << gui_manager.m_branch_number_of_new_branches << " ";
			file << gui_manager.m_branch_number_of_new_branches_fluctuation << " ";
			file << gui_manager.m_leaf_density << " ";
			file << gui_manager.m_leaf_height << " ";
			file << gui_manager.m_tree_random_seed << " ";

			Debugger::GetInstance()->WriteMessage("Project file saved successfully...");
		}

		// If we need to open project file
		if (gui_manager.m_need_to_open_project == true)
		{
			gui_manager.m_need_to_open_project = false;

			Debugger::GetInstance()->WriteMessage("Opening tree data from Project file...");

			std::ifstream file;
			file.open(debugger->GetProgramPath() + "\\580_Rendering_Project\\Resources\\Projects\\Project.tree");

			std::string read_one_line;
			std::getline(file, read_one_line);
			std::stringstream stream_one_line(read_one_line);

			stream_one_line >> gui_manager.m_iteration >> gui_manager.m_tree_size >> gui_manager.m_branch_tallness >> gui_manager.m_branch_shrinkage >> gui_manager.m_branch_max_angle >> gui_manager.m_branch_angle_turbulence >> gui_manager.m_branch_number_of_new_branches_spawned_by_one_trunk >> gui_manager.m_branch_number_of_new_branches >> gui_manager.m_branch_number_of_new_branches_fluctuation >> gui_manager.m_leaf_density >> gui_manager.m_leaf_height >> gui_manager.m_tree_random_seed;
			
			Debugger::GetInstance()->WriteMessage("Project file opened successfully...");

			gui_manager.m_need_to_regenerate = true; // We need regenerating tree
		}


		// Draw ImGui
		gui_manager.DrawGui();

		// Swap front and back buffers
		glfwSwapBuffers(window);

		// Poll for and process events
		glfwPollEvents();
	}


	// ImGui cleanup
	gui_manager.DeleteGui();

	// GLFW cleanup
	glfwTerminate();
	glfwDestroyWindow(window);

	// Delete debugger | 디버거 제거
	debugger->WriteMessage("Program is ready to shut down...");
	debuger_thread.join(); // Wait & close thread | 스레드 닫기
	debugger->FreeInstance();
	debugger = nullptr;

	return 0;
}


/* - This function called when window size changes | 윈도우 사이즈가 변경되면 호출되는 함수 - */
void WindowSizeCallback(GLFWwindow* window, int new_window_width, int new_window_height)
{
	window_width = new_window_width;
	window_height = new_window_height;
	glViewport(0, 0, window_width, window_height);
	//CameraController::getInstance().setWindowWidthHeight(new_window_width, new_window_height); // For arc ball rotation
}

