#include "engine.h"

#include <iostream>
#include <cstring>

#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <glm/gtc/type_ptr.hpp>
#include <chrono>

#define STB_IMAGE_IMPLEMENTATION
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include <stb/stb_image.h>
#include <stb/stb_image_write.h>

#include "internal.h"
#include "errordialog.h"
#include "objparser.h"
#include "resource.h"
#include "camera.h"
#include "constants.h"
#include "gameobject.h"
#include "scene.h"

kek::KekData kek::kekData;

namespace kek::Engine {

static void framebufferSizeCallback(GLFWwindow *window, int w, int h) {
	glViewport(0, 0, w, h);
	kekData.screenWidth = w;
	kekData.screenHeight = h;
}

static void glDebugOutput(GLenum source, GLenum type, unsigned int id, GLenum severity, GLsizei length, const char *message, const void *userParam) {
	//if(id == 0x20071) return;
	if(severity == GL_DEBUG_SEVERITY_NOTIFICATION) return;
	std::cout << "OpenGL Debug (" << id << "): " <<  message << std::endl;
}

void onCursorPosCallback(GLFWwindow *window, double x, double y) {
	static bool firstMouse = true;
	static double lastX = 0, lastY = 0;
	if(firstMouse) {
		lastX = x;
		lastY = y;
		firstMouse = false;
	}

	float xoff = lastX - x;
	float yoff = lastY - y;
	lastX = x;
	lastY = y;

	xoff *= 0.1f;
	yoff *= 0.1f;

	kekData.activeCamera->rotateYaw(xoff);
	kekData.activeCamera->rotatePitch(yoff);

	for(std::pair<InputListener, MouseCallback> cb : kekData.mouseCallbacks) {
		cb.second(window, x, y);
	}
}

void onKeyCallback(GLFWwindow *window, int key, int scancode, int action, int mods) {
	for(std::pair<InputListener, KeyCallback> cb : kekData.keyCallbacks) {
		cb.second(window, key, scancode, action, mods);
	}
}

int init() {
	// Init GLFW
	if (glfwInit() != GL_TRUE) {
		const char *errorMsg;
		int code = glfwGetError(&errorMsg);
		if(code != GLFW_NO_ERROR) {
			ErrorDialog::showError("Failed to initialize GLFW: " + std::string(errorMsg) + std::string(" (") + std::to_string(code) + std::string(")"));
		}else {
			ErrorDialog::showError("Failed to initialize GLFW: Unknown error");
		}

		return KEK_ERROR;
	}

	std::cout << "Initialized GLFW" << std::endl;

	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 5);
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
	glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
	glfwWindowHint(GLFW_SAMPLES, 4);
	glfwWindowHint(GLFW_MAXIMIZED, GL_TRUE);
	glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GL_TRUE);

	kekData.screenWidth = 800.0f;
	kekData.screenHeight = 600.0f;
	kekData.window = glfwCreateWindow(kekData.screenWidth, kekData.screenHeight, "KekEngine", NULL, NULL);
	if(!kekData.window) {
		const char *errorMsg;
		int code = glfwGetError(&errorMsg);
		if(code != GLFW_NO_ERROR) {
			ErrorDialog::showError("Failed to create window: " + std::string(errorMsg) + " (" + std::to_string(code) + ")");
		}else {
			ErrorDialog::showError("Failed to create window: Unknown error");
		}

		glfwTerminate();
		return KEK_ERROR;
	}

	glfwMakeContextCurrent(kekData.window);

	std::cout << "Initialized window" << std::endl;

	// Init GLEW
	glewExperimental = GL_TRUE;
	if (glewInit() != GLEW_OK) {
		ErrorDialog::showError("Failed to initialize GLEW");
		glfwTerminate();
		return KEK_ERROR;
	}

	std::cout << "Initialized GLEW" << std::endl;

	std::cout << "OpenGL version: " << glGetString(GL_VERSION) << std::endl;

	int flags;
	glGetIntegerv(GL_CONTEXT_FLAGS, &flags);
	if (flags & GL_CONTEXT_FLAG_DEBUG_BIT) {
		glEnable(GL_DEBUG_OUTPUT);
		glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
		glDebugMessageCallback(glDebugOutput, NULL);
		glDebugMessageControl(GL_DONT_CARE, GL_DONT_CARE, GL_DONT_CARE, 0, nullptr, GL_TRUE);
	}

	glViewport(0, 0, kekData.screenWidth, kekData.screenHeight);

	glfwSetFramebufferSizeCallback(kekData.window, framebufferSizeCallback);

	glEnable(GL_DEPTH_TEST);
	glEnable(GL_MULTISAMPLE);
	glEnable(GL_CULL_FACE);
	glCullFace(GL_BACK);

	if(Resource::init() != KEK_SUCCESS) {
		glfwTerminate();
		return KEK_ERROR;
	}

	glfwSetCursorPosCallback(kekData.window, onCursorPosCallback);
	glfwSetInputMode(kekData.window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);

	glClearColor(0.1f, 0.3f, 0.1f, 0.0f);
	// glfwSwapInterval(0);

	stbi_set_flip_vertically_on_load(true);

	kekData.activeCamera = new Camera();
	kekData.shader = new Shader("shader/mesh/vertex.glsl", "shader/mesh/fragment.glsl");

	return KEK_SUCCESS;
}

