#include "Application.h"
#include "Mesh.h"
#include "Shader.h"

#include "gl.h"

#include <glm/mat4x4.hpp>
#include <glm/mat3x3.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <imgui.h>
#include <imgui_impl_glfw_gl3.h>

#include <stb_image.h>

#include <stdlib.h>
#include <stdio.h>

#include <iostream>
#include <fstream>
#include <map>
#include <vector>

using glm::vec3;
using glm::uvec3;
using std::map;
using std::vector;

static void error_callback(int error, const char* description) {
	fprintf(stderr, "Error: %s\n", description);
}
static void key_callback(GLFWwindow* window, int key, int scancode, int action,
		int mods) {
	Application::instance().KeyCallback(window, key, scancode, action, mods);
}

static void cursor_callback(GLFWwindow* window, double x, double y) {
	Application::instance().CursorCallback(window, x, y);
}

static void mouse_button_callback(GLFWwindow* window, int button, int action,
		int mods) {
	Application::instance().MouseButtonCallback(window, button, action, mods);

}
static void scroll_callback(GLFWwindow* window, double xoffset,
		double yoffset) {
	Application::instance().ScrollCallback(window, xoffset, yoffset);
}

static void char_callback(GLFWwindow* window, unsigned int c) {
	Application::instance().CharCallback(window, c);
}

Application::Application() :
		window(0), density(), densityIsolevel(0.2), polygonizer(density,
				densityIsolevel), textureSize(512), texturemapper(density,
				textureSize), programId(0), meteoridCount(1000), meteroidMinRadius(
				0.005), meteroidMaxRadius(0.2), meteroidSizeExponent(2048), densityFrequency(
				2), densityOctave(2), densityScale(1.0f), resolution(0.1), smoothMesh(
				true), viewDistance(3.0f), fpsMode(false), meshUploadRequest(
				false), textureUploadRequest(false) {
	init();
}

Application::~Application() {
	shutdown();
}

Application &Application::instance() {
	static Application app;
	return app;
}

void Application::init() {
	glfwSetErrorCallback(error_callback);

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

	//	glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);

	window = glfwCreateWindow(1200, 800, "Procedural Asteroid Generator", NULL,
	NULL);
	if (!window) {
		glfwTerminate();
		exit(EXIT_FAILURE);
	}

	GLFWimage icon;
	int n;
	icon.pixels = stbi_load("assets/icon.png", &icon.width, &icon.height, &n, 4);
	if (icon.pixels) {
		glfwSetWindowIcon(window, 1, &icon);
		stbi_image_free(icon.pixels);
	} else
		std::cout << "Failed to load icon.png" << std::endl;

	glfwSetMouseButtonCallback(window, mouse_button_callback);
	glfwSetCursorPosCallback(window, cursor_callback);
	glfwSetScrollCallback(window, scroll_callback);
	glfwSetKeyCallback(window, key_callback);
	glfwSetCharCallback(window, char_callback);

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

	glfwGetCursorPos(window, &lastMouseX, &lastMouseY);

	std::cout << "OpenGL version: " << glGetString(GL_VERSION) << std::endl;
	std::cout << "GLSL version: " << glGetString(GL_SHADING_LANGUAGE_VERSION)
			<< std::endl;
	std::cout << "Vendor: " << glGetString(GL_VENDOR) << std::endl;
	std::cout << "Renderer: " << glGetString(GL_RENDERER) << std::endl;

	ImGui_ImplGlfwGL3_Init(window, false);

	GLCK(glGenTextures(1, &tAlbedoId))
	GLCK(glGenTextures(1, &tNormalId))
	GLCK(glGenTextures(1, &tRoughnessId))
	GLCK(glGenTextures(1, &tMetalicId))

	loadShader();
	glMesh.create();
	glMesh.update(programId);
	generateAsteroid();

	skybox.Load("background");
}

void Application::shutdown() {
	ImGui_ImplGlfwGL3_Shutdown();

	glfwDestroyWindow(window);
	glfwTerminate();
}

