#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 <iostream>
#include <vector> // NEW: Dynamic arrays
#include <cmath>  // NEW: Sine and Cosine math

// --- 1. INPUT HANDLING ---
void processInput(GLFWwindow *window, glm::vec3 &position, float deltaTime) {
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) {
        glfwSetWindowShouldClose(window, true);
    }

    float velocity = 1.0f * deltaTime;

    // X and Y Movement
    if (glfwGetKey(window, GLFW_KEY_E) == GLFW_PRESS) position.y += velocity; // Up
    if (glfwGetKey(window, GLFW_KEY_Q) == GLFW_PRESS) position.y -= velocity; // Down
    if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS) position.x -= velocity; // Left
    if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS) position.x += velocity; // Right

    // NEW: Z Movement
    if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS) position.z -= velocity; // Away (Deeper into screen)
    if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS) position.z += velocity; // Closer (Towards you)
}

// --- 2. SHADER PIPELINE ---
unsigned int compileShaders() {
    const char *vertexShaderSource = "#version 330 core\n"
    "layout (location = 0) in vec3 aPos;\n"
    "layout (location = 1) in vec3 aColor;\n"
    "out vec3 vertexColor;\n"
    "uniform mat4 model;\n"       // NEW: Object's position/rotation
    "uniform mat4 view;\n"        // NEW: Camera's position
    "uniform mat4 projection;\n"  // NEW: Perspective lens
    "void main()\n"
    "{\n"
    "   // The MVP Multiplication. Reads Right-to-Left.\n"
    "   gl_Position = projection * view * model * vec4(aPos, 1.0);\n"
    "   vertexColor = aColor;\n"
    "}\0";

    const char *fragmentShaderSource = "#version 330 core\n"
    "out vec4 FragColor;\n"
    "in vec3 vertexColor;\n"
    "void main()\n"
    "{\n"
    "   FragColor = vec4(vertexColor, 1.0f);\n"
    "}\n\0";

    unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
    glCompileShader(vertexShader);

    unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
    glCompileShader(fragmentShader);

    unsigned int shaderProgram = glCreateProgram();
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    glLinkProgram(shaderProgram);

    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);

    return shaderProgram;
}

/*
// --- 3. GEOMETRY: SQUARE ---
unsigned int createSquare() {
    float vertices[] = {
        0.5f,  0.5f, 0.0f,   1.0f, 0.0f, 0.0f,
        0.5f, -0.5f, 0.0f,   0.0f, 1.0f, 0.0f,
        -0.5f, -0.5f, 0.0f,   0.0f, 0.0f, 1.0f,
        -0.5f,  0.5f, 0.0f,   1.0f, 1.0f, 0.0f
    };

    unsigned int indices[] = {
        0, 1, 3,
        1, 2, 3
    };

    unsigned int VBO, VAO, EBO;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    glGenBuffers(1, &EBO);

    glBindVertexArray(VAO);

    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);

    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(1);

    glBindVertexArray(0);
    return VAO;
}
*/

// --- 3. GEOMETRY: DYNAMIC POLYGON ---
unsigned int createPolygon(int sides, float radius, int &outIndexCount) {
    std::vector<float> vertices;
    std::vector<unsigned int> indices;
    // 2* arccos(-1) = pi
    float angleStep = (2.0f * acos(-1.0f)) / sides;

    // 1. Create the Center Vertex (Index 0)
    // Position (0,0,0)
    vertices.push_back(0.0f); vertices.push_back(0.0f); vertices.push_back(0.0f);
    // Color (Solid White in the center)
    vertices.push_back(1.0f); vertices.push_back(1.0f); vertices.push_back(1.0f);

    // 2. Calculate the Outer Vertices using Trigonometry
    for (int i = 0; i < sides; i++) {
        float angle = i * angleStep;

        // X, Y, Z Position
        vertices.push_back(radius * cos(angle));
        vertices.push_back(radius * sin(angle));
        vertices.push_back(0.0f);

        // R, G, B Colors (Using math to create a gradient edge)
        vertices.push_back(abs(cos(angle))); // Red
        vertices.push_back(abs(sin(angle))); // Green
        vertices.push_back(0.8f);            // Blue constant
    }

    // 3. Connect the dots with Indices (Triangle Fan pattern)
    for (int i = 1; i <= sides; i++) {
        indices.push_back(0); // Always start at the center
        indices.push_back(i); // Current outer vertex

        if (i < sides) {
            indices.push_back(i + 1); // Next outer vertex
        } else {
            indices.push_back(1); // The last vertex connects back to vertex 1 to close the shape
        }
    }

    outIndexCount = indices.size();

    // 4. Send to OpenGL
    unsigned int VBO, VAO, EBO;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    glGenBuffers(1, &EBO);

    glBindVertexArray(VAO);

    // Notice we use .size() * sizeof(float) and .data() to read the vector memory
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(float), vertices.data(), GL_STATIC_DRAW);

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int), indices.data(), GL_STATIC_DRAW);

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);

    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(1);

    glBindVertexArray(0);
    return VAO;
}

