#include "ThreadPool.h"
#include <algorithm> 
#include "Engine.h"
#include <iostream>
#include <cmath>
#include <deque>
#include "OBJModelPrimitive.h"
// Utility functions
namespace {
    GLuint CompileShader(GLenum type, const char* source) {
        GLuint shader = glCreateShader(type);
        glShaderSource(shader, 1, &source, nullptr);
        glCompileShader(shader);
        
        GLint success;
        glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
        if (!success) {
            char log[512];
            glGetShaderInfoLog(shader, 512, nullptr, log);
            std::cerr << "Shader compilation error: " << log << std::endl;
        }
        return shader;
    }
    
    GLuint CreateProgram(const char* vertex_source, const char* fragment_source) {
        GLuint vertex_shader = CompileShader(GL_VERTEX_SHADER, vertex_source);
        GLuint fragment_shader = CompileShader(GL_FRAGMENT_SHADER, fragment_source);
        
        GLuint program = glCreateProgram();
        glAttachShader(program, vertex_shader);
        glAttachShader(program, fragment_shader);
        glLinkProgram(program);
        
        GLint success;
        glGetProgramiv(program, GL_LINK_STATUS, &success);
        if (!success) {
            char log[512];
            glGetProgramInfoLog(program, 512, nullptr, log);
            std::cerr << "Program linking error: " << log << std::endl;
        }
        
	
        glDeleteShader(vertex_shader);
        glDeleteShader(fragment_shader);
        return program;
    }
}


Engine::~Engine() {
   
}




void Engine::InitializeShader() {
    const char* vertex_shader = R"(
    #version 410 core
    layout(location = 0) in vec3 aPos;
    layout(location = 1) in vec3 aColor;
    layout(location = 2) in vec3 aNormal;
    layout(location = 6) in mat4 aInstanceModel;

    uniform mat4 uView;
    uniform mat4 uProjection;

    out vec3 FragPos;
    out vec3 Normal;
    out vec3 Color;

    void main() {
        vec4 worldPos = aInstanceModel * vec4(aPos, 1.0);
        FragPos = worldPos.xyz;
        Normal = mat3(transpose(inverse(aInstanceModel))) * aNormal;
        Color = aColor;
        gl_Position = uProjection * uView * worldPos;
    }
    )";

    
    const char* fragment_shader = R"(
    #version 410 core
    in vec3 FragPos;
    in vec3 Normal;
    in vec3 Color;
    
    out vec4 FragColor;
    
    uniform vec3 uLightPos;
    uniform vec3 uViewPos;
    uniform vec3 uLightColor;
    uniform float uAmbientStrength;
    uniform float uSpecularStrength;
    uniform float uShininess;
    
    void main() {
        vec3 norm = normalize(Normal);
        
        // Ambient
        vec3 ambient = uAmbientStrength * uLightColor;
        
        // Diffuse
        vec3 lightDir = normalize(uLightPos - FragPos);
        float diff = max(dot(norm, lightDir), 0.0);
        vec3 diffuse = diff * uLightColor;
        
        // Specular
        vec3 viewDir = normalize(uViewPos - FragPos);
        vec3 reflectDir = reflect(-lightDir, norm);
        float spec = pow(max(dot(viewDir, reflectDir), 0.0), uShininess);
        vec3 specular = uSpecularStrength * spec * uLightColor;
        
        vec3 result = (ambient + diffuse + specular) * Color;
        FragColor = vec4(result, 1.0);
    }
    )";

    shader_program_ = CreateProgram(vertex_shader, fragment_shader);
    
    // Keep overlay shader creation
    const char* vs_overlay = R"(
    #version 410 core
    layout(location=0) in vec2 aPos;
    layout(location=1) in vec3 aColor;
    uniform mat4 uOrtho;
    out vec3 vColor;
    void main() {
        vec4 pos = uOrtho * vec4(aPos, 0.0, 1.0);
        gl_Position = pos;
        vColor = aColor;
    }
    )";

    const char* fs_overlay = R"(
    #version 410 core
    in vec3 vColor;
    out vec4 FragColor;
    void main() {
        FragColor = vec4(vColor, 1.0);
    }
    )";
    overlay_program = CreateProgram(vs_overlay, fs_overlay);
}



void Engine::FramebufferSizeCallback(GLFWwindow* window, int width, int height) {
    (void)window;  // suppress unused
    glViewport(0, 0, width, height);
}