void Application::KeyCallback(GLFWwindow* window, int key, int scancode,
		int action, int mods) {
	if (key == GLFW_KEY_ESCAPE && action == GLFW_RELEASE)
		glfwSetWindowShouldClose(window, GLFW_TRUE);
	if (ImGui::IsMouseHoveringAnyWindow())
		ImGui_ImplGlfwGL3_KeyCallback(window, key, scancode, action, mods);
	else {
		if (key == GLFW_KEY_TAB && action == GLFW_RELEASE) {
			toogleFpsMode();
		}
	}
}
void Application::CursorCallback(GLFWwindow* window, double x, double y) {

}
void Application::MouseButtonCallback(GLFWwindow* window, int button,
		int action, int mods) {
	ImGui_ImplGlfwGL3_MouseButtonCallback(window, button, action, mods);
}
void Application::ScrollCallback(GLFWwindow* window, double xoffset,
		double yoffset) {
	if (ImGui::IsMouseHoveringAnyWindow())
		ImGui_ImplGlfwGL3_ScrollCallback(window, xoffset, yoffset);
	else
		viewDistance -= yoffset * viewDistance * 0.1;
}
void Application::CharCallback(GLFWwindow* window, unsigned int c) {

	ImGui_ImplGlfwGL3_CharCallback(window, c);
}

void Application::toogleFpsMode() {
	if (fpsMode) {
		fpsMode = false;
	} else {
		fpsMode = true;
		fpsTrafo = glm::lookAt(vec3(0, 0, viewDistance), vec3(0, 0, 0),
				vec3(0, 1, 0));
	}
}

void Application::generateAsteroid() {
	if (meshProgress.begin("Random Impacts", 1)) {
		if (density.getImpacts().size() == 0)
			density.addRandomImpacts(1000, densityIsolevel, 0.01, 0.005, 0.2,
					256);

		//density.saveCrossSection("density.png", 512);
	}
	polygonizer.polygonize(resolution, meshProgress);

	if (smoothMesh && meshProgress.begin("Smooth", 1)) {
		polygonizer.mesh.smooth();
//		{
//			std::ofstream out("stage2.obj");
//			saveAttrib(out, "v ", polygonizer.vertices);
//			saveFaces(out, polygonizer.indices, 1);
//		}
	}

	if (meshProgress.begin("Generate Edges", 1)) {
		polygonizer.mesh.generateEdges();
	}

	if (meshProgress.begin("Generate Adjacent", 1)) {
		polygonizer.mesh.generateAdjacent();
	}

	if (meshProgress.begin("Generate Face Normals", 1)) {
		polygonizer.mesh.generateFaceNormals();
	}

	if (meshProgress.begin("Generate Vertex Normals", 1)) {
		polygonizer.mesh.generateNormals();
	}

	if (meshProgress.begin("Generate Patched", 1)) {
		mesh = polygonizer.mesh.createPatched(0.8);
	}

	if (meshProgress.begin("Save Patched", 1)) {
		mesh.save("patched.obj");
	}

	if (meshProgress.begin("Map Textures", 1)) {
		texturemapper.map(mesh.positions, mesh.normals, mesh.indices);
	}

	if (meshProgress.begin("Move To Mean", 1)) {
		mesh.moveToMean();
	}

	if (meshProgress.begin("Save", 4)) {
		mesh.save("final.obj");
	}

	meshProgress.finish();

	if (!meshProgress.isCanceled()) {
		meshUploadRequest = true;
		textureUploadRequest = true;
	}

}

void Application::loadShader() {
	if (programId >= 0)
		GLCK(glDeleteProgram(programId))
	programId = loadProgram("standard");

	GLCK(MVPloc = glGetUniformLocation(programId, "MVP"))
	GLCK(Mloc = glGetUniformLocation(programId, "M"))
	GLCK(Vloc = glGetUniformLocation(programId, "V"))
	GLCK(
			LightPosition_worldspaceloc = glGetUniformLocation(programId, "LightPosition_worldspace"))

	GLCK(tAlbedoLoc = glGetAttribLocation(programId, "tAlbedo"))
	GLCK(tNormalLoc = glGetAttribLocation(programId, "tNormal"))
	GLCK(tRoughnessLoc = glGetAttribLocation(programId, "tRoughness"))
	GLCK(tMetalicLoc = glGetAttribLocation(programId, "tMetalic"))
}

