// main.cpp
//
// A physically based renderer that demonstrates height mapping via parallax,
// improved lighting (point & directional lights), and renders a skybox for background.
// It uses procedural spheres and a plane. Materials (including height maps) can be loaded
// and saved via YAML (using yaml-cpp) and are editable via ImGui.
// 
// Compile (on Linux):
//   g++ main.cpp -lglfw -lGLEW -lGL -ldl -limgui -lyaml-cpp -o pbr_renderer
//
// Adjust library paths and names as needed.

#include <GL/glew.h>
#include <GLFW/glfw3.h>

#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>

#include <fstream>
#include <iostream>
#include <vector>
#include <cmath>
#include <cstring>
#include <string>
#include <sstream>

// ImGui includes.
#include "imgui.h"
#include "imgui_internal.h" // For IM_PI
#include "imgui_impl_glfw.h"
#include "imgui_impl_opengl3.h"

// stb_image for texture loading.
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

// yaml-cpp for YAML material loading/saving.
#include <yaml-cpp/yaml.h>

// ------------------------------------------
// Shader sources
// ------------------------------------------

// Vertex Shader (common to objects)
const char* vertexShaderSource = R"(
#version 330 core
layout (location = 0) in vec3 aPos;      // position
layout (location = 1) in vec3 aNormal;   // normal
layout (location = 2) in vec2 aTexCoords; // texture coordinates
layout (location = 3) in vec3 aTangent;   // tangent

out vec3 WorldPos;
out vec3 Normal;
out vec2 TexCoords;
out vec3 Tangent;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main(){
    WorldPos = vec3(model * vec4(aPos, 1.0));
    Normal = mat3(transpose(inverse(model))) * aNormal;
    Tangent = mat3(model) * aTangent; // simple transform (ignore scale issues)
    TexCoords = aTexCoords;
    gl_Position = projection * view * vec4(WorldPos, 1.0);
}
)";

// Fragment Shader with parallax (height) mapping and two light types.
const char* fragmentShaderSource = R"(#version 330 core
out vec4 FragColor;

in vec3 WorldPos;
in vec3 Normal;
in vec2 TexCoords;
in vec3 Tangent;

uniform vec3 camPos;

// Base material uniforms.
uniform vec3 albedo;
uniform float metallic;
uniform float roughness;
uniform float ao;

// Texture samplers & usage flags.
uniform bool useAlbedoMap;
uniform bool useMetallicMap;
uniform bool useRoughnessMap;
uniform bool useAOMap;
uniform bool useNormalMap;
uniform bool useHeightMap;     // Height map flag

uniform sampler2D albedoMap;
uniform sampler2D metallicMap;
uniform sampler2D roughnessMap;
uniform sampler2D aoMap;
uniform sampler2D normalMap;
uniform sampler2D heightMap;   // Height map sampler

// Parallax mapping scale factor.
uniform float parallaxScale;

// Use ImGui's internal PI constant.
uniform float PI;

// Point light uniforms.
uniform vec3 pointLightPos;
uniform vec3 pointLightColor;

// Directional light uniforms.
uniform bool useDirLight;
uniform vec3 dirLightDir;
uniform vec3 dirLightColor;

// NEW: Environment reflection uniforms.
uniform samplerCube envMap;
uniform float envReflectionIntensity;

//
// Helper functions for PBR
//
vec3 fresnelSchlick(float cosTheta, vec3 F0) {
    return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
}

float DistributionGGX(vec3 N, vec3 H, float roughness) {
    float a      = roughness * roughness;
    float a2     = a * a;
    float NdotH  = max(dot(N, H), 0.0);
    float NdotH2 = NdotH * NdotH;
    float denom  = (NdotH2 * (a2 - 1.0) + 1.0);
    denom        = PI * denom * denom;
    return a2 / max(denom, 0.001);
}

float GeometrySchlickGGX(float NdotV, float roughness) {
    float r = roughness + 1.0;
    float k = (r * r) / 8.0;
    return NdotV / (NdotV * (1.0 - k) + k);
}

float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness) {
    float NdotV = max(dot(N, V), 0.0);
    float NdotL = max(dot(N, L), 0.0);
    float ggx1  = GeometrySchlickGGX(NdotV, roughness);
    float ggx2  = GeometrySchlickGGX(NdotL, roughness);
    return ggx1 * ggx2;
}

