#include <iostream>
#include <filesystem>

#include <glad/glad.h> 
#include <GLFW/glfw3.h>

#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>

#include <imgui/imgui.h>
#include <imgui/imgui_impl_glfw.h>
#include <imgui/imgui_impl_opengl3.h>

#include <myEngine/shader.hpp>
#include <myEngine/camera.hpp>
#include <myEngine/model.hpp>
#include <myEngine/object.hpp>

void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow *window);
void keyboard_callback(GLFWwindow* window, int key, int scancode, int action, int mods);
void mouse_pos_callback(GLFWwindow* window, double xpos, double ypos);
void mouse_button_callback(GLFWwindow* window, int button, int action, int mods);
void mouse_scroll_callback(GLFWwindow* window, double xoffset, double yoffset);

unsigned int width = 800;
unsigned int height = 600;

Camera camera(glm::vec3(0.0f, 0.0f, 3.0f));
float lastX = width / 2.0f;
float lastY = height / 2.0f;
bool firstMouse = true;
bool mouseCaptured = false;

float deltaTime = 0.0f;
float lastFrame = 0.0f;

int main()
{
	glfwInit();
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

#ifdef __APPLE__
	glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif

	GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
	if (window == NULL)
	{
		std::cout << "Failed to create GLFW window" << std::endl;
		glfwTerminate();
		return -1;
	}
	glfwMakeContextCurrent(window);
	glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_NORMAL);  
	glfwSetKeyCallback(window, keyboard_callback);
	glfwSetCursorPosCallback(window, mouse_pos_callback);
	glfwSetMouseButtonCallback(window, mouse_button_callback);
	glfwSetScrollCallback(window, mouse_scroll_callback);

	if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
	{
		std::cout << "Failed to initialize GLAD" << std::endl;
		return -1;
	}

	glViewport(0, 0, width, height);

	glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

	glEnable(GL_DEPTH_TEST);

	//stbi_set_flip_vertically_on_load(true);

	// Setup Dear ImGui context
	IMGUI_CHECKVERSION();
	ImGui::CreateContext();
	ImGuiIO& io = ImGui::GetIO(); (void)io;
	io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange;
	ImGui::StyleColorsDark();
	ImGui_ImplGlfw_InitForOpenGL(window, true);
	ImGui_ImplOpenGL3_Init("#version 330 core");

	// Shaders
	Shader ourShader("resources/shaders/vert.glsl", "resources/shaders/frag.glsl");
	Shader directional_lightShader("resources/shaders/vert.glsl", "resources/shaders/directional_light.glsl");
	Shader point_lightShader("resources/shaders/vert.glsl", "resources/shaders/point_light.glsl");
	Shader light_cubeShader("resources/shaders/vert.glsl", "resources/shaders/light_object.glsl");

	// Models
	Model croissant("resources/models/croissant/source/croissant.obj");
	Model cube("resources/models/cube/cube.obj");

	// Objects
	Object object1(croissant, glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.1f, 0.1f, 0.1f), glm::vec3(0.0f, 0.0f, 0.0f));
	Object object2(croissant, glm::vec3(1.0f, 0.0f, 0.0f), glm::vec3(0.1f, 0.1f, 0.1f), glm::vec3(0.0f, 0.0f, 0.0f));
	Object object3(croissant, glm::vec3(-1.0f, 0.0f, 0.0f), glm::vec3(0.1f, 0.1f, 0.1f), glm::vec3(0.0f, 0.0f, 0.0f));
	Object light_cube(cube, glm::vec3(1.2f, 1.0f, 2.0f), glm::vec3(0.05f, 0.05f, 0.05f), glm::vec3(0.0f, 0.0f, 0.0f));

	static float positionX = 0.0f;
	static float positionY = 0.0f;
	static float positionZ = 0.0f;

	static float lightCubePositionX = 1.2f;
	static float lightCubePositionY = 1.0f;
	static float lightCubePositionZ = 2.0f;
	static float* light_color = new float[3]{1.0f, 1.0f, 1.0f};

	while(!glfwWindowShouldClose(window))
	{
		float currentFrame = glfwGetTime();
		deltaTime = currentFrame - lastFrame;
		lastFrame = currentFrame;

		glfwPollEvents();
		processInput(window);

		if (mouseCaptured)
			io.AddMousePosEvent(-FLT_MAX, -FLT_MAX);

		glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

		ImGui_ImplOpenGL3_NewFrame();
		ImGui_ImplGlfw_NewFrame();
		ImGui::NewFrame();
		
		double mouseX, mouseY;
		glfwGetCursorPos(window, &mouseX, &mouseY);
		
		ImGui::Begin("Settings");
		ImGui::Text("FPS: %.2f", 1.0f / deltaTime);
		ImGui::SliderFloat("Object 1 Position X", &positionX, -5.0f, 5.0f);
		ImGui::SliderFloat("Object 1 Position Y", &positionY, -5.0f, 5.0f);
		ImGui::SliderFloat("Object 1 Position Z", &positionZ, -5.0f, 5.0f);
		ImGui::SliderFloat("Light Cube Position X", &lightCubePositionX, -5.0f, 5.0f);
		ImGui::SliderFloat("Light Cube Position Y", &lightCubePositionY, -5.0f, 5.0f);
		ImGui::SliderFloat("Light Cube Position Z", &lightCubePositionZ, -5.0f, 5.0f);
		ImGui::ColorEdit3("Light Cube Color", light_color);
		ImGui::End();

		glm::mat4 view = camera.GetViewMatrix();
		glm::mat4 projection = glm::perspective(glm::radians(45.0f), float(width) / float(height), 0.1f, 100.0f);

		// Bacis lighting
		ourShader.setVec3("viewPos",        camera.Position);
		ourShader.setVec3("light.position", 1.2f, 1.0f, 2.0f);
		ourShader.setVec3("light.color",    1.0f, 1.0f, 1.0f);
		ourShader.setVec3("light.ambient",  0.2f, 0.2f, 0.2f);
		ourShader.setVec3("light.diffuse",  0.5f, 0.5f, 0.5f);
		ourShader.setVec3("light.specular", 1.0f, 1.0f, 1.0f);
		ourShader.setMat4("view",           view);
		ourShader.setMat4("projection",     projection);

		// Directional light
		directional_lightShader.setVec3("light.direction", -0.2f, -1.0f, -0.3f);
		directional_lightShader.setVec3("light.color",    1.0f, 1.0f, 1.0f);
		directional_lightShader.setVec3("light.ambient",  0.2f, 0.2f, 0.2f);
		directional_lightShader.setVec3("light.diffuse",  0.5f, 0.5f, 0.5f);
		directional_lightShader.setVec3("light.specular", 1.0f, 1.0f, 1.0f);
		directional_lightShader.setMat4("view",           view);
		directional_lightShader.setMat4("projection",     projection);

		// Point light
		point_lightShader.use();
		point_lightShader.setVec3("viewPos",          camera.Position);
		point_lightShader.setVec3("light.position",   lightCubePositionX, lightCubePositionY, lightCubePositionZ);
		point_lightShader.setVec3("light.color",      light_color[0], light_color[1], light_color[2]);
		point_lightShader.setVec3("light.ambient",    0.2f, 0.2f, 0.2f);
		point_lightShader.setVec3("light.diffuse",    0.5f, 0.5f, 0.5f);
		point_lightShader.setVec3("light.specular",   1.0f, 1.0f, 1.0f);
		point_lightShader.setFloat("light.constant",  1.0f);
		point_lightShader.setFloat("light.linear",    0.09f);
		point_lightShader.setFloat("light.quadratic", 0.032f);
		point_lightShader.setMat4("view",             view);
		point_lightShader.setMat4("projection",       projection);

		object1.setPosition(glm::vec3(positionX, positionY, positionZ));
		object1.Draw(point_lightShader, deltaTime);
		object2.setAcceleration(glm::vec3(0.0f, 0.001f, 0.0f));
		object2.Draw(point_lightShader, deltaTime);
		object3.setAcceleration(glm::vec3(0.0f, -0.001f, 0.0f));
		object3.Draw(point_lightShader, deltaTime);

		// Light cube shader
		light_cubeShader.use();
		light_cubeShader.setMat4("view",       view);
		light_cubeShader.setMat4("projection", projection);
		light_cubeShader.setVec3("lightColor", light_color[0], light_color[1], light_color[2]);

		light_cube.setPosition(glm::vec3(lightCubePositionX, lightCubePositionY, lightCubePositionZ));
		light_cube.Draw(light_cubeShader, deltaTime);

		ImGui::Render();
		ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());

		glfwSwapBuffers(window);
	}

	ImGui_ImplOpenGL3_Shutdown();
	ImGui_ImplGlfw_Shutdown();
	ImGui::DestroyContext();

	glfwTerminate();

	return 0;
}

