#pragma once

#include <vector>
#include <unordered_map>
#include <memory>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <thread>
#include <algorithm>
#include <random>

#include <GL/glew.h>
#include <glm/glm.hpp>

// world dimensions
static constexpr int CHUNK_SIZE   = 16;
static constexpr int CHUNK_HEIGHT = 64;

struct ChunkKey {
    int x,z;
    bool operator==(ChunkKey const& o) const { return x==o.x && z==o.z; }
};
struct ChunkKeyHash {
    std::size_t operator()(ChunkKey const& k) const noexcept {
        return std::hash<long long>()(((long long)k.x<<32)|(unsigned)k.z);
    }
};

struct Chunk {
    int x,z;
    // voxels[x][y][z], y ∈ [0,CHUNK_HEIGHT)
    std::vector<std::vector<std::vector<int>>> voxels;
};

struct ChunkMesh {
    GLuint vao,vbo,ibo;
    GLsizei indexCount;
};

struct Vertex {
    glm::vec3 pos;
    glm::vec3 normal;
    glm::vec2 uv;
};

struct RawMesh {
    ChunkKey key;
    std::vector<Vertex> verts;
    std::vector<uint32_t> indices;
};

class VoxelGame {
public:
    VoxelGame();
    ~VoxelGame();

    // initialize with screen size (for cascades)
    bool init(int screenW,int screenH);
    void update(float dt, glm::vec3 const& camPos);
    void render(glm::mat4 const& view, glm::mat4 const& proj);
    void debugUI();

private:
    // chunk generation
    Chunk generateChunk(int cx,int cz);
    void updateChunks(glm::vec3 const& camPos);

    // CPU meshing
    void workerLoop();
    void greedyMesh(Chunk const&, std::vector<Vertex>&, std::vector<uint32_t>&);
    void destroyMesh(ChunkMesh*);

    std::unordered_map<ChunkKey,Chunk,ChunkKeyHash> chunks;
    std::unordered_map<ChunkKey,std::unique_ptr<ChunkMesh>,ChunkKeyHash> meshes;

    std::mutex queueMutex;
    std::condition_variable queueCV;
    std::queue<ChunkKey> toBuild;
    std::queue<RawMesh>  readyToUpload;
    bool stopWorkers=false;
    std::vector<std::thread> workers;

    // stats
    int totalChunksLoaded=0, totalChunksEverLoaded=0;
    int chunkGenCount=0;
    double totalChunkGenTime=0, lastChunkGenTime=0;
    float fps=0, lastDeltaTime=0;
    double frameTimeSum=0; int frameCount=0;
    int lastFaceCount=0, lastGLCalls=0;

    // textures
    GLuint textureDirt, textureGrass, textureWood;

    // cascaded shadow maps
    static constexpr int NUM_CASCADES = 3;
    static constexpr int SHADOW_MAP_SIZE = 1024;
    GLuint depthFBO[NUM_CASCADES];
    GLuint depthTex[NUM_CASCADES];
    float cascadeSplits[NUM_CASCADES];
    glm::mat4 lightSpaceMat[NUM_CASCADES];

    // shaders
    GLuint depthProg, mainProg;

    // light & camera
    glm::vec3 lightDir = glm::normalize(glm::vec3(1.0f, -1.0f, 0.5f));
    glm::vec3 cameraPos;

    int screenW, screenH;
    int renderDistance = 2;

    bool loadTextures();
    bool loadShaders();
    void initShadowMaps();
    void renderScene(GLuint prog);
};