void main(){
    // Start with the input geometry normal.
    vec3 Nn = normalize(Normal);
    
    // Compute the TBN matrix if using normal or height maps.
    mat3 TBN = mat3(1.0);
    if (useNormalMap || useHeightMap) {
        vec3 T = normalize(Tangent);
        vec3 B = normalize(cross(Normal, T));
        TBN = mat3(T, B, Normal);
    }
    
    // Compute modified texture coordinates (parallax mapping) if height mapping is enabled.
    vec2 texCoordsModified = TexCoords;
    if (useHeightMap) {
        // Transform view direction into tangent space.
        vec3 viewDirT = normalize(TBN * (camPos - WorldPos));
        // Sample the height value.
        float heightValue = texture(heightMap, TexCoords).r;
        // Offset the texture coordinates.
        texCoordsModified = TexCoords - viewDirT.xy * (heightValue * parallaxScale);
    }
    
    // Apply normal mapping if enabled.
    if (useNormalMap) {
        vec3 tangentNormal = texture(normalMap, texCoordsModified).rgb;
        tangentNormal = tangentNormal * 2.0 - 1.0;
        Nn = normalize(TBN * tangentNormal);
    }
    
    vec3 V = normalize(camPos - WorldPos);
    
    // Sample material textures using modified texture coordinates.
    vec3 albedoValue = albedo;
    if (useAlbedoMap)
        albedoValue = texture(albedoMap, texCoordsModified).rgb;
    
    float metallicValue = metallic;
    if (useMetallicMap)
        metallicValue = texture(metallicMap, texCoordsModified).r;
    
    float roughnessValue = roughness;
    if (useRoughnessMap)
        roughnessValue = texture(roughnessMap, texCoordsModified).r;
    
    float aoValue = ao;
    if (useAOMap)
        aoValue = texture(aoMap, texCoordsModified).r;
    
    // Calculate reflectance at normal incidence.
    vec3 F0 = vec3(0.04);
    F0 = mix(F0, albedoValue, metallicValue);
    
    // --- Compute point light contribution ---
    vec3 Lp = normalize(pointLightPos - WorldPos);
    vec3 Hp = normalize(V + Lp);
    float NDFp = DistributionGGX(Nn, Hp, roughnessValue);
    float Gp = GeometrySmith(Nn, V, Lp, roughnessValue);
    vec3 Fp = fresnelSchlick(max(dot(Hp, V), 0.0), F0);
    vec3 numeratorP = NDFp * Gp * Fp;
    float denominatorP = 4.0 * max(dot(Nn, V), 0.0) * max(dot(Nn, Lp), 0.0) + 0.001;
    vec3 specularP = numeratorP / denominatorP;
    
    vec3 kS = Fp;
    vec3 kD = vec3(1.0) - kS;
    kD *= 1.0 - metallicValue;
    float NdotLp = max(dot(Nn, Lp), 0.0);
    vec3 irradianceP = pointLightColor * NdotLp;
    vec3 diffuseP = (albedoValue / PI);
    vec3 lightingPoint = (kD * diffuseP + specularP) * irradianceP;
    
    // --- Compute directional light contribution ---
    vec3 lightingDir = vec3(0.0);
    if (useDirLight) {
        vec3 Ld = normalize(-dirLightDir);
        vec3 Hd = normalize(V + Ld);
        float NDFd = DistributionGGX(Nn, Hd, roughnessValue);
        float Gd = GeometrySmith(Nn, V, Ld, roughnessValue);
        vec3 Fd = fresnelSchlick(max(dot(Hd, V), 0.0), F0);
        vec3 numeratorD = NDFd * Gd * Fd;
        float denominatorD = 4.0 * max(dot(Nn, V), 0.0) * max(dot(Nn, Ld), 0.0) + 0.001;
        vec3 specularD = numeratorD / denominatorD;
        vec3 kS_d = Fd;
        vec3 kD_d = vec3(1.0) - kS_d;
        kD_d *= 1.0 - metallicValue;
        float NdotLd = max(dot(Nn, Ld), 0.0);
        vec3 irradianceD = dirLightColor * NdotLd;
        vec3 diffuseD = (albedoValue / PI);
        lightingDir = (kD_d * diffuseD + specularD) * irradianceD;
    }
    
    // Sum direct lighting contributions.
    vec3 lighting = lightingPoint + lightingDir;
    
    // --- Compute environment reflection contribution ---
    // Calculate reflection vector and sample cubemap.
    vec3 R = reflect(-V, Nn);
    vec3 envSpec = texture(envMap, R).rgb;
    lighting += envSpec * envReflectionIntensity;
    
    // Add a simple ambient term.
    vec3 ambient = vec3(0.03) * albedoValue * aoValue;
    lighting += ambient;
    
    // Tone mapping and gamma correction.
    lighting = lighting / (lighting + vec3(1.0));
    lighting = pow(lighting, vec3(1.0/2.2));
    
    FragColor = vec4(lighting, 1.0);
}

)";

// Skybox shaders
const char* skyboxVertexShaderSource = R"(
#version 330 core
layout (location = 0) in vec3 aPos;
out vec3 TexCoords;

uniform mat4 view;
uniform mat4 projection;
void main(){
    TexCoords = aPos;
    vec4 pos = projection * view * vec4(aPos, 1.0);
    gl_Position = pos.xyww; // set w component to 1.0 to always pass depth test
}
)";

const char* skyboxFragmentShaderSource = R"(
#version 330 core
out vec4 FragColor;
in vec3 TexCoords;
uniform samplerCube skybox;
void main(){
    FragColor = texture(skybox, TexCoords);
}
)";

// ------------------------------------------
// Helper functions
// ------------------------------------------
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 infoLog[512];
        glGetShaderInfoLog(shader,512,nullptr,infoLog);
        std::cerr << "Shader compile error:\n" << infoLog << std::endl;
    }
    return shader;
}

GLuint createProgram(GLuint vs, GLuint fs) {
    GLuint program = glCreateProgram();
    glAttachShader(program, vs);
    glAttachShader(program, fs);
    glLinkProgram(program);
    GLint success;
    glGetProgramiv(program, GL_LINK_STATUS, &success);
    if(!success) {
        char infoLog[512];
        glGetProgramInfoLog(program,512,nullptr,infoLog);
        std::cerr << "Program link error:\n" << infoLog << std::endl;
    }
    return program;
}

GLuint LoadTexture(const char* path) {
    int width, height, nrChannels;
    stbi_set_flip_vertically_on_load(true);
    unsigned char* data = stbi_load(path, &width, &height, &nrChannels, 0);
    if(!data) {
        std::cerr << "Failed to load texture: " << path << std::endl;
        return 0;
    }
    GLenum format;
    if(nrChannels == 1)
        format = GL_RED;
    else if(nrChannels == 3)
        format = GL_RGB;
    else if(nrChannels == 4)
        format = GL_RGBA;
    GLuint texID;
    glGenTextures(1, &texID);
    glBindTexture(GL_TEXTURE_2D, texID);
    glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
    glGenerateMipmap(GL_TEXTURE_2D);
    // Set texture parameters.
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);	
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    stbi_image_free(data);
    return texID;
}