void framebuffer_size_callback(GLFWwindow* window, int new_width, int new_height)
{
	width = new_width;
	height = new_height;
	glViewport(0, 0, new_width, new_height);
}

void processInput(GLFWwindow *window)
{
	if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS && mouseCaptured)
	{
		glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
		mouseCaptured = false;
	}

	if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS && mouseCaptured)
		camera.ProcessKeyboard(FORWARD, deltaTime);
	if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS && mouseCaptured)
		camera.ProcessKeyboard(BACKWARD, deltaTime);
	if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS && mouseCaptured)
		camera.ProcessKeyboard(LEFT, deltaTime);
	if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS && mouseCaptured)
		camera.ProcessKeyboard(RIGHT, deltaTime);
	
	if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS && !mouseCaptured && !ImGui::GetIO().WantCaptureMouse)
	{
		glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
		mouseCaptured = true;
	}
}

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

void mouse_pos_callback(GLFWwindow* window, double xposIn, double yposIn)
{
	float xpos = static_cast<float>(xposIn);
	float ypos = static_cast<float>(yposIn);

	if (firstMouse)
	{
		lastX = xpos;
		lastY = ypos;
		firstMouse = false;
	}

	float xoffset = xpos - lastX;
	float yoffset = lastY - ypos; // reversed since y-coordinates go from bottom to top

	lastX = xpos;
	lastY = ypos;

	if (mouseCaptured)
		camera.ProcessMouseMovement(xoffset, yoffset);
}

void mouse_button_callback(GLFWwindow* window, int button, int action, int mods) {}
void mouse_scroll_callback(GLFWwindow* window, double xoffset, double yoffset) {}
