#include <iostream>
#include <memory>
#include <filesystem>
#include <algorithm>
#include <chrono>
#include "glstate.hpp"

#define GLFW_INCLUDE_NONE
#include <GLFW/glfw3.h>

namespace fs = std::filesystem;

std::vector<std::string> meshFilenames;		// Paths to .obj files to load

// OpenGL state
std::unique_ptr<GLState> glState;
GLFWwindow* window = nullptr;

// Initialization functions
void initGLFW();
void findObjFiles();

// Callback functions
void display();
void windowSizeCallback(GLFWwindow* window, int width, int height);
void framebufferSizeCallback(GLFWwindow* window, int width, int height);
//void keyRelease(unsigned char key, int x, int y);
void mouseBtn(GLFWwindow* window, int button, int action, int mods);
void idle();
void cleanup();

// Program entry point
int main(int argc, char** argv) {
	try {
		// Create the window
		initGLFW();
		// Initialize OpenGL (buffers, shaders, etc.)
		glState = std::unique_ptr<GLState>(new GLState());
		glState->initializeGL();

		// Draw the initial frame once
		display();
	} catch (const std::exception& e) {
		// Handle any errors
		std::cerr << "Fatal error: " << e.what() << std::endl;
		cleanup();
		return -1;
	}

	// Execute main loop
	while (!glfwWindowShouldClose(window)) {
		glfwPollEvents();
		idle();
	}

	cleanup();

	return 0;
}

// Setup window and callbacks
void initGLFW() {
	// Initialize GLFW
	if (!glfwInit()) {
		throw std::runtime_error("Failed to initialize GLFW");
	}

	// Request an OpenGL 3.3 Core context
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

	glfwWindowHint(GLFW_SCALE_TO_MONITOR, GLFW_TRUE);

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

	// Create the window
	window = glfwCreateWindow(GLState::width, GLState::height, "GLFW Window", nullptr, nullptr);

	if (!window) {
		glfwTerminate();
		throw std::runtime_error("Failed to create GLFW window");
	}
	// Make this window's OpenGL context current
	glfwMakeContextCurrent(window);

	// Get actual window size.
	// This may differ from the requested size due to DPI scaling.
	int windowWidth, windowHeight;
	glfwGetWindowSize(window, &windowWidth, &windowHeight);

	GLState::width = windowWidth;
	GLState::height = windowHeight;

	// Set viewport using the actual framebuffer size.
	int framebufferWidth, framebufferHeight;
	glfwGetFramebufferSize(
		window,
		&framebufferWidth,
		&framebufferHeight
	);

	glViewport(
		0,
		0,
		framebufferWidth,
		framebufferHeight
	);

	// Register callbacks
	glfwSetWindowSizeCallback(window, windowSizeCallback);
	glfwSetFramebufferSizeCallback(window, framebufferSizeCallback);
	glfwSetMouseButtonCallback(window, mouseBtn);
}

void findObjFiles() {
	// Search the models/ directory for any file ending in .obj
	fs::path modelsDir = "models";
	for (auto& di : fs::directory_iterator(modelsDir)) {
		if (di.is_regular_file() && di.path().extension() == ".obj")
			meshFilenames.push_back(di.path().string());
	}
	std::sort(meshFilenames.begin(), meshFilenames.end());
}

// Called whenever a screen redraw is requested
void display() {
	// Tell the GLState to render the scene
	glState->paintGL();

	// Scene is rendered to the back buffer, so swap the buffers to display it
	glfwSwapBuffers(window);
}

void windowSizeCallback(GLFWwindow* window, int width, int height) {
	GLState::width = width;
	GLState::height = height;
}

void framebufferSizeCallback(GLFWwindow* window, int width, int height) {
	glViewport(0, 0, width, height);
}

// Called when a mouse button is pressed or released
void mouseBtn(GLFWwindow* window, int button, int action, int mods) {
	double x, y;
	// Get the cursor position
	glfwGetCursorPos(window, &x, &y);

	// Release left mouse button
	if (action == GLFW_RELEASE && button == GLFW_MOUSE_BUTTON_LEFT) {
		glState->moveLine(glm::vec2(static_cast<float>(x), static_cast<float>(y)));
	}
	// Release right mouse button
	if (action == GLFW_RELEASE && button == GLFW_MOUSE_BUTTON_RIGHT) {
		glState->moveBall(glm::vec2(static_cast<float>(x), static_cast<float>(y)));
	}
}

static auto start = std::chrono::time_point_cast<std::chrono::milliseconds>(std::chrono::system_clock::now());  // record start time

// Called when there are no events to process
void idle() {
	// NOTE: anything that happens every frame (e.g. movement) should be done here
	// Be sure to call glutPostRedisplay() if the screen needs to update as well
	auto finish = std::chrono::time_point_cast<std::chrono::milliseconds>(std::chrono::system_clock::now());  // record end time
	auto elapsed = static_cast<float>((finish - start).count());

	// ################### TODO6 ###################
	// NOTE: keep refreshing the screen in a constant rate using "elapsed" and "display".
}

// Called when the window is closed or the event loop is otherwise exited
void cleanup() {
	// Delete the GLState object, calling its destructor,
	// which releases the OpenGL objects
	glState.reset(nullptr);
}