// Load a cubemap (skybox) from 6 images. Expect faces in order: right, left, top, bottom, front, back.
GLuint loadCubemap(std::vector<std::string> faces) {
    GLuint textureID;
    glGenTextures(1, &textureID);
    glBindTexture(GL_TEXTURE_CUBE_MAP, textureID);
    int width, height, nrChannels;
    for(unsigned int i=0; i<faces.size(); i++){
        unsigned char* data = stbi_load(faces[i].c_str(), &width, &height, &nrChannels, 0);
        if(data){
            GLenum format;
            if(nrChannels == 1)
                format = GL_RED;
            else if(nrChannels == 3)
                format = GL_RGB;
            else if(nrChannels == 4)
                format = GL_RGBA;
            glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 
                         0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
            stbi_image_free(data);
        } else {
            std::cerr << "Cubemap texture failed to load at path: " << faces[i] << std::endl;
            stbi_image_free(data);
        }
    }
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);  
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);  
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
    return textureID;
}

// Generate a sphere mesh with per-vertex: position (3), normal (3), texcoords (2), tangent (3)
// (Total 11 floats per vertex)
void generateSphereMesh(std::vector<float>& vertices, std::vector<unsigned int>& indices, unsigned int X_SEGMENTS = 64, unsigned int Y_SEGMENTS = 64) {
    vertices.clear();
    indices.clear();
    for (unsigned int y = 0; y <= Y_SEGMENTS; ++y) {
        for (unsigned int x = 0; x <= X_SEGMENTS; ++x) {
            float xSegment = (float)x / (float)X_SEGMENTS;
            float ySegment = (float)y / (float)Y_SEGMENTS;
            float xPos = std::cos(xSegment * 2.0f * IM_PI) * std::sin(ySegment * IM_PI);
            float yPos = std::cos(ySegment * IM_PI);
            float zPos = std::sin(xSegment * 2.0f * IM_PI) * std::sin(ySegment * IM_PI);
            // Position.
            vertices.push_back(xPos);
            vertices.push_back(yPos);
            vertices.push_back(zPos);
            // Normal (for a unit sphere, same as position).
            vertices.push_back(xPos);
            vertices.push_back(yPos);
            vertices.push_back(zPos);
            // Texcoords.
            vertices.push_back(xSegment);
            vertices.push_back(ySegment);
            // Tangent: approximate (derivative with respect to u).
            float tangentX = -std::sin(xSegment * 2.0f * IM_PI);
            float tangentY = 0.0f;
            float tangentZ = std::cos(xSegment * 2.0f * IM_PI);
            vertices.push_back(tangentX);
            vertices.push_back(tangentY);
            vertices.push_back(tangentZ);
        }
    }
    for (unsigned int y = 0; y < Y_SEGMENTS; ++y) {
        for (unsigned int x = 0; x < X_SEGMENTS; ++x) {
            unsigned int i0 = y * (X_SEGMENTS + 1) + x;
            unsigned int i1 = i0 + 1;
            unsigned int i2 = i0 + (X_SEGMENTS + 1);
            unsigned int i3 = i2 + 1;
            indices.push_back(i0);
            indices.push_back(i2);
            indices.push_back(i1);
            indices.push_back(i1);
            indices.push_back(i2);
            indices.push_back(i3);
        }
    }
}

// Generate a plane mesh (quad) in the XZ plane centered at origin.
// The plane spans [-1,1] in X and Z, y=0.
void generatePlaneMesh(std::vector<float>& vertices, std::vector<unsigned int>& indices) {
    // 4 vertices: position (3), normal (3), texcoords (2), tangent (3) = 11 floats.
    float planeVerts[] = {
        // positions         // normals         // texcoords  // tangent
        -1.0f, 0.0f,  1.0f,   0.0f, 1.0f, 0.0f,   0.0f, 1.0f,   1.0f, 0.0f, 0.0f,
         1.0f, 0.0f,  1.0f,   0.0f, 1.0f, 0.0f,   1.0f, 1.0f,   1.0f, 0.0f, 0.0f,
         1.0f, 0.0f, -1.0f,   0.0f, 1.0f, 0.0f,   1.0f, 0.0f,   1.0f, 0.0f, 0.0f,
        -1.0f, 0.0f, -1.0f,   0.0f, 1.0f, 0.0f,   0.0f, 0.0f,   1.0f, 0.0f, 0.0f,
    };
    unsigned int planeIndices[] = {
        0, 1, 2,
        0, 2, 3
    };
    vertices.assign(planeVerts, planeVerts + sizeof(planeVerts) / sizeof(float));
    indices.assign(planeIndices, planeIndices + sizeof(planeIndices)/sizeof(unsigned int));
}

// ------------------------------------------
// Material and Object Structures
// ------------------------------------------
// Extend Material to include height map.
struct Material {
    glm::vec3 albedo;
    float metallic;
    float roughness;
    float ao;
    
    // Texture IDs.
    GLuint albedoTex;
    GLuint metallicTex;
    GLuint roughnessTex;
    GLuint aoTex;
    GLuint normalTex;
    GLuint heightTex;    // new height map texture.
    
    // Flags indicating texture usage.
    bool useAlbedoMap;
    bool useMetallicMap;
    bool useRoughnessMap;
    bool useAOMap;
    bool useNormalMap;
    bool useHeightMap;   // flag for height map
    
    // File path buffers.
    char albedoPath[256];
    char metallicPath[256];
    char roughnessPath[256];
    char aoPath[256];
    char normalPath[256];
    char heightPath[256];  // file path for height map.
    
