#pragma once
#include <glad.h>
#include <GLFW/glfw3.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <vector>
#include <memory>
#include <functional>
#include "ThreadPool.h"
#include "Font.h"
#include "Settings.h"
#include "Audio.h"
#include "AgentAPI.h"
#include "Physics.h"
#include <glm/gtc/matrix_access.hpp>
#include <unordered_map>


// Forward declarations
class Primitive;
class GameObject;


class Engine {
public:
    Engine(int window_width = 1280, int window_height = 720, const char* title = "C++ Engine");
    virtual ~Engine();


    virtual bool OnInitialize();
    virtual void OnShutdown();
    virtual void OnUpdate(float delta_time);
    virtual void OnRender();


    void Run();


    // Primitive factory methods
    std::shared_ptr<Primitive> CreateCube(const glm::vec3& color = glm::vec3(1.0f));
    std::shared_ptr<Primitive> CreateSphere(int segments = 32, const glm::vec3& color = glm::vec3(1.0f));
    std::shared_ptr<Primitive> CreatePlane(float width = 2.0f, float height = 2.0f, const glm::vec3& color = glm::vec3(1.0f));
    std::shared_ptr<Primitive> CreateGrid(int size = 20, float spacing = 1.0f, const glm::vec3& color = glm::vec3(0.6f));
    std::shared_ptr<Primitive> CreateOBJModel(const std::string& objPath, const std::string& texturePath = "");
    std::shared_ptr<Primitive> CreateCylinder(float radius = 0.5f, float height = 2.0f, int segments = 16, const glm::vec3& color = glm::vec3(1.0f));
    std::shared_ptr<Primitive> CreateCone(float radius = 1.0f, float height = 2.0f, int segments = 16, const glm::vec3& color = glm::vec3(1.0f));
    std::shared_ptr<Primitive> CreateTerrain(int resolution = 64, float size = 100.0f, float maxHeight = 8.0f, const glm::vec3& baseColor = glm::vec3(0.3f, 0.5f, 0.2f));


    // GameObject management
    std::shared_ptr<GameObject> CreateGameObject(std::shared_ptr<Primitive> primitive, const glm::vec3& position = glm::vec3(0.0f));
    void AddGameObject(std::shared_ptr<GameObject> obj);
    void RemoveGameObject(std::shared_ptr<GameObject> obj);


    // Camera control
    void SetCameraPosition(const glm::vec3& position);
    void SetCameraTarget(const glm::vec3& target);
    void SetCameraOrbit(float distance, float angle, float height = 5.0f);
    const glm::vec3& GetCameraPosition() const { return camera_position_; }
    const glm::vec3& GetCameraTarget() const { return camera_target_; }

    GLuint text_vao = 0, text_vbo = 0;


    GLFWwindow* GetWindow() const { return window_; }
    float GetTime() const { return (float)glfwGetTime(); }


   
private:
    ThreadPool threadPool_;
    
    // Culling result structure — now keyed by (primitive, texture) so batches
    // can be issued per-texture.
    struct BatchKey {
        Primitive* primitive;
        GLuint     texture;
        GLuint     normal_map;
        glm::vec2  uv_scale;
        float      metallic;
        float      roughness;
        glm::vec3  emissive;
        bool operator==(const BatchKey& o) const {
            return primitive == o.primitive && texture == o.texture
                && normal_map == o.normal_map && uv_scale == o.uv_scale
                && metallic == o.metallic && roughness == o.roughness
                && emissive == o.emissive;
        }
    };
    struct BatchKeyHash {
        size_t operator()(const BatchKey& k) const noexcept {
            auto mix = [](size_t a, size_t b) {
                return a ^ (b + 0x9e3779b9 + (a << 6) + (a >> 2));
            };
            size_t h = std::hash<void*>()((void*)k.primitive);
            h = mix(h, std::hash<GLuint>()(k.texture));
            h = mix(h, std::hash<GLuint>()(k.normal_map));
            h = mix(h, std::hash<float>()(k.uv_scale.x));
            h = mix(h, std::hash<float>()(k.uv_scale.y));
            h = mix(h, std::hash<float>()(k.metallic));
            h = mix(h, std::hash<float>()(k.roughness));
            h = mix(h, std::hash<float>()(k.emissive.x));
            h = mix(h, std::hash<float>()(k.emissive.y));
            h = mix(h, std::hash<float>()(k.emissive.z));
            return h;
        }
    };
    struct CullingBatch {
        BatchKey key{};
        std::vector<glm::mat4> modelMatrices;
    };

    using CullingResult = std::vector<CullingBatch>;

    CullingResult CullObjectsSubset(
        const std::vector<GameObject*>& objects,
        size_t startIdx,
        size_t endIdx,
        const glm::mat4& viewProjection
    );

    void MergeCullingResults(
        const std::vector<CullingResult>& results,
        std::unordered_map<BatchKey, std::vector<glm::mat4>, BatchKeyHash>& outBatches
    );