class CubePrimitive : public Primitive {
public:
    CubePrimitive(const glm::vec3& color) {
        color_ = color;
        
        // 24 vertices (4 per face) with Position (3) + Color (3) + Normal (3) = 9 floats per vertex
        float vertices[] = {
            // Front face (z = 1.0)
            -1.0f, -1.0f,  1.0f,  color.r, color.g, color.b,  0.0f,  0.0f,  1.0f,
             1.0f, -1.0f,  1.0f,  color.r, color.g, color.b,  0.0f,  0.0f,  1.0f,
             1.0f,  1.0f,  1.0f,  color.r, color.g, color.b,  0.0f,  0.0f,  1.0f,
            -1.0f,  1.0f,  1.0f,  color.r, color.g, color.b,  0.0f,  0.0f,  1.0f,

            // Back face (z = -1.0)
            -1.0f, -1.0f, -1.0f,  color.r, color.g, color.b,  0.0f,  0.0f, -1.0f,
            -1.0f,  1.0f, -1.0f,  color.r, color.g, color.b,  0.0f,  0.0f, -1.0f,
             1.0f,  1.0f, -1.0f,  color.r, color.g, color.b,  0.0f,  0.0f, -1.0f,
             1.0f, -1.0f, -1.0f,  color.r, color.g, color.b,  0.0f,  0.0f, -1.0f,

            // Top face (y = 1.0)
            -1.0f,  1.0f, -1.0f,  color.r, color.g, color.b,  0.0f,  1.0f,  0.0f,
            -1.0f,  1.0f,  1.0f,  color.r, color.g, color.b,  0.0f,  1.0f,  0.0f,
             1.0f,  1.0f,  1.0f,  color.r, color.g, color.b,  0.0f,  1.0f,  0.0f,
             1.0f,  1.0f, -1.0f,  color.r, color.g, color.b,  0.0f,  1.0f,  0.0f,

            // Bottom face (y = -1.0)
            -1.0f, -1.0f, -1.0f,  color.r, color.g, color.b,  0.0f, -1.0f,  0.0f,
             1.0f, -1.0f, -1.0f,  color.r, color.g, color.b,  0.0f, -1.0f,  0.0f,
             1.0f, -1.0f,  1.0f,  color.r, color.g, color.b,  0.0f, -1.0f,  0.0f,
            -1.0f, -1.0f,  1.0f,  color.r, color.g, color.b,  0.0f, -1.0f,  0.0f,

            // Right face (x = 1.0)
             1.0f, -1.0f, -1.0f,  color.r, color.g, color.b,  1.0f,  0.0f,  0.0f,
             1.0f,  1.0f, -1.0f,  color.r, color.g, color.b,  1.0f,  0.0f,  0.0f,
             1.0f,  1.0f,  1.0f,  color.r, color.g, color.b,  1.0f,  0.0f,  0.0f,
             1.0f, -1.0f,  1.0f,  color.r, color.g, color.b,  1.0f,  0.0f,  0.0f,

            // Left face (x = -1.0)
            -1.0f, -1.0f, -1.0f,  color.r, color.g, color.b, -1.0f,  0.0f,  0.0f,
            -1.0f, -1.0f,  1.0f,  color.r, color.g, color.b, -1.0f,  0.0f,  0.0f,
            -1.0f,  1.0f,  1.0f,  color.r, color.g, color.b, -1.0f,  0.0f,  0.0f,
            -1.0f,  1.0f, -1.0f,  color.r, color.g, color.b, -1.0f,  0.0f,  0.0f
        };
        
        // Indices for 12 triangles (2 per face)
        unsigned int indices[] = {
            0,  1,  2,    0,  2,  3,    // front
            4,  5,  6,    4,  6,  7,    // back
            8,  9,  10,   8,  10, 11,   // top
            12, 13, 14,   12, 14, 15,   // bottom
            16, 17, 18,   16, 18, 19,   // right
            20, 21, 22,   20, 22, 23    // left
        };
        
        glGenVertexArrays(1, &vao_);
        glGenBuffers(1, &vbo_);
        glGenBuffers(1, &ebo_);
        
        glBindVertexArray(vao_);
        
        // Upload vertex data
        glBindBuffer(GL_ARRAY_BUFFER, vbo_);
        glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
        
        // Upload index data
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo_);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
        
        // Position attribute (location 0)
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 9 * sizeof(float), (void*)0);
        glEnableVertexAttribArray(0);
        
        // Color attribute (location 1) 
        glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 9 * sizeof(float), (void*)(3 * sizeof(float)));
        glEnableVertexAttribArray(1);
        
        // Normal attribute (location 2)
        glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, 9 * sizeof(float), (void*)(6 * sizeof(float)));
        glEnableVertexAttribArray(2);
        
        glBindVertexArray(0);
        index_count_ = 36;
    }
    
    void Render() override {
        glBindVertexArray(vao_);
        glDrawElements(GL_TRIANGLES, index_count_, GL_UNSIGNED_INT, 0);
        glBindVertexArray(0);
    }
    
    void Bind() override {
        glBindVertexArray(vao_);
    }
    
    void Cleanup() override {
        if (vao_) glDeleteVertexArrays(1, &vao_);
        if (vbo_) glDeleteBuffers(1, &vbo_);
        if (ebo_) glDeleteBuffers(1, &ebo_);
    }
};



class SpherePrimitive : public Primitive {
public:
    SpherePrimitive(int segments, const glm::vec3& color) {
        color_ = color;
        std::vector<float> vertices;
        std::vector<unsigned int> indices;
        
        // Generate sphere vertices
        for (int i = 0; i <= segments; ++i) {
            for (int j = 0; j <= segments; ++j) {
                float x = std::cos(2.0f * M_PI * j / segments) * std::sin(M_PI * i / segments);
                float y = std::cos(M_PI * i / segments);
                float z = std::sin(2.0f * M_PI * j / segments) * std::sin(M_PI * i / segments);
                
                vertices.push_back(x);
                vertices.push_back(y);
                vertices.push_back(z);
                vertices.push_back(color.r);
                vertices.push_back(color.g);
                vertices.push_back(color.b);
            }
        }
        
        // Generate indices
        for (int i = 0; i < segments; ++i) {
            for (int j = 0; j < segments; ++j) {
                int first = i * (segments + 1) + j;
                int second = first + segments + 1;
                
                indices.push_back(first);
                indices.push_back(second);
                indices.push_back(first + 1);
                
                indices.push_back(second);
                indices.push_back(second + 1);
                indices.push_back(first + 1);
            }
        }
        
        glGenVertexArrays(1, &vao_);
        glGenBuffers(1, &vbo_);
        glGenBuffers(1, &ebo_);
        
        glBindVertexArray(vao_);
        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);
        index_count_ = (GLsizei)indices.size();
    }
    
    void Render() override {
        glBindVertexArray(vao_);
        glDrawElements(GL_TRIANGLES, index_count_, GL_UNSIGNED_INT, 0);
        glBindVertexArray(0);
    }

    void Bind() override {
        glBindVertexArray(vao_);
    }
    
    void Cleanup() override {
        if (vao_) glDeleteVertexArrays(1, &vao_);
        if (vbo_) glDeleteBuffers(1, &vbo_);
        if (ebo_) glDeleteBuffers(1, &ebo_);
    }
};

class PlanePrimitive : public Primitive {
public:
    PlanePrimitive(float width, float height, const glm::vec3& color) {
        color_ = color;
        float w = width / 2.0f;
        float h = height / 2.0f;
        
        float vertices[] = {
            -w, 0, -h,  color.r, color.g, color.b,
             w, 0, -h,  color.r, color.g, color.b,
             w, 0,  h,  color.r, color.g, color.b,
            -w, 0,  h,  color.r, color.g, color.b,
        };
        
        unsigned int indices[] = { 0, 1, 2, 2, 3, 0 };
        
        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);
        index_count_ = 6;
    }
    
    void Render() override {
        glBindVertexArray(vao_);
        glDrawElements(GL_TRIANGLES, index_count_, GL_UNSIGNED_INT, 0);
        glBindVertexArray(0);
    }

    void Bind() override {
        glBindVertexArray(vao_);
    }
    
    void Cleanup() override {
        if (vao_) glDeleteVertexArrays(1, &vao_);
        if (vbo_) glDeleteBuffers(1, &vbo_);
        if (ebo_) glDeleteBuffers(1, &ebo_);
    }
};