int start() {
	while(!glfwWindowShouldClose(kekData.window)) {
		auto start = std::chrono::high_resolution_clock::now();

		// Clear the screen
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
		glViewport(0, 0, kekData.screenWidth, kekData.screenHeight);

		for(std::pair<InputListener, PeriodicCallback> cb : kekData.periodicCallbacks) {
			cb.second(kekData.window);
		}

		if(glfwGetKey(kekData.window, GLFW_KEY_W) == GLFW_PRESS) {
			kekData.activeCamera->translate(kekData.activeCamera->direction * 0.1f);
		}

		if(glfwGetKey(kekData.window, GLFW_KEY_S) == GLFW_PRESS) {
			kekData.activeCamera->translate(kekData.activeCamera->direction * -0.1f);
		}

		if(glfwGetKey(kekData.window, GLFW_KEY_A) == GLFW_PRESS) {
			glm::vec3 camRight = glm::normalize(glm::cross(kekData.activeCamera->direction, glm::vec3(0.0f, 1.0f, 0.0f)));
			kekData.activeCamera->translate(-camRight * 0.1f);
		}

		if(glfwGetKey(kekData.window, GLFW_KEY_D) == GLFW_PRESS) {
			glm::vec3 camRight = glm::normalize(glm::cross(kekData.activeCamera->direction, glm::vec3(0.0f, 1.0f, 0.0f)));
			kekData.activeCamera->translate(camRight * 0.1f);
		}

		if(glfwGetKey(kekData.window, GLFW_KEY_SPACE) == GLFW_PRESS) {
			kekData.activeCamera->translateY(0.1f);
		}

		if(glfwGetKey(kekData.window, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS) {
			kekData.activeCamera->translateY(-0.1f);
		}

		if(glfwGetKey(kekData.window, GLFW_KEY_ESCAPE) == GLFW_PRESS) {
			break;
		}

		kekData.shader->use();

		glm::mat4 view = kekData.activeCamera->transformationMatrix();

		glm::mat4 projection;
		projection = glm::perspective(glm::radians(90.0f), kekData.screenWidth / (float) kekData.screenHeight, KEK_CAMERA_NEAR, KEK_CAMERA_FAR);

		glm::vec3 position = kekData.activeCamera->getPosition();

		glUniformMatrix4fv(glGetUniformLocation(kekData.shader->id, "view"), 1, GL_FALSE, glm::value_ptr(view));
		glUniformMatrix4fv(glGetUniformLocation(kekData.shader->id, "projection"), 1, GL_FALSE, glm::value_ptr(projection));
		glUniform3fv(glGetUniformLocation(kekData.shader->id, "cameraPos"), 1, glm::value_ptr(position));

		LightList *lights = kekData.activeScene->lights;

		std::vector<Light *> shaderLights; // TODO: Maybe don't compute on every frame

		for(PointLight *light : lights->point) {
			if(glm::length2(glm::abs(light->getPosition() - kekData.activeCamera->getPosition())) < KEK_LIGHT_MAX_DISTANCE_SQUARED) {
				shaderLights.push_back(light);
			}
		}

		for(DirectionalLight *light : lights->directional) {
			shaderLights.push_back(light);
		}

		for(SpotLight *light : lights->spot) {
			if(glm::length2(glm::abs(light->getPosition() - kekData.activeCamera->getPosition())) < KEK_LIGHT_MAX_DISTANCE_SQUARED) {
				shaderLights.push_back(light);
			}
		}

		unsigned int i = 0;
		int numPointLights = 0, numDirectionalLights = 0, numSpotLights = 0;

		for(Light *light : shaderLights) {
			std::string prefix = "lights[" + std::to_string(i) + "].";

			switch(light->getType()) {
				case LightType::POINT:
				{
					PointLight *l = (PointLight *) light;
					glUniform3fv(glGetUniformLocation(kekData.shader->id, (prefix + "color").c_str()), 1, glm::value_ptr(l->color));
					glUniform3fv(glGetUniformLocation(kekData.shader->id, (prefix + "position").c_str()), 1, glm::value_ptr(l->getPosition()));
					glUniform3fv(glGetUniformLocation(kekData.shader->id, (prefix + "attenuation").c_str()), 1, glm::value_ptr(glm::vec3(l->constant, l->linear, l->quadratic)));
					numPointLights++;
					break;
				}
				case LightType::DIRECTIONAL:
				{
					DirectionalLight *l = (DirectionalLight *) light;
					glUniform3fv(glGetUniformLocation(kekData.shader->id, (prefix + "color").c_str()), 1, glm::value_ptr(l->color));
					glUniform3fv(glGetUniformLocation(kekData.shader->id, (prefix + "direction").c_str()), 1, glm::value_ptr(l->direction));
					numDirectionalLights++;
					break;
				}
				case LightType::SPOT:
				{
					SpotLight *l = (SpotLight *) light;
					glUniform3fv(glGetUniformLocation(kekData.shader->id, (prefix + "color").c_str()), 1, glm::value_ptr(l->color));
					glUniform3fv(glGetUniformLocation(kekData.shader->id, (prefix + "position").c_str()), 1, glm::value_ptr(l->getPosition()));
					glUniform3fv(glGetUniformLocation(kekData.shader->id, (prefix + "direction").c_str()), 1, glm::value_ptr(l->direction));
					glUniform3fv(glGetUniformLocation(kekData.shader->id, (prefix + "attenuation").c_str()), 1, glm::value_ptr(glm::vec3(l->constant, l->linear, l->quadratic)));
					glUniform2fv(glGetUniformLocation(kekData.shader->id, (prefix + "cutoff").c_str()), 1, glm::value_ptr(glm::vec2(glm::cos(l->innerCutoff), glm::cos(l->outerCutoff))));
					numSpotLights++;
					break;
				}
			}

			i++;
		}

		glUniform1i(glGetUniformLocation(kekData.shader->id, "numPointLights"), numPointLights);
		glUniform1i(glGetUniformLocation(kekData.shader->id, "numDirectionalLights"), numDirectionalLights);
		glUniform1i(glGetUniformLocation(kekData.shader->id, "numSpotLights"), numSpotLights);

		if(kekData.activeScene) kekData.activeScene->draw(kekData.shader);

		// Swap buffers and poll window events
		glfwSwapBuffers(kekData.window);
		glfwPollEvents();

		auto end = std::chrono::high_resolution_clock::now();
		std::chrono::duration<float> secsTaken = end - start;
		// std::cout << "FT: " << secsTaken.count() * 1000 << std::endl;
		// std::cout << "FR: " << (1.0f / secsTaken.count()) << std::endl;
	}

	return KEK_SUCCESS;
}

void setActiveScene(Scene *scene) {
	kekData.activeScene = scene;
}

}