// --- 4. GEOMETRY: 3D AXES ---
unsigned int createAxes() {
    // I changed the axes to the 3D industry standard colors:
    // X = Red, Y = Green, Z = Blue. This will help you orient yourself.
    float axesVertices[] = {
        // Positions            // Colors (R, G, B)
        -1.0f,  0.0f,  0.0f,    1.0f, 0.0f, 0.0f, // X Axis (Red)
        1.0f,  0.0f,  0.0f,    1.0f, 0.0f, 0.0f,

        0.0f, -1.0f,  0.0f,    0.0f, 1.0f, 0.0f, // Y Axis (Green)
        0.0f,  1.0f,  0.0f,    0.0f, 1.0f, 0.0f,

        0.0f,  0.0f, -1.0f,    0.0f, 0.0f, 1.0f, // Z Axis (Blue - into screen)
        0.0f,  0.0f,  1.0f,    0.0f, 0.0f, 1.0f  // Z Axis (Blue - towards you)
    };

    unsigned int axesVAO, axesVBO;
    glGenVertexArrays(1, &axesVAO);
    glGenBuffers(1, &axesVBO);

    glBindVertexArray(axesVAO);

    glBindBuffer(GL_ARRAY_BUFFER, axesVBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(axesVertices), axesVertices, GL_STATIC_DRAW);

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);

    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(1);

    glBindVertexArray(0);
    return axesVAO;
}

// --- 5. SYSTEM INITIALIZATION ---
GLFWwindow* initSystem() {
    if (!glfwInit()) return nullptr;

    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    GLFWwindow* window = glfwCreateWindow(800, 600, "Bokken Engine", nullptr, nullptr);
    if (!window) {
        glfwTerminate();
        return nullptr;
    }
    glfwMakeContextCurrent(window);

    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) return nullptr;

    return window;
}

// --- 6. CORE ENGINE LOOP ---
int main() {
    GLFWwindow* window = initSystem();
    if (!window) return -1;

    // NEW: Enable Depth Testing (Z-Buffer).
    // This stops things drawn last from overriding things that are physically closer.
    glEnable(GL_DEPTH_TEST);

    unsigned int shaderProgram = compileShaders();

    // VARIABLES TO PLAY WITH:
    // Try changing 3 (Triangle) to 4 (Diamond), 6 (Hexagon), or 30 (Circle).
    // Try changing the radius from 0.5f to 1.5f.
    int polygonIndices = 0;
    unsigned int polygonVAO = createPolygon(4, 0.5f, polygonIndices);

    unsigned int axesVAO = createAxes();

    glm::vec3 squarePosition = glm::vec3(0.0f, 0.0f, 0.0f);
    float deltaTime = 0.0f;
    float lastFrame = 0.0f;

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

        processInput(window, squarePosition, deltaTime);

        int width, height;
        glfwGetFramebufferSize(window, &width, &height);
        if (height == 0) height = 1;
        float aspectRatio = (float)width / (float)height;
        glViewport(0, 0, width, height);

        // NEW: Clear both the color buffer and the depth buffer every frame
        glClearColor(0.0f, 0.192f, 0.325f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        glUseProgram(shaderProgram);

        // --- THE CAMERA (VIEW) & LENS (PROJECTION) ---

        // Move the camera up in the air (Y=3) and back (Z=3) so we look down at a 45-degree angle
        glm::vec3 cameraPos = glm::vec3(3.0f, 2.5f, 3.0f);

        // Tell the camera to aim directly at the center of the world
        glm::vec3 cameraTarget = glm::vec3(0.0f, 0.0f, 0.0f);

        // Tell the camera which direction is the sky, so it doesn't roll upside down
        glm::vec3 cameraUp = glm::vec3(0.0f, 1.0f, 0.0f);

        // Generate the View matrix based on our physical camera placement
        glm::mat4 view = glm::lookAt(cameraPos, cameraTarget, cameraUp);

        // 45 degree field of view, matching screen aspect ratio, viewing from 0.1 to 100 units away.
        glm::mat4 projection = glm::perspective(glm::radians(45.0f), aspectRatio, 0.1f, 100.0f);

        int viewLoc = glGetUniformLocation(shaderProgram, "view");
        int projLoc = glGetUniformLocation(shaderProgram, "projection");
        glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view));
        glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(projection));

        // --- 1. TRANSFORM THE SQUARE (MODEL) ---
        glm::mat4 model = glm::mat4(1.0f);
        model = glm::translate(model, squarePosition);

        // Spin it on the Y axis now, so you can see it rotate through 3D space!
        model = glm::rotate(model, (float)glfwGetTime(), glm::vec3(0.0f, 1.0f, 0.0f));

        int modelLoc = glGetUniformLocation(shaderProgram, "model");
        glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));

        glBindVertexArray(polygonVAO);
        // We now pass the dynamic integer so the GPU knows exactly how many triangles to build
        glDrawElements(GL_TRIANGLES, polygonIndices, GL_UNSIGNED_INT, 0);

        // --- 2. PROTECT THE AXES (MODEL) ---
        glm::mat4 identityModel = glm::mat4(1.0f);
        glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(identityModel));

        glBindVertexArray(axesVAO);

        // We now have 6 vertices to draw (X, Y, and Z lines)
        glDrawArrays(GL_LINES, 0, 6);

        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    glfwTerminate();
    return 0;
}