class GridPrimitive : public Primitive {
public:
    GridPrimitive(int size, float spacing, const glm::vec3& color) {
        color_ = color;
        std::vector<float> vertices;
        
        float extent = size * spacing;
        
        // Lines parallel to X
        for (int i = -size; i <= size; ++i) {
            float z = i * spacing;
            vertices.insert(vertices.end(), {-extent, 0, z, color.r, color.g, color.b});
            vertices.insert(vertices.end(), { extent, 0, z, color.r, color.g, color.b});
        }
        
        // Lines parallel to Z
        for (int i = -size; i <= size; ++i) {
            float x = i * spacing;
            vertices.insert(vertices.end(), {x, 0, -extent, color.r, color.g, color.b});
            vertices.insert(vertices.end(), {x, 0,  extent, color.r, color.g, color.b});
        }
        
        glGenVertexArrays(1, &vao_);
        glGenBuffers(1, &vbo_);
        
        glBindVertexArray(vao_);
        glBindBuffer(GL_ARRAY_BUFFER, vbo_);
        glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(float), vertices.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);
        vertex_count_ = (GLsizei)(vertices.size() / 6);
        draw_mode_ = GL_LINES;

       
    }

    GLenum GetDrawMode() const override { return GL_LINES; }
    
    void Render() override {
        glBindVertexArray(vao_);
        glDrawArrays(draw_mode_, 0, vertex_count_);
        glBindVertexArray(0);
    }

    void Bind() override {
        glBindVertexArray(vao_);
    }
    
    void Cleanup() override {
        if (vao_) glDeleteVertexArrays(1, &vao_);
        if (vbo_) glDeleteBuffers(1, &vbo_);
    }
};

// GameObject implementation
GameObject::GameObject(std::shared_ptr<Primitive> primitive, const glm::vec3& position) 
    : primitive_(primitive), position_(position) {}

glm::mat4 GameObject::GetModelMatrix() const {
    glm::mat4 model = glm::mat4(1.0f);
    model = glm::translate(model, position_);
    model = glm::rotate(model, glm::radians(rotation_.x), glm::vec3(1, 0, 0));
    model = glm::rotate(model, glm::radians(rotation_.y), glm::vec3(0, 1, 0));
    model = glm::rotate(model, glm::radians(rotation_.z), glm::vec3(0, 0, 1));
    model = glm::scale(model, scale_);
    return model;
}

void GameObject::Cleanup() {
    if (primitive_) {
        primitive_->Cleanup();
    }
}

float GameObject::GetBoundingRadius() const {
    // Approximate: assume unit primitive scaled by max scale component
    float maxScale = std::max({ scale_.x, scale_.y, scale_.z });
    return maxScale * 1.732f;  // √3 for cube diagonal
}



void GameObject::Render() {
    if (primitive_) {
        primitive_->Render();
    }
}


// Engine constructor – must be defined before the other methods
Engine::Engine(int w, int h, const char* title)
    : width_(w)
    , height_(h)
    , window_(nullptr)
    , threadPool_(std::thread::hardware_concurrency())
{
    if (!glfwInit()) {
        throw std::runtime_error("Failed to initialize GLFW");
    }

    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 1);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
#ifdef __APPLE__
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif

    window_ = glfwCreateWindow(width_, height_, title, nullptr, nullptr);
    if (!window_) {
        glfwTerminate();
        throw std::runtime_error("Failed to create GLFW window");
    }

    glfwMakeContextCurrent(window_);
    glfwSetFramebufferSizeCallback(window_, FramebufferSizeCallback);

    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
        throw std::runtime_error("Failed to initialize GLAD");
    }

    glEnable(GL_DEPTH_TEST);
    glClearColor(0.1f, 0.1f, 0.2f, 1.0f);
    glViewport(0, 0, width_, height_);

    // Set up 2D orthographic projection for overlays
    ortho2D = glm::ortho(0.0f, float(width_), 0.0f, float(height_));

    glDisable(GL_DEPTH_TEST);

    
    InitializeShader();
    InitOverlay();
}

// -------------------------------------------------------------
// CullSphere — Gribb/Hartmann frustum plane extraction
//
// Extracts 6 frustum planes directly from the view-projection
// matrix by combining its rows. [web:72][web:76]
// Returns TRUE if the sphere is fully OUTSIDE any plane
// (i.e. should be culled — skip rendering).
// -------------------------------------------------------------
bool Engine::CullSphere(const glm::mat4& vp,
                        const glm::vec3& center,
                        float radius)
{
    // GLM stores matrices column-major, so row(vp, n) extracts row n.
    // Each frustum plane = combination of row 3 ± row n. [web:75][web:76]
    const glm::vec4 row0 = glm::row(vp, 0);
    const glm::vec4 row1 = glm::row(vp, 1);
    const glm::vec4 row2 = glm::row(vp, 2);
    const glm::vec4 row3 = glm::row(vp, 3);

    const glm::vec4 planes[6] = {
        row3 + row0,   // Left
        row3 - row0,   // Right
        row3 + row1,   // Bottom
        row3 - row1,   // Top
        row3 + row2,   // Near
        row3 - row2,   // Far
    };

    const glm::vec4 pt(center, 1.0f);

    for (const auto& plane : planes) {
        // Signed distance from sphere centre to plane.
        // If distance < -radius, sphere is fully on the outside. [web:73]
        if (glm::dot(plane, pt) < -radius) {
            return true;   // Cull — outside this plane
        }
    }
    return false;   // Inside or intersecting all 6 planes — keep
}