    Material() : albedo(0.5f, 0.0f, 0.0f), metallic(0.0f), roughness(0.5f), ao(1.0f),
                 albedoTex(0), metallicTex(0), roughnessTex(0), aoTex(0), normalTex(0), heightTex(0),
                 useAlbedoMap(false), useMetallicMap(false), useRoughnessMap(false),
                 useAOMap(false), useNormalMap(false), useHeightMap(false)
    {
        strcpy(albedoPath, "");
        strcpy(metallicPath, "");
        strcpy(roughnessPath, "");
        strcpy(aoPath, "");
        strcpy(normalPath, "");
        strcpy(heightPath, "");
    }
};

struct SphereInstance {
    glm::vec3 position;
    float rotation; // in radians.
    Material material;
};

struct PlaneInstance {
    glm::vec3 position;
    float rotation;  // around Y axis.
    Material material;
};

// ------------------------------------------
// YAML Material Loading and Saving
// ------------------------------------------
Material loadMaterialFromYAML(const std::string& filename) {
    Material mat;
    try {
        YAML::Node config = YAML::LoadFile(filename);
        if (config["albedo"]) {
            mat.albedo = glm::vec3(config["albedo"][0].as<float>(),
                                   config["albedo"][1].as<float>(),
                                   config["albedo"][2].as<float>());
        }
        if (config["metallic"])
            mat.metallic = config["metallic"].as<float>();
        if (config["roughness"])
            mat.roughness = config["roughness"].as<float>();
        if (config["ao"])
            mat.ao = config["ao"].as<float>();
        
        if (config["albedo_texture"]) {
            std::string path = config["albedo_texture"].as<std::string>();
            strncpy(mat.albedoPath, path.c_str(), sizeof(mat.albedoPath));
            GLuint tex = LoadTexture(mat.albedoPath);
            if (tex != 0) { mat.albedoTex = tex; mat.useAlbedoMap = true; }
        }
        if (config["metallic_texture"]) {
            std::string path = config["metallic_texture"].as<std::string>();
            strncpy(mat.metallicPath, path.c_str(), sizeof(mat.metallicPath));
            GLuint tex = LoadTexture(mat.metallicPath);
            if (tex != 0) { mat.metallicTex = tex; mat.useMetallicMap = true; }
        }
        if (config["roughness_texture"]) {
            std::string path = config["roughness_texture"].as<std::string>();
            strncpy(mat.roughnessPath, path.c_str(), sizeof(mat.roughnessPath));
            GLuint tex = LoadTexture(mat.roughnessPath);
            if (tex != 0) { mat.roughnessTex = tex; mat.useRoughnessMap = true; }
        }
        if (config["ao_texture"]) {
            std::string path = config["ao_texture"].as<std::string>();
            strncpy(mat.aoPath, path.c_str(), sizeof(mat.aoPath));
            GLuint tex = LoadTexture(mat.aoPath);
            if (tex != 0) { mat.aoTex = tex; mat.useAOMap = true; }
        }
        if (config["normal_texture"]) {
            std::string path = config["normal_texture"].as<std::string>();
            strncpy(mat.normalPath, path.c_str(), sizeof(mat.normalPath));
            GLuint tex = LoadTexture(mat.normalPath);
            if (tex != 0) { mat.normalTex = tex; mat.useNormalMap = true; }
        }
        if (config["height_texture"]) {
            std::string path = config["height_texture"].as<std::string>();
            strncpy(mat.heightPath, path.c_str(), sizeof(mat.heightPath));
            GLuint tex = LoadTexture(mat.heightPath);
            if (tex != 0) { mat.heightTex = tex; mat.useHeightMap = true; }
        }
    } catch(const std::exception& e) {
        std::cerr << "Error loading YAML material from " << filename << ": " << e.what() << std::endl;
    }
    return mat;
}

bool saveMaterialToYAML(const std::string& filename, const Material& mat) {
    YAML::Emitter out;
    out << YAML::BeginMap;
    out << YAML::Key << "albedo" << YAML::Value << YAML::Flow << std::vector<float>{mat.albedo.r, mat.albedo.g, mat.albedo.b};
    out << YAML::Key << "metallic" << YAML::Value << mat.metallic;
    out << YAML::Key << "roughness" << YAML::Value << mat.roughness;
    out << YAML::Key << "ao" << YAML::Value << mat.ao;
    if (mat.useAlbedoMap)
        out << YAML::Key << "albedo_texture" << YAML::Value << std::string(mat.albedoPath);
    if (mat.useMetallicMap)
        out << YAML::Key << "metallic_texture" << YAML::Value << std::string(mat.metallicPath);
    if (mat.useRoughnessMap)
        out << YAML::Key << "roughness_texture" << YAML::Value << std::string(mat.roughnessPath);
    if (mat.useAOMap)
        out << YAML::Key << "ao_texture" << YAML::Value << std::string(mat.aoPath);
    if (mat.useNormalMap)
        out << YAML::Key << "normal_texture" << YAML::Value << std::string(mat.normalPath);
    if (mat.useHeightMap)
        out << YAML::Key << "height_texture" << YAML::Value << std::string(mat.heightPath);
    out << YAML::EndMap;
    
    std::ofstream fout(filename);
    if(!fout.is_open()){
        std::cerr << "Failed to open " << filename << " for saving material." << std::endl;
        return false;
    }
    fout << out.c_str();
    fout.close();
    return true;
}

