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

#define GLM_ENABLE_EXPERIMENTAL
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtx/transform.hpp>
#include <glm/gtc/type_ptr.hpp>

#include <imgui.h>
#include <imgui_impl_glfw.h>
#include <imgui_impl_opengl3.h>

#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
#include <cstdint>
#include <algorithm>

#include "scene.h"
#include "Uniforms.h"
#include "InitShader.h"         //Functions for loading shaders from text files
#include "DebugCallback.h"
#include "platform_utils.h"

namespace {

const std::string kVertexShaderPath = "shaders/vertex.glsl";
const std::string kTessellationCtrlPath = "shaders/tessellation_ctrl.glsl";
const std::string kTessellationEvalPath = "shaders/tessellation_eval.glsl";
const std::string kFragmentShaderPath = "shaders/fragment.glsl";

const std::string kQuadVertexPath = "shaders/quad_vertex.glsl";
const std::string kQuadFragmentPath = "shaders/quad_fragment.glsl";

const std::string kHGTPath = "hgt/N02E016.hgt";

GLuint tex_id = -1;

float tessellationFactor = 32.0f;

std::vector<Vertex> vertices;
std::vector<GLuint> indices;

} // namespace

Scene::Scene(int width, int height) 
  : window_width(width),
    window_height(height),
    shader_program_(-1),
    quad_shader_program_(-1),
    vao_(-1), 
    quad_vao_(-1),
    angle_(-glm::pi<float>() / 2.0f), 
    scale_(1.0f), 
    aspect_(static_cast<float>(width) / static_cast<float>(height)), 
    near_z_(0.1f), 
    far_z_(10.0f), 
    fov_(glm::pi<float>() / 6.0f) 
{}

Scene::~Scene() {
    glDeleteProgram(shader_program_);
    glDeleteVertexArrays(1, &vao_);

    fbo_.~FBO();
    post_fbo_.~FBO();
}

void Scene::Init() {
    glewInit();

    glEnable(GL_DEPTH_TEST);
    glEnable(GL_CULL_FACE);

    fbo_.Init(window_width, window_height);
    post_fbo_.Init(window_width, window_height);

    std::vector<int16_t> heightData = LoadHGT(kHGTPath, 3601, 3601);
    tex_id = CreateHeightmapTexture(heightData, 3601, 3601);

    InitBuffers();
    ReloadShader();
    InitQuadBuffers();

    UpdateCamera();
    Uniforms::Init();
}

void Scene::GenerateGrid(int divisions, std::vector<Vertex>& verts, std::vector<GLuint>& inds) {
    float step = 1.0f / static_cast<float>(divisions);
    float halfSize = 0.5f;

    // Create vertices for patches
    for (int i = 0; i <= divisions; i++) {
        for (int j = 0; j <= divisions; j++) {
            float x = -halfSize + j * step;
            float y = -halfSize + i * step;

            Vertex vertex;
            vertex.position = glm::vec3(x, y, 0.0f);
            vertex.texCoord = glm::vec2(static_cast<float>(j) / divisions, static_cast<float>(i) / divisions);

            verts.push_back(vertex);
        }
    }

    // Create indices for patches
    for (int i = 0; i < divisions; i++) {
        for (int j = 0; j < divisions; j++) {
            int bottomLeft = i * (divisions + 1) + j;
            int bottomRight = bottomLeft + 1;
            int topLeft = bottomLeft + (divisions + 1);
            int topRight = topLeft + 1;

            inds.push_back(topLeft);
            inds.push_back(topRight);
            inds.push_back(bottomRight);
            inds.push_back(bottomLeft);
        }
    }
}

// Currently creates a test triangle and initializes its buffers
void Scene::InitBuffers() {
    int divisions = 16;  // Number of divisions along one axis of the grid
    GenerateGrid(divisions, vertices, indices);

    // Create and bind VAO, VBO, and EBO, and pass the data to OpenGL
    GLuint vao, vbo, ebo;
    glGenVertexArrays(1, &vao_);
    glGenBuffers(1, &vbo);
    glGenBuffers(1, &ebo);

    glBindVertexArray(vao_);

    // Bind and fill the vertex buffer
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), vertices.data(), GL_STATIC_DRAW);

    // Bind and fill the element buffer
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(GLuint), indices.data(), GL_STATIC_DRAW);

    // Define vertex position attribute
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, position));
    glEnableVertexAttribArray(0);

    // Define texture coordinate attribute
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, texCoord));
    glEnableVertexAttribArray(1);

    // Unbind VAO
    glBindVertexArray(0);
}