// Culling subset method
Engine::CullingResult Engine::CullObjectsSubset(
    const std::vector<GameObject*>& objects,
    size_t startIdx, 
    size_t endIdx,
    const glm::mat4& viewProjection)
{
    CullingResult result;
    std::unordered_map<Primitive*, size_t> primitiveToIndex;
    
    for (size_t i = startIdx; i < endIdx && i < objects.size(); ++i) {
        GameObject* obj = objects[i];
        
        // Perform frustum culling
        glm::vec3 pos = obj->GetPosition();
        float radius = obj->GetBoundingRadius();
        if (CullSphere(viewProjection, pos, radius)) {
            continue;  // Object is outside frustum
        }
        
        // Group by primitive
        Primitive* prim = obj->GetPrimitive();
        auto it = primitiveToIndex.find(prim);
        
        if (it == primitiveToIndex.end()) {
            // New primitive - create new batch
            result.emplace_back();
            result.back().primitive = prim;
            result.back().modelMatrices.push_back(obj->GetModelMatrix());
            primitiveToIndex[prim] = result.size() - 1;
        } else {
            // Existing primitive - add to existing batch
            result[it->second].modelMatrices.push_back(obj->GetModelMatrix());
        }
    }
    
    return result;
}

// Merge results from all threads
void Engine::MergeCullingResults(
    const std::vector<CullingResult>& results,
    std::unordered_map<Primitive*, std::vector<glm::mat4>>& outBatches)
{
    for (const auto& result : results) {
        for (const auto& batch : result) {
            auto& targetBatch = outBatches[batch.primitive];
            targetBatch.insert(targetBatch.end(), 
                             batch.modelMatrices.begin(), 
                             batch.modelMatrices.end());
        }
    }
}


struct Plane { glm::vec4 p; };
bool CullSphere(const glm::mat4& vp, const glm::vec3& center, float r) {
    // Extract clip planes
    for (int i = 0; i < 6; ++i) {
        glm::vec4 plane;
        switch(i){
            case 0: plane = vp[3] + vp[0]; break; // left
            case 1: plane = vp[3] - vp[0]; break; // right
            case 2: plane = vp[3] + vp[1]; break; // bottom
            case 3: plane = vp[3] - vp[1]; break; // top
            case 4: plane = vp[3] + vp[2]; break; // near
            case 5: plane = vp[3] - vp[2]; break; // far
        }
        float d = glm::dot(glm::vec3(plane), center) + plane.w;
        if (d < -r) return true;
    }
    return false;
}

void Engine::InitOverlay() {
    // 1) Generate VAO/VBO for overlay
    glGenVertexArrays(1, &overlay_vao);
    glGenBuffers(1, &overlay_vbo);

    // 2) Overlay shaders
    const char* vs_overlay = R"(
    #version 410 core
    layout(location = 0) in vec2 aPos;      // Position in screen pixels
    layout(location = 1) in vec3 aColor;    // Vertex color
    uniform mat4 uOrtho;                    // Orthographic projection
    out vec3 vColor;
    void main() {
        gl_Position = uOrtho * vec4(aPos, 0.0, 1.0);
        vColor = aColor;
    }
    )";

    const char* fs_overlay = R"(
    #version 410 core
    in vec3 vColor;
    out vec4 FragColor;
    void main() {
        FragColor = vec4(vColor, 1.0);
    }
    )";

    // 3) Compile and link overlay program
    overlay_program = CreateProgram(vs_overlay, fs_overlay);

    // 4) Configure VAO/VBO vertex attributes (two floats + three floats)
    glBindVertexArray(overlay_vao);
    glBindBuffer(GL_ARRAY_BUFFER, overlay_vbo);

    // Attribute 0: aPos (vec2)
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(
        0,                   // index
        2,                   // size (x, y)
        GL_FLOAT,            // type
        GL_FALSE,            // normalized
        sizeof(float) * 5,   // stride (2 pos + 3 color)
        (void*)0             // offset
    );

    // Attribute 1: aColor (vec3)
    glEnableVertexAttribArray(1);
    glVertexAttribPointer(
        1,                               // index
        3,                               // size (r, g, b)
        GL_FLOAT,                        // type
        GL_FALSE,                        // normalized
        sizeof(float) * 5,               // stride
        (void*)(sizeof(float) * 2)       // offset after 2 floats
    );

    // Unbind VAO to avoid accidental modification
    glBindVertexArray(0);
}





bool Engine::OnInitialize() { return true; }

void Engine::OnShutdown() {
    for (auto& obj : game_objects_) {
        if (obj) obj->Cleanup();
    }
    game_objects_.clear();
}

void Engine::OnUpdate(float delta_time) {}

void Engine::RenderOverlayTestPoint() {
    glDisable(GL_DEPTH_TEST);
    glUseProgram(overlay_program);
    GLint locOrtho = glGetUniformLocation(overlay_program, "uOrtho");
    glm::mat4 ortho = glm::ortho(0.0f, (float)width_, 0.0f, (float)height_);
    glUniformMatrix4fv(locOrtho, 1, GL_FALSE, &ortho[0][0]);

    // Single white point at (100,100)
    struct P { float x,y; float r,g,b; };
    P pt = {100, 100, 1,1,1};
    glBindVertexArray(overlay_vao);
    glBindBuffer(GL_ARRAY_BUFFER, overlay_vbo);
    glBufferData(GL_ARRAY_BUFFER, sizeof(pt), &pt, GL_DYNAMIC_DRAW);

    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(P), (void*)0);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(P), (void*)(2*sizeof(float)));
    glEnableVertexAttribArray(1);

    glDrawArrays(GL_POINTS, 0, 1);

    glBindVertexArray(0);
    glUseProgram(0);
    glEnable(GL_DEPTH_TEST);
}