// ------------------------------------------
// Skybox geometry
// ------------------------------------------
// Cube vertices for the skybox.
float skyboxVertices[] = {
    // positions          
    -1.0f,  1.0f, -1.0f, 
    -1.0f, -1.0f, -1.0f,  
     1.0f, -1.0f, -1.0f,  
     1.0f, -1.0f, -1.0f,  
     1.0f,  1.0f, -1.0f,  
    -1.0f,  1.0f, -1.0f,  

    -1.0f, -1.0f,  1.0f, 
    -1.0f, -1.0f, -1.0f, 
    -1.0f,  1.0f, -1.0f, 
    -1.0f,  1.0f, -1.0f, 
    -1.0f,  1.0f,  1.0f, 
    -1.0f, -1.0f,  1.0f, 

     1.0f, -1.0f, -1.0f, 
     1.0f, -1.0f,  1.0f, 
     1.0f,  1.0f,  1.0f, 
     1.0f,  1.0f,  1.0f, 
     1.0f,  1.0f, -1.0f, 
     1.0f, -1.0f, -1.0f, 

    -1.0f, -1.0f,  1.0f, 
    -1.0f,  1.0f,  1.0f, 
     1.0f,  1.0f,  1.0f, 
     1.0f,  1.0f,  1.0f, 
     1.0f, -1.0f,  1.0f, 
    -1.0f, -1.0f,  1.0f, 

    -1.0f,  1.0f, -1.0f, 
     1.0f,  1.0f, -1.0f, 
     1.0f,  1.0f,  1.0f, 
     1.0f,  1.0f,  1.0f, 
    -1.0f,  1.0f,  1.0f, 
    -1.0f,  1.0f, -1.0f, 

    -1.0f, -1.0f, -1.0f, 
    -1.0f, -1.0f,  1.0f, 
     1.0f, -1.0f, -1.0f, 
     1.0f, -1.0f, -1.0f, 
    -1.0f, -1.0f,  1.0f, 
     1.0f, -1.0f,  1.0f
};