void Scene::InitQuadBuffers() {
    GLuint quad_vbo;
    // Full-screen quad vertices (position and texture coordinates)
    float quadVertices[] = {
        // positions        // texCoords
        -1.0f,  1.0f,  0.0f, 1.0f,  // Top-left
        -1.0f, -1.0f,  0.0f, 0.0f,  // Bottom-left
         1.0f,  1.0f,  1.0f, 1.0f,  // Top-right
         1.0f, -1.0f,  1.0f, 0.0f   // Bottom-right
    };

    glGenVertexArrays(1, &quad_vao_);
    glGenBuffers(1, &quad_vbo);
    glBindVertexArray(quad_vao_);

    glBindBuffer(GL_ARRAY_BUFFER, quad_vbo);
    glBufferData(GL_ARRAY_BUFFER, sizeof(quadVertices), &quadVertices, GL_STATIC_DRAW);

    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0);

    glEnableVertexAttribArray(1);
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float)));

    glBindVertexArray(0);
}


// Allows for runtime shader updates
void Scene::ReloadShader() {
    GLuint geo_shader = InitShader(kVertexShaderPath.c_str(), kTessellationCtrlPath.c_str(), kTessellationEvalPath.c_str(), kFragmentShaderPath.c_str());
    GLuint lht_shader = InitShader(kQuadVertexPath.c_str(), kQuadFragmentPath.c_str());
    if (geo_shader == -1 || lht_shader == -1) {
        DEBUG_BREAK();
        glClearColor(1.0f, 0.0f, 1.0f, 0.0f);
    } else {
        glClearColor(0.35f, 0.35f, 0.35f, 0.0f);
        if (shader_program_ != -1 && quad_shader_program_ != -1) {
            glDeleteProgram(shader_program_);
            glDeleteProgram(quad_shader_program_);
        }
        shader_program_ = geo_shader;
        quad_shader_program_ = lht_shader;
    }
}

void Scene::Display(GLFWwindow* window) {
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    fbo_.Bind();   
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    glUseProgram(shader_program_);

    //glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

    view_matrix_ = glm::lookAt(glm::vec3(0.0f, 1.4f, 1.4f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
    Uniforms::SceneData.PV = projection_matrix_ * view_matrix_; // Projection-View matrix
    Uniforms::BufferSceneData();

    glm::mat4 model_matrix = glm::rotate(angle_, glm::vec3(1.0f, 0.0f, 0.0f)) * glm::scale(glm::vec3(scale_));
    glUniformMatrix4fv(Uniforms::UniformLocs::M, 1, false, glm::value_ptr(model_matrix));

    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, tex_id);

    GLint heightTextureLoc = glGetUniformLocation(shader_program_, "heightTexture");
    glUniform1i(heightTextureLoc, 0);

    GLint tessFactorLoc = glGetUniformLocation(shader_program_, "tessellationFactor");
    glUniform1f(tessFactorLoc, tessellationFactor);

    glBindVertexArray(vao_);
    glPatchParameteri(GL_PATCH_VERTICES, 4);
    glDrawElements(GL_PATCHES, indices.size(), GL_UNSIGNED_INT, 0);

    //glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);

    fbo_.Unbind();

    post_fbo_.Bind();
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    glUseProgram(quad_shader_program_);

    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, fbo_.GetAlbedoGbuffer());
    glActiveTexture(GL_TEXTURE1);
    glBindTexture(GL_TEXTURE_2D, fbo_.GetPositionGbuffer());
    glActiveTexture(GL_TEXTURE2);
    glBindTexture(GL_TEXTURE_2D, fbo_.GetNormalGbuffer());

    GLint colorTextureLoc = glGetUniformLocation(quad_shader_program_, "albedoGbuffer");
    glUniform1i(colorTextureLoc, 0);
    GLint positionTextureLoc = glGetUniformLocation(quad_shader_program_, "positionGbuffer");
    glUniform1i(positionTextureLoc, 1);
    GLint normalTextureLoc = glGetUniformLocation(quad_shader_program_, "normalGbuffer");
    glUniform1i(normalTextureLoc, 2);

    // Render the full-screen quad
    glBindVertexArray(quad_vao_);
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

    post_fbo_.Unbind();

    DrawGui(window);
    glfwSwapBuffers(window);
}