void Engine::RenderOverlay() {
    // 1) Update stats every 1.0s
    static float timer = 0.0f;
    static float last_fps = 0.0f, last_ms = 0.0f;
    static float min_fps = FLT_MAX, max_fps = 0.0f, sum_fps = 0.0f;
    static int count = 0;
    timer += perf_times[(perf_pos + PERF_HISTORY - 1) % PERF_HISTORY] / 1000.0f;
    if (timer >= 1.0f) {
        float ms = perf_times[(perf_pos + PERF_HISTORY - 1) % PERF_HISTORY];
        last_ms  = ms;
        last_fps = 1000.0f / (ms > 0 ? ms : 1);
        min_fps = std::min(min_fps, last_fps);
        max_fps = std::max(max_fps, last_fps);
        sum_fps += last_fps;
        count++;
        timer = 0.0f;
    }
    float avg_fps = count ? sum_fps / count : last_fps;

    // 2) GL state
    glDisable(GL_DEPTH_TEST);
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    glUseProgram(overlay_program);
    GLint locOrtho = glGetUniformLocation(overlay_program, "uOrtho");
    glm::mat4 ortho = glm::ortho(0.0f, float(width_), 0.0f, float(height_));
    glUniformMatrix4fv(locOrtho, 1, GL_FALSE, &ortho[0][0]);

    glBindVertexArray(overlay_vao);
    glBindBuffer(GL_ARRAY_BUFFER, overlay_vbo);

    struct V { float x,y; float r,g,b; };

    // 3) Define strokes for text
    static const std::vector<std::vector<std::pair<float,float>>> strokes = {
        {{0,0},{1,0},{1,1},{0,1},{0,0}},
        {{0.5f,0},{0.5f,1}},
        {{0,1},{1,1},{1,0.5f},{0,0.5f},{0,0},{1,0}},
        {{0,1},{1,1},{1,0},{0,0},{1,0.5f}},
        {{0,1},{0,0.5f},{1,0.5f},{1,1},{1,0}},
        {{1,1},{0,1},{0,0.5f},{1,0.5f},{1,0},{0,0}},
        {{1,1},{0,1},{0,0},{1,0},{1,0.5f},{0,0.5f}},
        {{0,1},{1,1},{0.5f,0}},
        {{0,0},{0,1},{1,1},{1,0},{0,0},{0,0.5f},{1,0.5f}},
        {{1,0},{1,1},{0,1},{0,0.5f},{1,0.5f}}
    };

    // 4) drawChar lambda
    auto drawChar = [&](char c, float x, float y, float size){
        std::vector<float> verts;
        if (c >= '0' && c <= '9') {
            for (auto&p:strokes[c - '0']) {
                verts.push_back(x + p.first * size);
                verts.push_back(y + p.second * size);
                verts.push_back(1); verts.push_back(1); verts.push_back(1);
            }
            glBufferData(GL_ARRAY_BUFFER, verts.size()*sizeof(float), verts.data(), GL_DYNAMIC_DRAW);
            glVertexAttribPointer(0,2,GL_FLOAT,GL_FALSE,5*sizeof(float),0);
            glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,5*sizeof(float),(void*)(2*sizeof(float)));
            glDrawArrays(GL_LINE_STRIP,0,(GLsizei)(verts.size()/5));
        } else if (c == '.') {
            float s = size * 0.2f;
            float pts[] = {
                x - s, y - s, 1,1,1, x + s, y - s, 1,1,1, x + s, y + s, 1,1,1,
                x - s, y - s, 1,1,1, x + s, y + s, 1,1,1, x - s, y + s, 1,1,1
            };
            glBufferData(GL_ARRAY_BUFFER,sizeof(pts),pts,GL_DYNAMIC_DRAW);
            glVertexAttribPointer(0,2,GL_FLOAT,GL_FALSE,5*sizeof(float),0);
            glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,5*sizeof(float),(void*)(2*sizeof(float)));
            glDrawArrays(GL_TRIANGLES,0,6);
        } else if (c=='F') {
            float pts[] = {
                x,y,1,1,1, x,y+size,1,1,1,
                x,y+size,1,1,1, x+size,y+size,1,1,1,
                x,y+size*0.5f,1,1,1, x+size*0.7f,y+size*0.5f,1,1,1
            };
            glBufferData(GL_ARRAY_BUFFER,sizeof(pts),pts,GL_DYNAMIC_DRAW);
            glVertexAttribPointer(0,2,GL_FLOAT,GL_FALSE,5*sizeof(float),0);
            glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,5*sizeof(float),(void*)(2*sizeof(float)));
            glDrawArrays(GL_LINES,0,6);
        } else if (c=='P') {
            float pts[] = {
                x,y,1,1,1, x,y+size,1,1,1,
                x,y+size,1,1,1, x+size,y+size,1,1,1,
                x+size,y+size,1,1,1, x+size,y+size*0.5f,1,1,1,
                x+size,y+size*0.5f,1,1,1, x,y+size*0.5f,1,1,1
            };
            glBufferData(GL_ARRAY_BUFFER,sizeof(pts),pts,GL_DYNAMIC_DRAW);
            glVertexAttribPointer(0,2,GL_FLOAT,GL_FALSE,5*sizeof(float),0);
            glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,5*sizeof(float),(void*)(2*sizeof(float)));
            glDrawArrays(GL_LINES,0,8);
        } else if (c=='S') {
            float pts[] = {
                x,         y+size,      1,1,1, x+size,    y+size,      1,1,1,
                x,         y+size,      1,1,1, x,         y+size*0.5f,1,1,1,
                x,         y+size*0.5f,1,1,1, x+size,    y+size*0.5f,1,1,1,
                x+size,    y+size*0.5f,1,1,1, x+size,    y,           1,1,1,
                x+size,    y,           1,1,1, x,         y,           1,1,1
            };
            glBufferData(GL_ARRAY_BUFFER,sizeof(pts),pts,GL_DYNAMIC_DRAW);
            glVertexAttribPointer(0,2,GL_FLOAT,GL_FALSE,5*sizeof(float),0);
            glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,5*sizeof(float),(void*)(2*sizeof(float)));
            glDrawArrays(GL_LINES,0,10);
        } else if (c==':') {
            float pts[] = {
                x+size*0.5f, y+size*0.75f, 1,1,1, x+size*0.5f, y+size*0.25f, 1,1,1
            };
            glBufferData(GL_ARRAY_BUFFER,sizeof(pts),pts,GL_DYNAMIC_DRAW);
            glVertexAttribPointer(0,2,GL_FLOAT,GL_FALSE,5*sizeof(float),0);
            glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,5*sizeof(float),(void*)(2*sizeof(float)));
            glDrawArrays(GL_POINTS,0,2);
        } else if (c == 'M') {
            float pts[] = {
                x,      y,       1,1,1,  x,          y+size, 1,1,1,
                x,      y+size,  1,1,1,  x+size*0.5f, y+size*0.5f,1,1,1,
                x+size*0.5f,y+size*0.5f,1,1,1, x+size, y+size, 1,1,1,
                x+size, y+size,  1,1,1,  x+size,     y,      1,1,1
            };
            glBufferData(GL_ARRAY_BUFFER, sizeof(pts), pts, GL_DYNAMIC_DRAW);
            glVertexAttribPointer(0,2,GL_FLOAT,GL_FALSE,5*sizeof(float),0);
            glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,5*sizeof(float),(void*)(2*sizeof(float)));
            glDrawArrays(GL_LINES, 0, 8);
        } else if (c == 'I') {
            float pts[] = {
                x,       y+size,  1,1,1, x+size, y+size, 1,1,1,
                x+size*0.5f,y+size,1,1,1, x+size*0.5f, y, 1,1,1,
                x,       y,       1,1,1, x+size, y,      1,1,1
            };
            glBufferData(GL_ARRAY_BUFFER, sizeof(pts), pts, GL_DYNAMIC_DRAW);
            glVertexAttribPointer(0,2,GL_FLOAT,GL_FALSE,5*sizeof(float),0);
            glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,5*sizeof(float),(void*)(2*sizeof(float)));
            glDrawArrays(GL_LINES, 0, 6);
        } else if (c == 'N') {
            float pts[] = {
                x,      y,      1,1,1, x,      y+size, 1,1,1,
                x,      y+size,1,1,1, x+size, y,      1,1,1,
                x+size, y,      1,1,1, x+size, y+size,1,1,1
            };
            glBufferData(GL_ARRAY_BUFFER, sizeof(pts), pts, GL_DYNAMIC_DRAW);
            glVertexAttribPointer(0,2,GL_FLOAT,GL_FALSE,5*sizeof(float),0);
            glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,5*sizeof(float),(void*)(2*sizeof(float)));
            glDrawArrays(GL_LINES, 0, 6);
        } else if (c == 'A') {
            float pts[] = {
                x,         y,        1,1,1, x+size*0.5f, y+size, 1,1,1,
                x+size,    y,        1,1,1, x+size*0.5f, y+size, 1,1,1,
                x+size*0.2f,y+size*0.5f,1,1,1, x+size*0.8f, y+size*0.5f, 1,1,1
            };
            glBufferData(GL_ARRAY_BUFFER, sizeof(pts), pts, GL_DYNAMIC_DRAW);
            glVertexAttribPointer(0,2,GL_FLOAT,GL_FALSE,5*sizeof(float),0);
            glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,5*sizeof(float),(void*)(2*sizeof(float)));
            glDrawArrays(GL_LINES, 0, 6);
        } else if (c == 'V') {
            float pts[] = {
                x,      y+size, 1,1,1, x+size*0.5f, y,      1,1,1,
                x+size, y+size, 1,1,1, x+size*0.5f, y,      1,1,1
            };
            glBufferData(GL_ARRAY_BUFFER, sizeof(pts), pts, GL_DYNAMIC_DRAW);
            glVertexAttribPointer(0,2,GL_FLOAT,GL_FALSE,5*sizeof(float),0);
            glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,5*sizeof(float),(void*)(2*sizeof(float)));
            glDrawArrays(GL_LINES, 0, 4);
        } else if (c == 'G') {
            float pts[] = {
                x+size, y,        1,1,1, x,     y,       1,1,1,
                x,      y,        1,1,1, x,     y+size,  1,1,1,
                x,      y+size,   1,1,1, x+size, y+size, 1,1,1,
                x+size, y+size,   1,1,1, x+size, y+size*0.5f,1,1,1,
                x+size, y+size*0.5f,1,1,1, x+size*0.5f, y+size*0.5f,1,1,1
            };
            glBufferData(GL_ARRAY_BUFFER, sizeof(pts), pts, GL_DYNAMIC_DRAW);
            glVertexAttribPointer(0,2,GL_FLOAT,GL_FALSE,5*sizeof(float),0);
            glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,5*sizeof(float),(void*)(2*sizeof(float)));
            glDrawArrays(GL_LINES, 0, 10);
        } else if (c == 'X') {
            float pts[] = {
                x,          y+size, 1,1,1,   x+size, y,        1,1,1,
                x,          y,      1,1,1,   x+size, y+size,  1,1,1
            };
            glBufferData(GL_ARRAY_BUFFER, sizeof(pts), pts, GL_DYNAMIC_DRAW);
            glVertexAttribPointer(0,2,GL_FLOAT,GL_FALSE,5*sizeof(float),0);
            glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,5*sizeof(float),(void*)(2*sizeof(float)));
            glDrawArrays(GL_LINES, 0, 4);
        }
    };

    // 5) Dynamic panel background
    int samples = std::min(perf_count, PERF_HISTORY);
    float scrollSpeed = 0.5f;
    float graphHeight = 50.0f, padding = 10.0f, sz = 12.0f;
    float totalWidth = samples * scrollSpeed;
    float x0 = 10.0f, y0 = height_ - (3 * (graphHeight + padding) + 60.0f);
    float bgW = totalWidth + 60.0f, bgH = 3 * (graphHeight + padding) + 60.0f;

    V bgPanel[6] = {
        {x0,      y0,      0.1f,0.1f,0.15f},
        {x0+bgW,  y0,      0.1f,0.1f,0.15f},
        {x0+bgW,  y0+bgH,  0.15f,0.15f,0.2f},
        {x0,      y0,      0.1f,0.1f,0.15f},
        {x0+bgW,  y0+bgH,  0.15f,0.15f,0.2f},
        {x0,      y0+bgH,  0.15f,0.15f,0.2f},
    };
    glBufferData(GL_ARRAY_BUFFER, sizeof(bgPanel), bgPanel, GL_DYNAMIC_DRAW);
    glVertexAttribPointer(0,2,GL_FLOAT,GL_FALSE,sizeof(V),(void*)0);
    glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,sizeof(V),(void*)(2*sizeof(float)));
    glDrawArrays(GL_TRIANGLES, 0, 6);

    // 6) Stats arrays
    static float avg_times[PERF_HISTORY];
    for(int i=0;i<samples;++i)
        avg_times[i] = 1000.0f / perf_times[(perf_pos+PERF_HISTORY-samples+i)%PERF_HISTORY];
    struct Stat { const float* data; glm::vec3 color; const char* label; };
    Stat stats[3] = {
        { perf_times, {0.0f,1.0f,0.0f}, "MS" },
        { avg_times,  {0.0f,1.0f,1.0f}, "AVG" },
        { nullptr,    {1.0f,1.0f,0.0f}, "FPS" }
    };
    static std::deque<float> fpsHistory;
    if(fpsHistory.size()>= (size_t)samples) fpsHistory.pop_front();
    fpsHistory.push_back(last_fps);

    // 7) Draw each graph row with labels
    for(int g=0; g<3; ++g) {
        float gx = x0 + 50.0f, gy = y0 + 10.0f + g*(graphHeight+padding);
        
        // Label
        drawChar(stats[g].label[0], x0 + 5, gy + graphHeight*0.5f - sz*0.5f, sz);
        if(stats[g].label[1])
            drawChar(stats[g].label[1], x0 + 5 + sz*1.2f, gy + graphHeight*0.5f - sz*0.5f, sz);

        // Build strip
        std::vector<V> strip;
        for(int i=0;i<samples;++i) {
            float val = (g<2 ? stats[g].data[i] : fpsHistory[i]);
            float norm = glm::clamp(val / 100.0f, 0.0f, 1.0f);
            float fx = gx + i * scrollSpeed;
            float fy = gy + norm * graphHeight;
            strip.push_back({fx, fy, stats[g].color.r, stats[g].color.g, stats[g].color.b});
        }

        glBufferData(GL_ARRAY_BUFFER, strip.size()*sizeof(V), strip.data(), GL_DYNAMIC_DRAW);
        glVertexAttribPointer(0,2,GL_FLOAT,GL_FALSE,sizeof(V),(void*)0);
        glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,sizeof(V),(void*)(2*sizeof(float)));
        glEnable(GL_LINE_SMOOTH);
        glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
        glLineWidth(2.0f);
        glDrawArrays(GL_LINE_STRIP, 0, (GLsizei)strip.size());
        glLineWidth(1.0f);
        glDisable(GL_LINE_SMOOTH);
    }

    // 8) Bottom text stats
    char txtbuf[256];
    int len = snprintf(txtbuf, sizeof(txtbuf),
        "FPS:%.1f  MS:%.1f  MIN:%.1f  MAX:%.1f  AVG:%.1f",
        last_fps, last_ms, min_fps, max_fps, avg_fps);

    float cx = x0 + 10, cy = y0 + bgH - 20, s_x = 12;
    for (int i = 0; i < len; ++i) {
        char c = txtbuf[i];
        drawChar(c, cx, cy, s_x);
        cx += sz * 1.5f;
    }

    // 9) Restore GL state
    glBindVertexArray(0);
    glUseProgram(0);
    glDisable(GL_BLEND);
    glEnable(GL_DEPTH_TEST);
}