int main(){
    if(!glfwInit()){
        std::cerr << "Failed to initialize GLFW!" << std::endl;
        return -1;
    }
    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, "PBR Renderer with Height Maps, Improved Lighting & Skybox", nullptr, nullptr);
    if(!window){
        std::cerr << "Failed to create GLFW window!" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);
    
    glewExperimental = GL_TRUE;
    if(glewInit() != GLEW_OK){
        std::cerr << "Failed to initialize GLEW!" << std::endl;
        return -1;
    }
    glEnable(GL_DEPTH_TEST);
    
    // Setup ImGui.
    IMGUI_CHECKVERSION();
    ImGui::CreateContext();
    ImGuiIO& io = ImGui::GetIO(); (void)io;
    ImGui::StyleColorsDark();
    ImGui_ImplGlfw_InitForOpenGL(window, true);
    ImGui_ImplOpenGL3_Init("#version 330");

    // Compile main shaders.
    GLuint vs = compileShader(GL_VERTEX_SHADER, vertexShaderSource);
    GLuint fs = compileShader(GL_FRAGMENT_SHADER, fragmentShaderSource);
    GLuint shaderProgram = createProgram(vs, fs);
    glDeleteShader(vs);
    glDeleteShader(fs);
    
    // Compile skybox shaders.
    GLuint skybox_vs = compileShader(GL_VERTEX_SHADER, skyboxVertexShaderSource);
    GLuint skybox_fs = compileShader(GL_FRAGMENT_SHADER, skyboxFragmentShaderSource);
    GLuint skyboxShader = createProgram(skybox_vs, skybox_fs);
    glDeleteShader(skybox_vs);
    glDeleteShader(skybox_fs);
    
    // Generate sphere mesh.
    std::vector<float> sphereVertices;
    std::vector<unsigned int> sphereIndices;
    generateSphereMesh(sphereVertices, sphereIndices);
    GLuint sphereVAO, sphereVBO, sphereEBO;
    glGenVertexArrays(1, &sphereVAO);
    glGenBuffers(1, &sphereVBO);
    glGenBuffers(1, &sphereEBO);
    glBindVertexArray(sphereVAO);
    glBindBuffer(GL_ARRAY_BUFFER, sphereVBO);
    glBufferData(GL_ARRAY_BUFFER, sphereVertices.size() * sizeof(float), sphereVertices.data(), GL_STATIC_DRAW);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, sphereEBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sphereIndices.size() * sizeof(unsigned int), sphereIndices.data(), GL_STATIC_DRAW);
    // Attributes: pos (3), normal (3), texcoords (2), tangent (3)
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 11 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 11 * sizeof(float), (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(1);
    glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 11 * sizeof(float), (void*)(6 * sizeof(float)));
    glEnableVertexAttribArray(2);
    glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, 11 * sizeof(float), (void*)(8 * sizeof(float)));
    glEnableVertexAttribArray(3);
    glBindVertexArray(0);
    
    // Generate plane mesh.
    std::vector<float> planeVertices;
    std::vector<unsigned int> planeIndices;
    generatePlaneMesh(planeVertices, planeIndices);
    GLuint planeVAO, planeVBO, planeEBO;
    glGenVertexArrays(1, &planeVAO);
    glGenBuffers(1, &planeVBO);
    glGenBuffers(1, &planeEBO);
    glBindVertexArray(planeVAO);
    glBindBuffer(GL_ARRAY_BUFFER, planeVBO);
    glBufferData(GL_ARRAY_BUFFER, planeVertices.size() * sizeof(float), planeVertices.data(), GL_STATIC_DRAW);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, planeEBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, planeIndices.size() * sizeof(unsigned int), planeIndices.data(), GL_STATIC_DRAW);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 11 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 11 * sizeof(float), (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(1);
    glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 11 * sizeof(float), (void*)(6 * sizeof(float)));
    glEnableVertexAttribArray(2);
    glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, 11 * sizeof(float), (void*)(8 * sizeof(float)));
    glEnableVertexAttribArray(3);
    glBindVertexArray(0);
    
    // Set up skybox VAO & VBO.
    GLuint skyboxVAO, skyboxVBO;
    glGenVertexArrays(1, &skyboxVAO);
    glGenBuffers(1, &skyboxVBO);
    glBindVertexArray(skyboxVAO);
    glBindBuffer(GL_ARRAY_BUFFER, skyboxVBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(skyboxVertices), &skyboxVertices, GL_STATIC_DRAW);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    glBindVertexArray(0);
    
    // Load a default cubemap for the skybox.
    std::vector<std::string> faces{
        "skybox/right.jpg",
        "skybox/left.jpg",
        "skybox/top.jpg",
        "skybox/bottom.jpg",
        "skybox/front.jpg",
        "skybox/back.jpg"
    };
    GLuint cubemapTexture = loadCubemap(faces);
    
    // ------------------------------------------
    // Scene Data
    // ------------------------------------------
    std::vector<SphereInstance> spheres;
    spheres.push_back({ glm::vec3(0.0f, 0.0f, 0.0f), 0.0f, Material() });
    std::vector<PlaneInstance> planes;
    planes.push_back({ glm::vec3(0.0f, -1.0f, 0.0f), 0.0f, Material() });
    
    // Global lighting parameters.
    glm::vec3 camPosVec(0.0f, 1.0f, 5.0f);
    glm::vec3 pointLightPos(0.0f, 3.0f, 3.0f);
    glm::vec3 pointLightColor(300.0f, 300.0f, 300.0f);
    // Directional light.
    bool useDirLight = true;
    glm::vec3 dirLightDir(-0.2f, -1.0f, -0.3f);
    glm::vec3 dirLightColor(0.8f, 0.8f, 0.8f);
    
    // Toggle for which object to render: 0 = sphere, 1 = plane.
    int activeObject = 0;
    
    // For material YAML loading/saving.
    static char yamlPathBuffer[256] = "";
    
    // Declare a global parallax scale variable so it is available later.
    static float globalParallaxScale = 0.05f;
    
    // Main Loop.
    while(!glfwWindowShouldClose(window)){
        glfwPollEvents();
        ImGui_ImplOpenGL3_NewFrame();
        ImGui_ImplGlfw_NewFrame();
        ImGui::NewFrame();
        
        // ---------------------- ImGui UI -----------------------
        {
            ImGui::Begin("Scene Controls");
            ImGui::Text("Global Lighting");
            ImGui::DragFloat3("Camera Pos", glm::value_ptr(camPosVec), 0.1f);
            ImGui::DragFloat3("Point Light Pos", glm::value_ptr(pointLightPos), 0.1f);
            ImGui::ColorEdit3("Point Light Color", glm::value_ptr(pointLightColor));
            ImGui::Checkbox("Use Directional Light", &useDirLight);
            ImGui::DragFloat3("Dir Light Dir", glm::value_ptr(dirLightDir), 0.1f);
            ImGui::ColorEdit3("Dir Light Color", glm::value_ptr(dirLightColor));
            ImGui::Separator();
            ImGui::RadioButton("Render Sphere", &activeObject, 0);
            ImGui::RadioButton("Render Plane", &activeObject, 1);
            ImGui::Separator();
            // Material controls for the active object.
            Material* mat = nullptr;
            if(activeObject == 0 && !spheres.empty())
                mat = &spheres[0].material;
            else if(activeObject == 1 && !planes.empty())
                mat = &planes[0].material;
            if(mat){
                ImGui::Text("Material Properties:");
                ImGui::ColorEdit3("Albedo", glm::value_ptr(mat->albedo));
                ImGui::SliderFloat("Metallic", &mat->metallic, 0.0f, 1.0f);
                ImGui::SliderFloat("Roughness", &mat->roughness, 0.05f, 1.0f);
                ImGui::SliderFloat("AO", &mat->ao, 0.0f, 1.0f);
                ImGui::SliderFloat("Parallax Scale", &globalParallaxScale, 0.0f, 0.2f);
                ImGui::InputText("Albedo Texture Path", mat->albedoPath, sizeof(mat->albedoPath));
                if(ImGui::Button("Load Albedo Texture")){
                    GLuint tex = LoadTexture(mat->albedoPath);
                    if(tex != 0){ mat->albedoTex = tex; mat->useAlbedoMap = true; }
                }
                ImGui::InputText("Metallic Texture Path", mat->metallicPath, sizeof(mat->metallicPath));
                if(ImGui::Button("Load Metallic Texture")){
                    GLuint tex = LoadTexture(mat->metallicPath);
                    if(tex != 0){ mat->metallicTex = tex; mat->useMetallicMap = true; }
                }
                ImGui::InputText("Roughness Texture Path", mat->roughnessPath, sizeof(mat->roughnessPath));
                if(ImGui::Button("Load Roughness Texture")){
                    GLuint tex = LoadTexture(mat->roughnessPath);
                    if(tex != 0){ mat->roughnessTex = tex; mat->useRoughnessMap = true; }
                }
                ImGui::InputText("AO Texture Path", mat->aoPath, sizeof(mat->aoPath));
                if(ImGui::Button("Load AO Texture")){
                    GLuint tex = LoadTexture(mat->aoPath);
                    if(tex != 0){ mat->aoTex = tex; mat->useAOMap = true; }
                }
                ImGui::InputText("Normal Texture Path", mat->normalPath, sizeof(mat->normalPath));
                if(ImGui::Button("Load Normal Texture")){
                    GLuint tex = LoadTexture(mat->normalPath);
                    if(tex != 0){ mat->normalTex = tex; mat->useNormalMap = true; }
                }
                ImGui::InputText("Height Texture Path", mat->heightPath, sizeof(mat->heightPath));
                if(ImGui::Button("Load Height Texture")){
                    GLuint tex = LoadTexture(mat->heightPath);
                    if(tex != 0){ mat->heightTex = tex; mat->useHeightMap = true; }
                }
                ImGui::InputText("YAML Material Path", yamlPathBuffer, sizeof(yamlPathBuffer));
                if(ImGui::Button("Load Material from YAML")){
                    Material m = loadMaterialFromYAML(yamlPathBuffer);
                    *mat = m;
                }
                if(ImGui::Button("Save Material to YAML")){
                    if(saveMaterialToYAML(std::string(yamlPathBuffer), *mat))
                        std::cout << "Material saved to " << yamlPathBuffer << std::endl;
                }
            }
            ImGui::Separator();
            ImGui::Text("Skybox: Using default paths in code.");
            ImGui::End();
        }
        
        // ---------------------- Rendering ----------------------
        int scrWidth, scrHeight;
        glfwGetFramebufferSize(window, &scrWidth, &scrHeight);
        float aspect = scrWidth / static_cast<float>(scrHeight);
        glViewport(0, 0, scrWidth, scrHeight);
        glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        
        glm::mat4 projection = glm::perspective(glm::radians(45.0f), aspect, 0.1f, 100.0f);
        glm::mat4 view = glm::lookAt(camPosVec, glm::vec3(0.0f,0.0f,0.0f), glm::vec3(0,1,0));
        
        // Render scene objects using the main shader.
        glUseProgram(shaderProgram);
        glUniformMatrix4fv(glGetUniformLocation(shaderProgram, "projection"), 1, GL_FALSE, glm::value_ptr(projection));
        glUniformMatrix4fv(glGetUniformLocation(shaderProgram, "view"), 1, GL_FALSE, glm::value_ptr(view));
        glUniform3fv(glGetUniformLocation(shaderProgram, "camPos"), 1, glm::value_ptr(camPosVec));
        glUniform3fv(glGetUniformLocation(shaderProgram, "pointLightPos"), 1, glm::value_ptr(pointLightPos));
        glUniform3fv(glGetUniformLocation(shaderProgram, "pointLightColor"), 1, glm::value_ptr(pointLightColor));
        glUniform1i(glGetUniformLocation(shaderProgram, "useDirLight"), useDirLight);
        glUniform3fv(glGetUniformLocation(shaderProgram, "dirLightDir"), 1, glm::value_ptr(dirLightDir));
        glUniform3fv(glGetUniformLocation(shaderProgram, "dirLightColor"), 1, glm::value_ptr(dirLightColor));
        glUniform1f(glGetUniformLocation(shaderProgram, "parallaxScale"), globalParallaxScale);
        glUniform1f(glGetUniformLocation(shaderProgram, "PI"), IM_PI);
        // Set environment reflection uniforms.
        glActiveTexture(GL_TEXTURE6);
        glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTexture);
        glUniform1i(glGetUniformLocation(shaderProgram, "envMap"), 6);
        glUniform1f(glGetUniformLocation(shaderProgram, "envReflectionIntensity"), 0.3f); // adjust intensity as needed
        
        if(activeObject == 0 && !spheres.empty()){
            // Render sphere.
            glm::mat4 model = glm::translate(glm::mat4(1.0f), spheres[0].position);
            model = glm::rotate(model, spheres[0].rotation, glm::vec3(0,1,0));
            glUniformMatrix4fv(glGetUniformLocation(shaderProgram, "model"), 1, GL_FALSE, glm::value_ptr(model));
            Material& m = spheres[0].material;
            glUniform3fv(glGetUniformLocation(shaderProgram, "albedo"), 1, glm::value_ptr(m.albedo));
            glUniform1f(glGetUniformLocation(shaderProgram, "metallic"), m.metallic);
            glUniform1f(glGetUniformLocation(shaderProgram, "roughness"), m.roughness);
            glUniform1f(glGetUniformLocation(shaderProgram, "ao"), m.ao);
            glUniform1i(glGetUniformLocation(shaderProgram, "useAlbedoMap"), m.useAlbedoMap ? 1 : 0);
            glUniform1i(glGetUniformLocation(shaderProgram, "useMetallicMap"), m.useMetallicMap ? 1 : 0);
            glUniform1i(glGetUniformLocation(shaderProgram, "useRoughnessMap"), m.useRoughnessMap ? 1 : 0);
            glUniform1i(glGetUniformLocation(shaderProgram, "useAOMap"), m.useAOMap ? 1 : 0);
            glUniform1i(glGetUniformLocation(shaderProgram, "useNormalMap"), m.useNormalMap ? 1 : 0);
            glUniform1i(glGetUniformLocation(shaderProgram, "useHeightMap"), m.useHeightMap ? 1 : 0);
            if(m.useAlbedoMap){
                glActiveTexture(GL_TEXTURE0);
                glBindTexture(GL_TEXTURE_2D, m.albedoTex);
                glUniform1i(glGetUniformLocation(shaderProgram, "albedoMap"), 0);
            }
            if(m.useMetallicMap){
                glActiveTexture(GL_TEXTURE1);
                glBindTexture(GL_TEXTURE_2D, m.metallicTex);
                glUniform1i(glGetUniformLocation(shaderProgram, "metallicMap"), 1);
            }
            if(m.useRoughnessMap){
                glActiveTexture(GL_TEXTURE2);
                glBindTexture(GL_TEXTURE_2D, m.roughnessTex);
                glUniform1i(glGetUniformLocation(shaderProgram, "roughnessMap"), 2);
            }
            if(m.useAOMap){
                glActiveTexture(GL_TEXTURE3);
                glBindTexture(GL_TEXTURE_2D, m.aoTex);
                glUniform1i(glGetUniformLocation(shaderProgram, "aoMap"), 3);
            }
            if(m.useNormalMap){
                glActiveTexture(GL_TEXTURE4);
                glBindTexture(GL_TEXTURE_2D, m.normalTex);
                glUniform1i(glGetUniformLocation(shaderProgram, "normalMap"), 4);
            }
            if(m.useHeightMap){
                glActiveTexture(GL_TEXTURE5);
                glBindTexture(GL_TEXTURE_2D, m.heightTex);
                glUniform1i(glGetUniformLocation(shaderProgram, "heightMap"), 5);
            }
            glBindVertexArray(sphereVAO);
            glDrawElements(GL_TRIANGLES, sphereIndices.size(), GL_UNSIGNED_INT, 0);
            glBindVertexArray(0);
        }
        else if(activeObject == 1 && !planes.empty()){
            // Render plane.
            glm::mat4 model = glm::translate(glm::mat4(1.0f), planes[0].position);
            model = glm::rotate(model, planes[0].rotation, glm::vec3(0,1,0));
            glUniformMatrix4fv(glGetUniformLocation(shaderProgram, "model"), 1, GL_FALSE, glm::value_ptr(model));
            Material& m = planes[0].material;
            glUniform3fv(glGetUniformLocation(shaderProgram, "albedo"), 1, glm::value_ptr(m.albedo));
            glUniform1f(glGetUniformLocation(shaderProgram, "metallic"), m.metallic);
            glUniform1f(glGetUniformLocation(shaderProgram, "roughness"), m.roughness);
            glUniform1f(glGetUniformLocation(shaderProgram, "ao"), m.ao);
            glUniform1i(glGetUniformLocation(shaderProgram, "useAlbedoMap"), m.useAlbedoMap ? 1 : 0);
            glUniform1i(glGetUniformLocation(shaderProgram, "useMetallicMap"), m.useMetallicMap ? 1 : 0);
            glUniform1i(glGetUniformLocation(shaderProgram, "useRoughnessMap"), m.useRoughnessMap ? 1 : 0);
            glUniform1i(glGetUniformLocation(shaderProgram, "useAOMap"), m.useAOMap ? 1 : 0);
            glUniform1i(glGetUniformLocation(shaderProgram, "useNormalMap"), m.useNormalMap ? 1 : 0);
            glUniform1i(glGetUniformLocation(shaderProgram, "useHeightMap"), m.useHeightMap ? 1 : 0);
            if(m.useAlbedoMap){
                glActiveTexture(GL_TEXTURE0);
                glBindTexture(GL_TEXTURE_2D, m.albedoTex);
                glUniform1i(glGetUniformLocation(shaderProgram, "albedoMap"), 0);
            }
            if(m.useMetallicMap){
                glActiveTexture(GL_TEXTURE1);
                glBindTexture(GL_TEXTURE_2D, m.metallicTex);
                glUniform1i(glGetUniformLocation(shaderProgram, "metallicMap"), 1);
            }
            if(m.useRoughnessMap){
                glActiveTexture(GL_TEXTURE_2D);
                glBindTexture(GL_TEXTURE_2D, m.roughnessTex);
                glUniform1i(glGetUniformLocation(shaderProgram, "roughnessMap"), 2);
            }
            if(m.useAOMap){
                glActiveTexture(GL_TEXTURE_2D);
                glBindTexture(GL_TEXTURE_2D, m.aoTex);
                glUniform1i(glGetUniformLocation(shaderProgram, "aoMap"), 3);
            }
            if(m.useNormalMap){
                glActiveTexture(GL_TEXTURE_2D);
                glBindTexture(GL_TEXTURE_2D, m.normalTex);
                glUniform1i(glGetUniformLocation(shaderProgram, "normalMap"), 4);
            }
            if(m.useHeightMap){
                glActiveTexture(GL_TEXTURE_2D);
                glBindTexture(GL_TEXTURE_2D, m.heightTex);
                glUniform1i(glGetUniformLocation(shaderProgram, "heightMap"), 5);
            }
            glBindVertexArray(planeVAO);
            glDrawElements(GL_TRIANGLES, planeIndices.size(), GL_UNSIGNED_INT, 0);
            glBindVertexArray(0);
        }
        
        // ---------------------- Render Skybox ----------------------
        glDepthFunc(GL_LEQUAL);
        glUseProgram(skyboxShader);
        // Remove translation from the view matrix.
        glm::mat4 viewSky = glm::mat4(glm::mat3(view));
        glUniformMatrix4fv(glGetUniformLocation(skyboxShader, "view"), 1, GL_FALSE, glm::value_ptr(viewSky));
        glUniformMatrix4fv(glGetUniformLocation(skyboxShader, "projection"), 1, GL_FALSE, glm::value_ptr(projection));
        glBindVertexArray(skyboxVAO);
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTexture);
        glUniform1i(glGetUniformLocation(skyboxShader, "skybox"), 0);
        glDrawArrays(GL_TRIANGLES, 0, 36);
        glBindVertexArray(0);
        glDepthFunc(GL_LESS);
        
        ImGui::Render();
        ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
        glfwSwapBuffers(window);
    }
    
    // Cleanup
    glDeleteVertexArrays(1, &sphereVAO);
    glDeleteBuffers(1, &sphereVBO);
    glDeleteBuffers(1, &sphereEBO);
    glDeleteVertexArrays(1, &planeVAO);
    glDeleteBuffers(1, &planeVBO);
    glDeleteBuffers(1, &planeEBO);
    glDeleteVertexArrays(1, &skyboxVAO);
    glDeleteBuffers(1, &skyboxVBO);
    glDeleteProgram(shaderProgram);
    glDeleteProgram(skyboxShader);
    
    ImGui_ImplOpenGL3_Shutdown();
    ImGui_ImplGlfw_Shutdown();
    ImGui::DestroyContext();
    glfwTerminate();
    return 0;
}