void Scene::DrawGui(GLFWwindow* window) {
    // Begin ImGui frame
    ImGui_ImplOpenGL3_NewFrame();
    ImGui_ImplGlfw_NewFrame();

    // Enable docking
    ImGuiIO& io = ImGui::GetIO();
    io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;

    ImGui::NewFrame();

    static ImGuiDockNodeFlags dockspace_flags = ImGuiDockNodeFlags_PassthruCentralNode;

    // Add menu bar
    ImGuiWindowFlags window_flags = ImGuiWindowFlags_MenuBar | ImGuiWindowFlags_NoDocking;

    // Style windows
    const ImGuiViewport* viewport = ImGui::GetMainViewport();
    ImGui::SetNextWindowPos(viewport->WorkPos);
    ImGui::SetNextWindowSize(viewport->WorkSize);
    ImGui::SetNextWindowViewport(viewport->ID);
    ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
    ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
    window_flags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove;
    window_flags |= ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNavFocus;
    window_flags |= ImGuiWindowFlags_NoBackground;

    ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
    ImGui::Begin("TerraVisor", nullptr, window_flags);
    ImGui::PopStyleVar(3);

    // Submit the DockSpace
    if (io.ConfigFlags & ImGuiConfigFlags_DockingEnable) {
        ImGuiID dockspace_id = ImGui::GetID("MyDockSpace");
        ImGui::DockSpace(dockspace_id, ImVec2(0.0f, 0.0f), dockspace_flags);
    }

    if (ImGui::BeginMenuBar()) {
        if (ImGui::BeginMenu("Options")) {
            ImGui::Text("Nothing Here Yet, Check Back Later!");

            ImGui::EndMenu();
        }

        ImGui::EndMenuBar();
    }

    ImGui::End();

    // Draw Gui
    ImGui::Begin("Terrain Controls");
    if (ImGui::Button("Quit")) {
        glfwSetWindowShouldClose(window, GLFW_TRUE);
    }

    ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / ImGui::GetIO().Framerate, ImGui::GetIO().Framerate);

    ImGui::SliderFloat("Tessellation Level", &tessellationFactor, 1.0f, 64.0f);
    ImGui::End();

    // Draw FBO to ImGui window
    ImGui::Begin("Scene Window");
    ImVec2 windowSize = ImGui::GetContentRegionAvail();
    ImGui::Image((void*)(intptr_t)post_fbo_.GetAlbedoGbuffer(), windowSize, ImVec2(0, 1), ImVec2(1, 0));
    ImGui::End();

    ImGui::Render();
    ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
}

void Scene::Idle() {

}

int16_t Scene::SwapEndian(int16_t val) {
    return (val << 8) | ((val >> 8) & 0xFF);
}

std::vector<int16_t> Scene::LoadHGT(const std::string& filename, int width, int height) {
    std::ifstream file(filename, std::ios::binary);
    std::vector<int16_t> elevationData;

    if (file) {
        for (int i = 0; i < width * height; ++i) {
            int16_t value;
            file.read(reinterpret_cast<char*>(&value), sizeof(int16_t));

            // Handle endianness
            value = SwapEndian(value);

            if (value == -32768) {
                value = -32767; // Clamp no-data values
            }

            elevationData.push_back(value);
        }
    } else {
        std::cerr << "Failed to open HGT file!" << std::endl;
    }

    return elevationData;
}

void Scene::UpdateCamera() {
    projection_matrix_ = glm::perspective(fov_, aspect_, near_z_, far_z_);
}

GLuint Scene::CreateHeightmapTexture(std::vector<int16_t> data, int width, int height) {
    int16_t minVal = *std::min_element(data.begin(), data.end());
    int16_t maxVal = *std::max_element(data.begin(), data.end());

    std::vector<float> floatData;
    for (int16_t &d : data) {
        // Normalize the value between 0 and 1 based on the min/max range
        d = static_cast<int16_t>(255 * (d - minVal) / (maxVal - minVal));

        floatData.push_back(static_cast<float>(d / 255.0f));
    }

    GLuint textureID;
    glGenTextures(1, &textureID);
    glBindTexture(GL_TEXTURE_2D, textureID);

    glTexImage2D(GL_TEXTURE_2D, 0, GL_R32F, width, height, 0, GL_RED, GL_FLOAT, floatData.data());
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

    glBindTexture(GL_TEXTURE_2D, 0);
    return textureID;
}