void Application::uploadTexture() {
	printf("upload texture\n");
	glBindTexture(GL_TEXTURE_2D, tAlbedoId);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, texturemapper.getTextureSize(),
			texturemapper.getTextureSize(), 0, GL_RGBA, GL_UNSIGNED_BYTE,
			texturemapper.albedo.data());
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
}

void Application::renderMesh() {
	glEnable(GL_DEPTH_TEST);
	glDepthFunc(GL_LESS);
	glEnable(GL_CULL_FACE);

	glMesh.render();
}

void Application::updateMatrices() {
	double mouseX, mouseY;
	glfwGetCursorPos(window, &mouseX, &mouseY);

	if (fpsMode) {
		glm::mat4 fpsTrafoInv = glm::inverse(fpsTrafo);

		glm::vec4 movement(0.0);
		if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS) {
			movement += glm::vec4(0.0, 0.0, 0.1, 0.0);
		} else if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS) {
			movement -= glm::vec4(0.0, 0.0, 0.1, 0.0);
		}

		if (glfwGetMouseButton(window, 0) == GLFW_PRESS) {
			glm::vec3 up = fpsTrafoInv * glm::vec4(0.0, 1.0, 0.0, 0.0);
			glm::vec3 side = fpsTrafoInv * glm::vec4(1.0, 0.0, 0.0, 0.0);
			glm::vec3 front = fpsTrafoInv * glm::vec4(0.0, 0.0, 1.0, 0.0);
			fpsTrafo = glm::rotate(fpsTrafo,
					(float) (mouseX - lastMouseX) * 0.01f, up);
			fpsTrafo = glm::rotate(fpsTrafo,
					(float) (mouseY - lastMouseY) * 0.01f, side);
		}
		//fpsTrafo = glm::rotate(fpsTrafo, mouseX - lastMouseX, up);
		glm::vec3 absolute = fpsTrafoInv * movement;
		fpsTrafo = glm::translate(fpsTrafo, absolute);

		view = fpsTrafo;
	} else {

		if (!ImGui::IsMouseHoveringAnyWindow() && glfwGetMouseButton(window, 0) == GLFW_PRESS) {
			glm::mat4 modelTrafoInv = glm::inverse(modelTrafo);
			modelTrafo = glm::rotate(modelTrafo,
					(float) (mouseX - lastMouseX) * 0.01f,
					vec3(modelTrafoInv * glm::vec4(0.0, 1.0, 0.0, 0.0)));
			modelTrafo = glm::rotate(modelTrafo,
					(float) (mouseY - lastMouseY) * 0.01f,
					vec3(modelTrafoInv * glm::vec4(1.0, 0.0, 0.0, 0.0)));
		}

		//model = glm::rotate(model, (float) glfwGetTime(), vec3(0.f, 1.f, 0.f));

		view = glm::lookAt(vec3(0, 0, viewDistance), // Camera is at (4,3,3), in World Space
		vec3(0, 0, 0), // and looks at the origin
		vec3(0, 1, 0) // Head is up (set to 0,-1,0 to look upside-down)
				);
	}
	lastMouseX = mouseX;
	lastMouseY = mouseY;

	perspective = glm::perspective(glm::radians(45.0f),
			(float) width / (float) height, 0.1f, 100.0f);
	mvp = perspective * view * modelTrafo;
}