    static bool CullSphere(const glm::mat4& vp,
                           const glm::vec3& center,
                           float radius);



   
protected:
    void InitializeShader();
    void RenderScene();
    void RenderSceneInstanced();
    void RenderOverlay();
    void RenderOverlayTestPoint();
    void InitOverlay();                    

    static void FramebufferSizeCallback(GLFWwindow* window, int width, int height);

    int width_, height_;
    GLFWwindow* window_;
    float last_time_ = 0.0f;
    
    // Camera
    glm::vec3 camera_position_ = glm::vec3(0, 5, 10);
    glm::vec3 camera_target_   = glm::vec3(0, 0, 0);
    glm::vec3 camera_up_       = glm::vec3(0, 1, 0);
    
    // Rendering

   
    GLuint shader_program_ = 0;
    std::vector<std::shared_ptr<GameObject>> game_objects_;

    // Overlay VAO/VBO
    GLuint overlay_vao = 0;                 
    GLuint overlay_vbo = 0;                  
    GLuint overlay_program = 0;
    
    // Performance stats
    static constexpr int PERF_HISTORY = 300;
    float perf_times[PERF_HISTORY] = {0};
    int perf_pos = 0;
    int perf_count = 0;

    // Overlay toggle
    bool show_overlay_ = true;

    // Projection matrix for overlay
    glm::mat4 ortho2D;

    // Font renderer
    Font font_;

    // Persistent settings
    Settings settings_;

    // Audio engine
    Audio audio_;

    // Agent API server
    AgentAPI agentApi_;
    std::unordered_map<std::string, std::shared_ptr<GameObject>> namedObjects_;
    std::unordered_map<std::string, json::Value> namedObjectCreationParams_;
    glm::vec3 clear_color_ = glm::vec3(0.1f);

    // Physics
    phys::World physics_;
    std::unordered_map<std::string, int> bodyHandleByName_;
    void SyncPhysicsWriteback();

    // Sky / sun
    glm::vec3 sun_dir_    = glm::normalize(glm::vec3(-0.35f, 0.9f, -0.25f));
    glm::vec3 sun_color_  = glm::vec3(1.4f, 1.25f, 1.0f);
    glm::vec3 sky_color_  = glm::vec3(0.55f, 0.68f, 0.85f);
    glm::vec3 zenith_color_  = glm::vec3(0.35f, 0.5f, 0.78f);
    glm::vec3 horizon_color_ = glm::vec3(0.75f, 0.82f, 0.92f);
    glm::vec3 ground_color_  = glm::vec3(0.22f, 0.20f, 0.18f);
    GLuint sky_program_ = 0;
    GLuint sky_vao_ = 0;
    GLuint sky_vbo_ = 0;
    void InitSky();
    void RenderSky(const glm::mat4& view, const glm::mat4& proj);

    // Water
    bool  water_enabled_    = false;
    float water_level_      = 0.0f;
    float water_amplitude_  = 0.3f;
    float water_wavelength_ = 8.0f;
    float water_speed_      = 1.0f;
    float water_size_       = 120.0f;
    glm::vec3 water_color_shallow_ = glm::vec3(0.25f, 0.55f, 0.6f);
    glm::vec3 water_color_deep_    = glm::vec3(0.02f, 0.12f, 0.22f);
    GLuint water_program_ = 0;
    GLuint water_vao_ = 0;
    GLuint water_vbo_ = 0;
    GLuint water_ebo_ = 0;
    int    water_index_count_ = 0;
    void InitWater();
    void RebuildWaterMesh(int resolution);
    void RenderWater(const glm::mat4& view, const glm::mat4& proj);

    // Secondary water patch — visual-only, localized, at arbitrary height.
    // Shares the primary water shader but has its own mesh + per-patch params.
    struct SecWater {
        bool enabled = false;
        glm::vec3 center = glm::vec3(0);
        float size = 40.0f;
        float level = 5.0f;
        float amplitude = 0.08f;
        float wavelength = 5.0f;
        float speed = 0.6f;
        glm::vec3 shallow = glm::vec3(0.35f, 0.55f, 0.6f);
        glm::vec3 deep    = glm::vec3(0.05f, 0.15f, 0.22f);
        GLuint vao = 0, vbo = 0, ebo = 0;
        int idx_count = 0;
    };
    SecWater sec_water_;
    void RebuildSecWaterMesh(int resolution);
    void RenderSecWater(const glm::mat4& view, const glm::mat4& proj);

    // Weather — drives wave amplitude, choppiness, sky darkening, current
    float wind_speed_     = 0.0f;
    glm::vec2 wind_dir_xz_ = glm::vec2(1.0f, 0.0f);  // normalized XZ
    float storminess_     = 0.0f;  // 0 calm, 1 gale
    float AmpScale() const   { return 1.0f + storminess_ * 2.0f; }
    float ChopScale() const  { return 1.0f + storminess_ * 1.5f; }