void Engine::RenderSceneInstanced() {
    glViewport(0, 0, width_, height_);
    glEnable(GL_DEPTH_TEST);
    glDepthFunc(GL_LESS);
    glClear(GL_DEPTH_BUFFER_BIT);
    glUseProgram(shader_program_);

    // Set lighting uniforms
    float time = (float)glfwGetTime() * 0.5f;
    glm::vec3 lightPos = glm::vec3(20.0f * cos(time), 25.0f, 20.0f * sin(time));

    // Set texture uniforms 
    glUniform1i(glGetUniformLocation(shader_program_, "texture_diffuse1"), 0);
    
    glUniform3fv(glGetUniformLocation(shader_program_, "uLightPos"), 1, &lightPos[0]);
    glUniform3fv(glGetUniformLocation(shader_program_, "uViewPos"), 1, &camera_position_[0]);
    glUniform3f(glGetUniformLocation(shader_program_, "uLightColor"), 2.0f, 1.8f, 1.5f);
    glUniform1f(glGetUniformLocation(shader_program_, "uAmbientStrength"), 0.1f);
    glUniform1f(glGetUniformLocation(shader_program_, "uSpecularStrength"), 0.5f);
    glUniform1f(glGetUniformLocation(shader_program_, "uShininess"), 32.0f);

    // Set view and projection matrices
    glm::mat4 view = glm::lookAt(camera_position_, camera_target_, camera_up_);
    glm::mat4 proj = glm::perspective(glm::radians(60.0f), float(width_) / height_, 0.1f, 1000.0f);
    
    GLint viewLoc = glGetUniformLocation(shader_program_, "uView");
    GLint projLoc = glGetUniformLocation(shader_program_, "uProjection");
    glUniformMatrix4fv(viewLoc, 1, GL_FALSE, &view[0][0]);
    glUniformMatrix4fv(projLoc, 1, GL_FALSE, &proj[0][0]);

    // MULTITHREADED CULLING SECTION
    glm::mat4 vp = proj * view;
   
    std::vector<GameObject*> raw_objects;
    raw_objects.reserve(game_objects_.size());
    for (const auto& sp : game_objects_)
        raw_objects.push_back(sp.get());

    if (raw_objects.empty()) return;

    // Determine number of threads and chunk size
    size_t numThreads = std::min(threadPool_.size(), game_objects_.size());
    numThreads = std::max(numThreads, static_cast<size_t>(1));
    size_t objectsPerThread = (game_objects_.size() + numThreads - 1) / numThreads;
    
    // Launch culling tasks
    std::vector<std::future<CullingResult>> futures;
    futures.reserve(numThreads);
    
    for (size_t i = 0; i < numThreads; ++i) {
        size_t startIdx = i * objectsPerThread;
        size_t endIdx = std::min(startIdx + objectsPerThread, game_objects_.size());
        
        if (startIdx >= game_objects_.size()) break;
        
        futures.push_back(threadPool_.enqueue(
            [this, &raw_objects, startIdx, endIdx, vp]() -> CullingResult {
        return CullObjectsSubset(raw_objects, startIdx, endIdx, vp);
        }
    ));
    }
    
    // Collect results
    std::vector<CullingResult> cullingResults;
    cullingResults.reserve(futures.size());
    
    for (auto& future : futures) {
        cullingResults.push_back(future.get());
    }
    
    // Merge into final batches
    std::unordered_map<Primitive*, std::vector<glm::mat4>> batches;
    MergeCullingResults(cullingResults, batches);

    // Instance buffer setup (same as before)
    static GLuint instVBO = 0;
    if (!instVBO) glGenBuffers(1, &instVBO);

    // Render each batch (same as before)
    for (auto& kv : batches) {
        auto& models = kv.second;
        if (models.empty()) continue;

        // Upload model matrices to instance buffer
        glBindBuffer(GL_ARRAY_BUFFER, instVBO);
        glBufferData(GL_ARRAY_BUFFER,
                     models.size() * sizeof(glm::mat4),
                     models.data(),
                     GL_DYNAMIC_DRAW);

        // Bind primitive VAO
        Primitive* prim = kv.first;
        prim->Bind();
        
        // Set up instanced attributes for model matrix (locations 6-9)
        glBindBuffer(GL_ARRAY_BUFFER, instVBO);
        for (int i = 0; i < 4; ++i) {
            int loc = 6 + i;
            glEnableVertexAttribArray(loc);
            glVertexAttribPointer(loc, 4, GL_FLOAT, GL_FALSE,
                                  sizeof(glm::mat4), (void*)(sizeof(float) * 4 * i));
            glVertexAttribDivisor(loc, 1);
        }

        // Draw instanced geometry
        if (prim->GetIndexCount() > 0) {
            glDrawElementsInstanced(
                prim->GetDrawMode(),
                prim->GetIndexCount(),
                GL_UNSIGNED_INT,
                0,
                (GLsizei)models.size()
            );
        } else {
            glDrawArraysInstanced(
                prim->GetDrawMode(),
                0,
                prim->GetVertexCount(),
                (GLsizei)models.size()
            );
        }

        // Reset divisors to 0 for next batch
        for (int i = 0; i < 4; ++i)
            glVertexAttribDivisor(6 + i, 0);
    }

    glUseProgram(0);
}