void Application::prepareShader() {
	GLCK(glUseProgram(programId))
	if (MVPloc >= 0) {
		GLCK(glUniformMatrix4fv(MVPloc, 1, GL_FALSE, glm::value_ptr(mvp)))
	}
	if (Mloc >= 0) {
		GLCK(glUniformMatrix4fv(Mloc, 1, GL_FALSE, glm::value_ptr(modelTrafo)))
	}
	if (Vloc >= 0) {
		GLCK(glUniformMatrix4fv(Vloc, 1, GL_FALSE, glm::value_ptr(view)))
	}
	if (LightPosition_worldspaceloc >= 0) {
		vec3 LightPosition_worldspace(10.0f, 10.0f, 10.0f);
		GLCK(
				glUniform3fv(LightPosition_worldspaceloc, 1, glm::value_ptr(LightPosition_worldspace)))
	}

	int idx = 0;
	if (tAlbedoLoc >= 0) {
		glUniform1i(tAlbedoLoc, idx);
		glActiveTexture(GL_TEXTURE0 + idx);
		glBindTexture(GL_TEXTURE_2D, tAlbedoId);
		idx++;
	}
	if (tNormalLoc >= 0) {
		glUniform1i(tNormalLoc, idx);
		glActiveTexture(GL_TEXTURE0 + idx);
		glBindTexture(GL_TEXTURE_2D, tNormalId);
		idx++;
	}
	if (tRoughnessLoc >= 0) {
		glUniform1i(tRoughnessLoc, idx);
		glActiveTexture(GL_TEXTURE0 + idx);
		glBindTexture(GL_TEXTURE_2D, tRoughnessId);
		idx++;
	}
	if (tMetalicLoc >= 0) {
		glUniform1i(tMetalicLoc, idx);
		glActiveTexture(GL_TEXTURE0 + idx);
		glBindTexture(GL_TEXTURE_2D, tMetalicId);
		idx++;
	}
}

void Application::gui() {
	ImGui::Begin("Mesh Generation");
	ImGui::Text("Vertices: %lu, Triangles: %lu", mesh.positions.size(),
			mesh.indices.size());
	//ImGui::InputFloat("Resolution", &resolution);
	ImGui::SliderFloat("Resolution", &resolution, 0.1, 0.001, "%.3f", 0.1);
	if (ImGui::SliderFloat("Frequency", &densityFrequency, 1, 10, "%.0f", 1))
		density.setFrequency(densityFrequency);
	if (ImGui::SliderFloat("Octave", &densityOctave, 1, 10, "%.0f", 1))
		density.setOctave(densityOctave);
	ImGui::Checkbox("Smooth", &smoothMesh);

	//	if (ImGui::InputFloat("Frequency", &densityFrequency))
//	if (ImGui::InputFloat("Octave", &densityOctave))
//		density.setOctave(densityOctave);

	static const char *textureSizesStrings[] = { "512", "1024", "2048", "4096" };
	static const int textureSizes[] = { 512, 1024, 2048, 4096 };
	static int currentTextureSize = 0;
	if (ImGui::Combo("Texture Size", &currentTextureSize, textureSizesStrings,
			4, -1)) {
		texturemapper.setTextureSize(textureSizes[currentTextureSize]);
	}

	if (meshProgress.isRunning()) {
		if (ImGui::Button("Cancel"))
			meshProgress.cancel();
		ImGui::Text("Task: %s", meshProgress.getTask().c_str());
		ImGui::ProgressBar(meshProgress.getProgress());
	} else {
		if (meshProgress.isFinished() && meshThread.joinable())
			meshThread.join();

		if (ImGui::Button("Generate"))
			meshThread = std::thread(&Application::generateAsteroid, this);
	}
	ImGui::End();

	ImGui::Begin("Rendering");
	if (ImGui::Button("Reload Shader")) {
		loadShader();
		glMesh.update(programId);
	}
	if (ImGui::Button("Wireframe"))
		glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
	if (ImGui::Button("Fill"))
		glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
	ImGui::Text("Application average %.3f ms/frame (%.1f FPS)",
			1000.0f / ImGui::GetIO().Framerate, ImGui::GetIO().Framerate);
	ImGui::End();

	ImGui::Begin("Textures");
	ImGui::Image((ImTextureID) (intptr_t) tAlbedoId,
			ImVec2(ImGui::GetWindowContentRegionMax().x,
					ImGui::GetWindowContentRegionMax().x));
	ImGui::End();
}

void Application::run() {

	while (!glfwWindowShouldClose(window)) {
		glfwPollEvents();
		glfwGetFramebufferSize(window, &width, &height);
		ImGui_ImplGlfwGL3_NewFrame();

		gui();

		glViewport(0, 0, width, height);
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

		if (meshUploadRequest) {
			glMesh.upload(mesh);
			meshUploadRequest = false;
		}
		if (textureUploadRequest) {
			uploadTexture();
			textureUploadRequest = false;
		}
		updateMatrices();
		prepareShader();
		renderMesh();
		skybox.Render(mvp);
		ImGui::Render();
		glfwSwapBuffers(window);

	}
}
