
#include <iomanip>
#include <iostream>

#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 "stb/stb_easy_font.h"

#include <engine/mesh.h>
#include <engine/shader.h>
#include <engine/texture.h>
#include <engine/camera.h>

void window_resize_callback(GLFWwindow* window, int width, int height);
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);
void processInput(GLFWwindow *window);

const int SCREEN_WIDTH = 1920;
const int SCREEN_HEIGHT = 1080;

static bool drawWireframe = false;
static bool wireframeHeld = false;

Camera camera(glm::vec3(0.0f, 0.0f, 3.0f));
float lastX = SCREEN_WIDTH / 2.0f;
float lastY = SCREEN_HEIGHT / 2.0f;
bool firstMouse = true;


float deltaTime = 0.0f;	// time between current frame and last frame
float lastFrame = 0.0f;

int main(int argc, char* argv[])
{
    // Initialize GLFW
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    // Create GLFW Window
    GLFWwindow* window = glfwCreateWindow(SCREEN_WIDTH, SCREEN_HEIGHT, "Renderer", NULL, NULL);
    if (window == NULL)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);

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

    // Initialize Viewport & setup resize callback
    glViewport(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
    glfwSetFramebufferSizeCallback(window, window_resize_callback);
    glfwSetCursorPosCallback(window, mouse_callback);
    glfwSetScrollCallback(window, scroll_callback);

    // Hide & Lock Cursor
    glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
    glfwSetInputMode(window, GLFW_RAW_MOUSE_MOTION, GLFW_TRUE);

    /*
     Testing
    */

    // Load Shader
    std::vector<Vertex> vertices = {
        // Front face (-z) – normal: (0, 0, -1)
        { -0.5f, -0.5f, -0.5f,   0.0f, 0.0f, -1.0f,   0.0f, 0.0f },
        {  0.5f, -0.5f, -0.5f,   0.0f, 0.0f, -1.0f,   1.0f, 0.0f },
        {  0.5f,  0.5f, -0.5f,   0.0f, 0.0f, -1.0f,   1.0f, 1.0f },
        {  0.5f,  0.5f, -0.5f,   0.0f, 0.0f, -1.0f,   1.0f, 1.0f },
        { -0.5f,  0.5f, -0.5f,   0.0f, 0.0f, -1.0f,   0.0f, 1.0f },
        { -0.5f, -0.5f, -0.5f,   0.0f, 0.0f, -1.0f,   0.0f, 0.0f },

        // Back face (+z) – normal: (0, 0, +1)
        { -0.5f, -0.5f,  0.5f,   0.0f, 0.0f,  1.0f,   0.0f, 0.0f },
        {  0.5f, -0.5f,  0.5f,   0.0f, 0.0f,  1.0f,   1.0f, 0.0f },
        {  0.5f,  0.5f,  0.5f,   0.0f, 0.0f,  1.0f,   1.0f, 1.0f },
        {  0.5f,  0.5f,  0.5f,   0.0f, 0.0f,  1.0f,   1.0f, 1.0f },
        { -0.5f,  0.5f,  0.5f,   0.0f, 0.0f,  1.0f,   0.0f, 1.0f },
        { -0.5f, -0.5f,  0.5f,   0.0f, 0.0f,  1.0f,   0.0f, 0.0f },

        // Left face (-x) – normal: (-1, 0, 0)
        { -0.5f,  0.5f,  0.5f,  -1.0f, 0.0f,  0.0f,   1.0f, 0.0f },
        { -0.5f,  0.5f, -0.5f,  -1.0f, 0.0f,  0.0f,   1.0f, 1.0f },
        { -0.5f, -0.5f, -0.5f,  -1.0f, 0.0f,  0.0f,   0.0f, 1.0f },
        { -0.5f, -0.5f, -0.5f,  -1.0f, 0.0f,  0.0f,   0.0f, 1.0f },
        { -0.5f, -0.5f,  0.5f,  -1.0f, 0.0f,  0.0f,   0.0f, 0.0f },
        { -0.5f,  0.5f,  0.5f,  -1.0f, 0.0f,  0.0f,   1.0f, 0.0f },

        // Right face (+x) – normal: (+1, 0, 0)
        {  0.5f,  0.5f,  0.5f,   1.0f, 0.0f,  0.0f,   1.0f, 0.0f },
        {  0.5f,  0.5f, -0.5f,   1.0f, 0.0f,  0.0f,   1.0f, 1.0f },
        {  0.5f, -0.5f, -0.5f,   1.0f, 0.0f,  0.0f,   0.0f, 1.0f },
        {  0.5f, -0.5f, -0.5f,   1.0f, 0.0f,  0.0f,   0.0f, 1.0f },
        {  0.5f, -0.5f,  0.5f,   1.0f, 0.0f,  0.0f,   0.0f, 0.0f },
        {  0.5f,  0.5f,  0.5f,   1.0f, 0.0f,  0.0f,   1.0f, 0.0f },

        // Bottom face (-y) – normal: (0, -1, 0)
        { -0.5f, -0.5f, -0.5f,   0.0f, -1.0f,  0.0f,   0.0f, 1.0f },
        {  0.5f, -0.5f, -0.5f,   0.0f, -1.0f,  0.0f,   1.0f, 1.0f },
        {  0.5f, -0.5f,  0.5f,   0.0f, -1.0f,  0.0f,   1.0f, 0.0f },
        {  0.5f, -0.5f,  0.5f,   0.0f, -1.0f,  0.0f,   1.0f, 0.0f },
        { -0.5f, -0.5f,  0.5f,   0.0f, -1.0f,  0.0f,   0.0f, 0.0f },
        { -0.5f, -0.5f, -0.5f,   0.0f, -1.0f,  0.0f,   0.0f, 1.0f },

        // Top face (+y) – normal: (0, +1, 0)
        { -0.5f,  0.5f, -0.5f,   0.0f,  1.0f,  0.0f,   0.0f, 1.0f },
        {  0.5f,  0.5f, -0.5f,   0.0f,  1.0f,  0.0f,   1.0f, 1.0f },
        {  0.5f,  0.5f,  0.5f,   0.0f,  1.0f,  0.0f,   1.0f, 0.0f },
        {  0.5f,  0.5f,  0.5f,   0.0f,  1.0f,  0.0f,   1.0f, 0.0f },
        { -0.5f,  0.5f,  0.5f,   0.0f,  1.0f,  0.0f,   0.0f, 0.0f },
        { -0.5f,  0.5f, -0.5f,   0.0f,  1.0f,  0.0f,   0.0f, 1.0f }
    };
    std::vector<unsigned int> indices = {
        0, 1, 2, 2, 4, 0,
        6, 7, 8, 8, 10, 6,
        12, 13, 14, 14, 16, 12,
        18, 19, 20, 20, 22, 18,
        24, 25, 26, 26, 28, 24,
        30, 31, 32, 32, 34, 30
    };

    Mesh testMesh(vertices, indices);

    Texture texture1("assets/wall.jpg");
    Texture texture2("assets/awesomeface.png", true);

    Shader shaderTest("assets/shaders/basicVertex.vert", "assets/shaders/basicFragment.frag");
    shaderTest.Use(); // don't forget to activate/use the shader before setting uniforms!
    shaderTest.setInt("ourTexture", 0);
    shaderTest.setInt("decal", 1);

    Shader lightingShader("assets/shaders/basicVertex.vert", "assets/shaders/lightFragment.frag");
    Mesh lightMesh(vertices, indices);

    // Render Loop
    float lastUpdate = 0;
    glm::vec3 lightPos = glm::vec3(0.0f, 0.0f, 0.0f);
    glm::vec3 orbitCenter = glm::vec3(0.0f, 0.0f, 0.0f);
    while (!glfwWindowShouldClose(window))
    {
        // per-frame time logic
        // --------------------
        float currentFrame = static_cast<float>(glfwGetTime());
        deltaTime = currentFrame - lastFrame;
        lastFrame = currentFrame;

        // crappy fps printout
        if (currentFrame - lastUpdate >= 1.0) {
            std::cout << 1.0f / deltaTime << std::endl;
            lastUpdate += 1.0f;
        }

        // Input processing
        processInput(window);

        // Rendering
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        glEnable(GL_DEPTH_TEST);

        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, texture1.ID);
        glActiveTexture(GL_TEXTURE1);
        glBindTexture(GL_TEXTURE_2D, texture2.ID);

        shaderTest.Use();
        shaderTest.setVec3("lightColor",  1.0f, 1.0f, 1.0f);
        shaderTest.setVec3("lightPos",  orbitCenter);
        shaderTest.setVec3("viewPos", camera.position);

        // pass projection matrix to shader (note that in this case it could change every frame)
        glm::mat4 projection = glm::perspective(glm::radians(camera.zoom), (float)SCREEN_WIDTH / (float)SCREEN_HEIGHT, 0.1f, 100.0f);
        shaderTest.setMat4("projection", projection);

        // camera/view transformation
        glm::mat4 view = camera.GetViewMatrix();
        shaderTest.setMat4("view", view);

        glm::mat4 model = glm::mat4(1.0f);
        shaderTest.setMat4("model", model);

        testMesh.Draw();


        lightingShader.Use();
        lightingShader.setMat4("projection", projection);
        lightingShader.setMat4("view", view);
        model = glm::mat4(1.0f);

        glm::vec3 offset = glm::vec3(
            2.0f * cos(glfwGetTime() * 1.0f),
            0.0f,
            sin(glfwGetTime() * 1.0f) * 2.0f
        );
        orbitCenter = lightPos + offset;
        model = glm::translate(model, orbitCenter);

        model = glm::scale(model, glm::vec3(0.2f)); // a smaller cube
        lightingShader.setMat4("model", model);

        lightMesh.Draw();

        // Call events & swap buffers
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    // Clean up memory
    glfwTerminate();
    return 0;
}


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

void processInput(GLFWwindow* window)
{
    // Close on Escape
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);

    // Wireframe toggle
    if (glfwGetKey(window, GLFW_KEY_F1) == GLFW_PRESS && !wireframeHeld)
    {
        wireframeHeld = true;
        drawWireframe = !drawWireframe;
        if (drawWireframe)
        {
            glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
        }
        else
        {
            glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
        }
    }
    if (glfwGetKey(window, GLFW_KEY_F1) == GLFW_RELEASE)
        wireframeHeld = false;

    // Camera
    if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
        camera.processKeyboard(FORWARD, deltaTime);
    if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
        camera.processKeyboard(BACKWARD, deltaTime);
    if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
        camera.processKeyboard(LEFT, deltaTime);
    if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
        camera.processKeyboard(RIGHT, deltaTime);
}


// glfw: whenever the mouse moves, this callback is called
// -------------------------------------------------------
void mouse_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;

    camera.processMouseMovement(xoffset, yoffset);
}

// glfw: whenever the mouse scroll wheel scrolls, this callback is called
// ----------------------------------------------------------------------
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
    camera.processMouseScroll(static_cast<float>(yoffset));
}