void Engine::OnRender() {
    //RenderScene();
    RenderSceneInstanced();
    glDisable(GL_DEPTH_TEST);
    RenderOverlay();
    
}


void Engine::Run() {
    if (!OnInitialize()) return;


    last_time_ = (float)glfwGetTime();

    
    while (!glfwWindowShouldClose(window_)) {
        float current_time = (float)glfwGetTime();
        float delta_time = current_time - last_time_;
        last_time_ = current_time;

        // <-- Record performance here -->
        perf_times[perf_pos] = delta_time * 1000.0f;
        perf_pos = (perf_pos + 1) % PERF_HISTORY;
        if (perf_count < PERF_HISTORY) ++perf_count;

        glfwPollEvents();
        OnUpdate(delta_time);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

       

        OnRender();  // Calls RenderScene() and now RenderOverlay()
        glfwSwapBuffers(window_);
    }
    OnShutdown();
}



std::shared_ptr<Primitive> Engine::CreateOBJModel(const std::string& objPath, const std::string& texturePath) {
    return std::make_shared<OBJModelPrimitive>(objPath, texturePath);
}


void Engine::RenderScene() {
    glUseProgram(shader_program_);
    
    // Set up matrices
    glm::mat4 view = glm::lookAt(camera_position_, camera_target_, camera_up_);
    glm::mat4 projection = glm::perspective(glm::radians(60.0f), (float)width_ / height_, 0.1f, 100.0f);
    
    GLint viewLoc = glGetUniformLocation(shader_program_, "uView");
    GLint projLoc = glGetUniformLocation(shader_program_, "uProjection");
    GLint modelLoc = glGetUniformLocation(shader_program_, "uModel");
    
    glUniformMatrix4fv(viewLoc, 1, GL_FALSE, &view[0][0]);
    glUniformMatrix4fv(projLoc, 1, GL_FALSE, &projection[0][0]);
    
    // Render all game objects
    for (const auto& obj : game_objects_) {
        if (obj) {
            glm::mat4 model = obj->GetModelMatrix();
            glUniformMatrix4fv(modelLoc, 1, GL_FALSE, &model[0][0]);
            obj->Render();
        }
    }
    
    glUseProgram(0);
}

