#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 "errordialog.h"
#include "objparser.h"
#include "resource.h"
#include "camera.h"
#include "constants.h"
#include "gameobject.h"
#include "scene.h"
#include "defaults.h"
#include "ui.h"
#include "uielements.h"
#include "physics.h"

#include "internal.h"
#include "internal/ui.h"
#include "internal/input.h"

kek::KekData kek::kekData;

namespace kek::Engine {

static TextElement *fpsText;

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;
}

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", nullptr, nullptr);
	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, nullptr);
		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);

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

	stbi_set_flip_vertically_on_load(true);

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

	FT_Error error = FT_Init_FreeType(&kekData.freetype);
	if(error) {
		ErrorDialog::showError("Failed to initialize FreeType: " + std::string(FT_Error_String(error)));
		glfwTerminate();
		return KEK_ERROR;
	}

	UI::init();
	Input::init();
	Physics::init();
	Defaults::init();

	MemoryBuffer *buf = Resource::loadResource("image/icon.png");
	if(buf) {
		int width, height, nrChannels;
		unsigned char *data = stbi_load_from_memory((stbi_uc *) buf->buffer, buf->length, &width, &height, &nrChannels, 0);
		GLFWimage image;
		image.width = width;
		image.height = height;
		image.pixels = data;
		glfwSetWindowIcon(kekData.window, 1, &image);
		delete buf;
	}

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

	glfwSetCursorPosCallback(kekData.window, Input::onCursorPosCallback);
	Input::setCursorMode(GLFWCursorMode::CAPTURE);
	glfwSetKeyCallback(kekData.window, Input::onKeyCallback);
	glfwSetCharCallback(kekData.window, Input::onKeyCharCallback);
	glfwSetMouseButtonCallback(kekData.window, Input::onMouseButtonCallback);

	fpsText = new TextElement(uiPx(0), uiPx(0));
	UI::addElement(fpsText);

	return KEK_SUCCESS;
}

int start() {
	int prevTime = 0;
	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);

		Physics::step(kekData.lastFrameTime);

		for(auto cb : kekData.input->periodicCallbacks) {
			cb.second(kekData.window);
		}

		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));

		if(kekData.shader->lighting) {
			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) {
				ShaderLight shLight;
				memcpy(shLight.color, glm::value_ptr(light->color), sizeof(shLight.color));
				switch(light->getType()) {
					case LightType::POINT:
					{
						PointLight *l = (PointLight *) light;
						memcpy(&shLight.position, glm::value_ptr(l->getPosition()), sizeof(shLight.position));
						memcpy(&shLight.attenuation, glm::value_ptr(glm::vec3(l->constant, l->linear, l->quadratic)), sizeof(shLight.attenuation));
						numPointLights++;
						break;
					}
					case LightType::DIRECTIONAL:
					{
						DirectionalLight *l = (DirectionalLight *) light;
						memcpy(&shLight.direction, glm::value_ptr(l->direction), sizeof(shLight.direction));
						numDirectionalLights++;
						break;
					}
					case LightType::SPOT:
					{
						SpotLight *l = (SpotLight *) light;
						memcpy(&shLight.position, glm::value_ptr(l->getPosition()), sizeof(shLight.position));
						memcpy(&shLight.direction, glm::value_ptr(l->direction), sizeof(shLight.direction));
						memcpy(&shLight.attenuation, glm::value_ptr(glm::vec3(l->constant, l->linear, l->quadratic)), sizeof(shLight.attenuation));
						memcpy(&shLight.cutoff, glm::value_ptr(glm::vec2(glm::cos(l->innerCutoff), glm::cos(l->outerCutoff))), sizeof(shLight.cutoff));
						numSpotLights++;
						break;
					}
				}
				kekData.shader->lighting->buffer[i] = shLight;
				i++;
			}

			glNamedBufferData(kekData.shader->lighting->ubo, kekData.shader->lighting->blockSize, &kekData.shader->lighting->buffer, GL_DYNAMIC_DRAW);
			glBindBufferBase(GL_UNIFORM_BUFFER, KEK_UNIFORM_LIGHTS_BINDING, kekData.shader->lighting->ubo);

			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);

		glDisable(GL_DEPTH_TEST);
		glEnable(GL_BLEND);
		glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

		glm::mat4 uiProjection = glm::ortho(0.0f, (float) kekData.screenWidth, (float) kekData.screenHeight, 0.0f);

		int time = (int) (glfwGetTime() * 10);
		if(time != prevTime) fpsText->setText("FPS: " + std::to_string((int) floor(1.0f / kekData.lastFrameTime)) + " (" + std::to_string(kekData.lastFrameTime * 1000) + " ms)");
		prevTime = time;

		for(UIElement *uiEl : kekData.ui->elements) {
			UIPoint pos = uiEl->getPosition();
			uiEl->drawAll(pos, uiProjection);
		}

		glDisable(GL_BLEND);
		glEnable(GL_DEPTH_TEST);

		// 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;
		kekData.lastFrameTime = secsTaken.count();
	}

	return KEK_SUCCESS;
}

void exit() {
	FT_Done_FreeType(kekData.freetype);
	Defaults::destroy();
	Physics::destroy();
	Input::destroy();
	UI::destroy();
	Resource::destroy();
}

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

}