    // Shadow map (directional sun, single cascade)
    static constexpr int SHADOW_SIZE = 2048;
    GLuint shadow_fbo_     = 0;
    GLuint shadow_map_     = 0;
    GLuint shadow_program_ = 0;
    glm::mat4 light_vp_    = glm::mat4(1.0f);
    float shadow_extent_   = 50.0f;  // half-width of ortho frustum
    void InitShadowMap();
    void RenderShadowPass();
    glm::mat4 ComputeLightVP() const;

    // HDR + post-process pipeline
    static constexpr int MSAA_SAMPLES = 4;
    GLuint hdr_fbo_ms_     = 0;  // multisample render target for scene
    GLuint hdr_color_ms_   = 0;
    GLuint hdr_depth_rb_   = 0;
    GLuint hdr_fbo_        = 0;  // resolved single-sample target (read for bloom/composite)
    GLuint hdr_color_      = 0;
    int    post_w_ = 0, post_h_ = 0;   // current FBO size, recreate on mismatch
    GLuint bloom_fbo_[2]   = {0, 0};
    GLuint bloom_tex_[2]   = {0, 0};
    int    bloom_w_ = 0, bloom_h_ = 0;
    GLuint bloom_extract_program_ = 0;
    GLuint bloom_blur_program_    = 0;
    GLuint composite_program_     = 0;
    GLuint fullscreen_vao_ = 0;
    // Look controls
    float exposure_      = 1.0f;
    float bloom_threshold_ = 1.2f;
    float bloom_intensity_ = 0.8f;
    float fog_density_   = 0.0025f;
    void InitPostFBOs();
    void EnsurePostFBOs();
    void InitPostShaders();
    void RenderBloom();
    void CompositeToScreen();
    std::string agentLog_;
    float agentLogTimer_ = 0.0f;
    void processAgentCommands();
    std::string processAgentCommand(const json::Value& cmd);
public:
    std::string saveWorldToFile(const std::string& path);
    std::string loadWorldFromFile(const std::string& path);



};


// Primitive base class
class Primitive {
public:
    virtual ~Primitive() = default;
    virtual void Render() = 0;
    virtual void Bind() = 0; 
    virtual void Cleanup() = 0;

    virtual GLenum GetDrawMode() const { return GL_TRIANGLES; }
    virtual GLsizei GetIndexCount() const { return index_count_; }
    virtual GLsizei GetVertexCount() const { return vertex_count_; }
    virtual void Bind() const { glBindVertexArray(vao_); }
    float GetIntrinsicRadius() const { return intrinsic_radius_; }


protected:
    GLuint vao_ = 0;
    GLuint vbo_ = 0;
    GLuint ebo_ = 0;
    GLsizei index_count_ = 0;
    GLsizei vertex_count_ = 0;
    GLenum draw_mode_ = GL_TRIANGLES;
    glm::vec3 color_;
    float intrinsic_radius_ = 1.732f;  // default: unit-cube corner-to-center
};

// GameObject class
class GameObject {
public:
    GameObject(std::shared_ptr<Primitive> prim, const glm::vec3& pos);
    // Add:
    Primitive* GetPrimitive() const { return primitive_.get(); }
    float GetBoundingRadius() const;

    glm::vec3 GetPosition() const { return position_; }
    glm::vec3 GetRotation() const { return rotation_; }
    glm::vec3 GetScale() const { return scale_; }
    glm::mat4 GetModelMatrix() const;
    void Render();
    void Cleanup();

    void SetPosition(const glm::vec3& p) { position_ = p; }
    void SetRotation(const glm::vec3& r) { rotation_ = r; }
    void SetScale(const glm::vec3& s) { scale_ = s; }

    void SetTexture(GLuint tex) { texture_ = tex; }
    GLuint GetTexture() const { return texture_; }
    void SetUVScale(const glm::vec2& s) { uv_scale_ = s; }
    glm::vec2 GetUVScale() const { return uv_scale_; }

    void SetNormalMap(GLuint tex) { normal_map_ = tex; }
    GLuint GetNormalMap() const { return normal_map_; }
    void SetMetallic(float v)   { metallic_ = v; }
    float GetMetallic() const   { return metallic_; }
    void SetRoughness(float v)  { roughness_ = std::max(0.02f, v); }
    float GetRoughness() const  { return roughness_; }
    void SetEmissive(const glm::vec3& v) { emissive_ = v; }
    glm::vec3 GetEmissive() const { return emissive_; }

private:
    std::shared_ptr<Primitive> primitive_;
    glm::vec3 position_;
    glm::vec3 rotation_;
    glm::vec3 scale_{1.0f};
    GLuint    texture_ = 0;
    GLuint    normal_map_ = 0;
    glm::vec2 uv_scale_{1.0f};
    float     metallic_   = 0.0f;
    float     roughness_  = 0.7f;
    glm::vec3 emissive_   = glm::vec3(0.0f);
};