// Primitive factory methods
std::shared_ptr<Primitive> Engine::CreateCube(const glm::vec3& color) {
    return std::make_shared<CubePrimitive>(color);
}

std::shared_ptr<Primitive> Engine::CreateSphere(int segments, const glm::vec3& color) {
    return std::make_shared<SpherePrimitive>(segments, color);
}

std::shared_ptr<Primitive> Engine::CreatePlane(float width, float height, const glm::vec3& color) {
    return std::make_shared<PlanePrimitive>(width, height, color);
}

std::shared_ptr<Primitive> Engine::CreateGrid(int size, float spacing, const glm::vec3& color) {
    return std::make_shared<GridPrimitive>(size, spacing, color);
}

// GameObject management
std::shared_ptr<GameObject> Engine::CreateGameObject(std::shared_ptr<Primitive> primitive, const glm::vec3& position) {
    return std::make_shared<GameObject>(primitive, position);
}

void Engine::AddGameObject(std::shared_ptr<GameObject> obj) {
    game_objects_.push_back(obj);
}

void Engine::RemoveGameObject(std::shared_ptr<GameObject> obj) {
    game_objects_.erase(std::remove(game_objects_.begin(), game_objects_.end(), obj), game_objects_.end());
}

// Camera control
void Engine::SetCameraPosition(const glm::vec3& position) {
    camera_position_ = position;
}

void Engine::SetCameraTarget(const glm::vec3& target) {
    camera_target_ = target;
}

void Engine::SetCameraOrbit(float distance, float angle, float height) {
    camera_position_ = glm::vec3(
        std::cos(glm::radians(angle)) * distance,
        height,
        std::sin(glm::radians(angle)) * distance
    );
}
