#include "imgui.h"
#include "backends/imgui_impl_glfw.h"
#include "backends/imgui_impl_opengl3.h"

#include <iostream>
#include <fstream>
#include <vector>
#include <string>

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

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

#include <Camera.h>
//#include <Primitive.h>

#define P_SPHERE 0x00;
#define P_TORUS 0x01;
#define P_CONE 0x02;
#define P_CUBE 0x03;
#define P_ROUNDED_CUBE 0x04;
#define P_CYLINDER 0x05;

struct Primitive
{
    glm::vec4 position = glm::vec4(0.0f, 0.0f, 0.0f, 1.0f);         // 16 bytes
    glm::vec4 diffuse_color = glm::vec4(0.8f, 0.8f, 0.8f, 1.0f);    // 16 bytes
    glm::vec4 specular_color = glm::vec4(1.0f, 1.0f, 1.0f, 1.0f);   // 16 bytes
    glm::vec4 ambient_color = glm::vec4(0.2f, 0.2f, 0.2f, 1.0f);    // 16 bytes
    
    int specular_exponent = 40;                                     //  4 bytes
    
    unsigned int type = -1;                                         //  4 bytes

    float radius = -1.f;                                            //  4 bytes
    float height = -1.f;                                            //  4 bytes
    float inner_radius = -1.f;                                      //  4 bytes
    float outer_radius = -1.f;                                      //  4 bytes
    float size = -1.f;                                              //  4 bytes

    // Has to be a multiple of 32 bytes (96 in this case), currently only 92
    unsigned int dummy = 0;                                                    //  4 bytes
};

struct Light
{
    glm::vec3 diffuse_color = glm::vec3(0.8f);
    glm::vec3 specular_color = glm::vec3(1.0f);
    glm::vec3 ambient_color = glm::vec3(0.2f);
    glm::vec3 position = glm::vec3(0.0f);
};

float canvas[] = {
    -1.0f, 1.0f, 0.0f,
    -1.0f, -1.0f, 0.0f,
     1.0f,  1.0f, 0.0f,
     1.0f, -1.0f, 0.0f
};

unsigned int VBO;
unsigned int VAO;

namespace window
{
    int size[] = { 1920, 1080};
}

namespace scene
{
    //std::vector<Primitive> prims;
    Camera camera;
    unsigned int shader = -1;

    float angle = glm::pi<float>() / 2.0f;

    float yaw = -90.f;
    float pitch = 0.f;

    std::vector<Primitive> primitiveData;

    glm::vec3 Lp = { 0.f, 1.f, 0.f };       // Light position
    glm::vec3 La = { 124.0 / 255.0, 114.0 / 255.0, 160.0 / 255.0 };    // Light ambient color
    glm::vec3 Ld = { 1.f, 1.f, 1.f };       // Light diffuse color
    glm::vec3 Ls = { 1.f, 1.f, 1.f };       // Light specular color

    int specular_exponent = 10;

    float smoothing = 0.0f;
    glm::vec3 sphere_center = { 0.0, 0.0, 0.0 };

    unsigned int p_vbo = -1;
    int object = 0;
}

namespace mouse
{
    bool altPressed = false;

    double xlast;
    double ylast;
    double xoffset;
    double yoffset;

    float sensitivity = 0.1f;
}

//Create a NULL-terminated string by reading the provided file
static char* ReadShaderSource(const char* shaderFile)
{
    std::ifstream ifs(shaderFile, std::ios::in | std::ios::binary | std::ios::ate);
    if (ifs.is_open())
    {
        unsigned int filesize = static_cast<unsigned int>(ifs.tellg());
        ifs.seekg(0, std::ios::beg);
        char* bytes = new char[filesize + 1];
        memset(bytes, 0, filesize + 1);
        ifs.read(bytes, filesize);
        ifs.close();
        return bytes;
    }
    return NULL;
}

void AddPrimitive(Primitive newPrim)
{
    for (int i = 0; i < scene::primitiveData.size(); i++)
    {
        if (newPrim.type == scene::primitiveData[i].type) newPrim.dummy++;
    }
    scene::primitiveData.push_back(newPrim);
}

void InitPrimitive()
{
    glGenBuffers(1, &scene::p_vbo);
    glBindBuffer(GL_SHADER_STORAGE_BUFFER, scene::p_vbo);
    glBufferData(GL_SHADER_STORAGE_BUFFER, scene::primitiveData.size() * sizeof(Primitive), scene::primitiveData.data(), GL_DYNAMIC_DRAW);
    glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, scene::p_vbo);
    glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
}

void UpdatePrimitive()
{
    glBindBuffer(GL_SHADER_STORAGE_BUFFER, scene::p_vbo);
    glBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, scene::primitiveData.size() * sizeof(Primitive), scene::primitiveData.data());
    glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
}

static unsigned int CompileShader(unsigned int type, const std::string& source)
{
    unsigned int id = glCreateShader(type);
    const char* src = source.c_str();
    glShaderSource(id, 1, &src, nullptr);
    glCompileShader(id);

    int result;
    glGetShaderiv(id, GL_COMPILE_STATUS, &result);
    if (result == GL_FALSE)
    {
        int length;
        glGetShaderiv(id, GL_INFO_LOG_LENGTH, &length);
        char* message = (char*)_malloca(length * sizeof(char));
        glGetShaderInfoLog(id, length, &length, message);
        std::cout << "Failed to compile " << (type == GL_VERTEX_SHADER ? "vertex" : "fragment") << " shader!" << std::endl;
        std::cout << message << std::endl;
        glDeleteShader(id);
        return 0;
    }

    return id;
}

static unsigned int CreateShader(const std::string& vertexShaderFile, const std::string& fragmentShaderFile)
{
    std::string vertexShader = ReadShaderSource(vertexShaderFile.c_str());
    std::string fragmentShader = ReadShaderSource(fragmentShaderFile.c_str());
    GLuint program = glCreateProgram();
    unsigned int vs = CompileShader(GL_VERTEX_SHADER, vertexShader);
    unsigned int fs = CompileShader(GL_FRAGMENT_SHADER, fragmentShader);

    glAttachShader(program, vs);
    glAttachShader(program, fs);
    glLinkProgram(program);
    glValidateProgram(program);

    glDeleteShader(vs);
    glDeleteShader(fs);

    return program;
}

void InitCanvas()
{
    glBindAttribLocation(scene::shader, 0, "pos_attrib");

    glGenVertexArrays(1, &VAO);
    glBindVertexArray(VAO);

    glGenBuffers(1, &VBO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(canvas), canvas, GL_STATIC_DRAW);

    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 3, GL_FLOAT, 0, 0, 0);

    glBindVertexArray(0);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

}

std::string GetName(Primitive prim)
{
    std::string name;

    switch (prim.type)
    {
        case 0x00:
        {
            name = "Sphere";
            break;
        }
        case 0x01:
        {
            name = "Torus";
            break;
        }
        case 0x02:
        {
            name = "Cone";
            break;
        }
        case 0x03:
        {
            name = "Cube";
            break;
        }
        case 0x04:
        {
            name = "Rounded_Cube";
            break;
        }
        default:
        {
            name = "Primitive";
            break;
        }
    }

    if (prim.dummy > 0) name += "_" + std::to_string(prim.dummy);

    return name;
}

void PrimGUI(unsigned int obj)
{
    ImGui::SliderFloat3("Position", glm::value_ptr(scene::primitiveData[obj].position), -2.0f, 2.0f);

    switch (scene::primitiveData[obj].type)
    {
        case 0x00:
        {   
            ImGui::SliderFloat("Radius", &scene::primitiveData[obj].radius, 0.f, 5.0f);
            break;
        }
        case 0x01:
        {
            ImGui::SliderFloat("Outer Radius", &scene::primitiveData[obj].outer_radius, 0.f, 5.0f);
            ImGui::SliderFloat("Inner Radius", &scene::primitiveData[obj].inner_radius, 0.f, 5.0f);
            break;
        }
        case 0x02:
        {
            break;
        }
        case 0x03:
        {
            ImGui::SliderFloat("Size", &scene::primitiveData[obj].size, 0.f, 5.0f);
            break;
        }
        case 0x04:
        {
            ImGui::SliderFloat("Size", &scene::primitiveData[obj].size, 0.f, 5.0f);
            ImGui::SliderFloat("Radius", &scene::primitiveData[obj].radius, 0.f, 2.0f);
            break;
        }
        default:
        {
            break;
        }
    }

    ImGui::Separator();
    ImGui::ColorEdit3("Diffuse", glm::value_ptr(scene::primitiveData[obj].diffuse_color));
    ImGui::ColorEdit3("Specular", glm::value_ptr(scene::primitiveData[obj].specular_color));
    ImGui::ColorEdit3("Ambient", glm::value_ptr(scene::primitiveData[obj].ambient_color));
    ImGui::SliderInt("Specular Exponent", &scene::primitiveData[obj].specular_exponent, 1, 500);
}

void AddSphere()
{
    Primitive s;
    s.type = P_SPHERE;
    s.radius = 0.5f;
    AddPrimitive(s);
}
void AddTorus()
{
    Primitive t;
    t.type = P_TORUS;
    t.outer_radius = 0.5f;
    t.inner_radius = 0.15f;
    AddPrimitive(t);
}
void AddCube()
{
    Primitive c;
    c.type = P_CUBE;
    c.size = 0.5f;
    AddPrimitive(c);
}
void AddRoundedCube()
{
    Primitive rc;
    rc.type = P_ROUNDED_CUBE;
    rc.size = 0.25f;
    rc.radius = 0.025f;
    AddPrimitive(rc);
}

//Draw the ImGui user interface
void draw_gui(GLFWwindow* window)
{ 
    //Begin ImGui Frame
    ImGui_ImplOpenGL3_NewFrame();
    ImGui_ImplGlfw_NewFrame();
    

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

    ImGui::NewFrame();

    static ImGuiDockNodeFlags dockspace_flags = ImGuiDockNodeFlags_PassthruCentralNode;

    // We are using the ImGuiWindowFlags_NoDocking flag to make the parent window not dockable into,
    // because it would be confusing to have two docking targets within each others.
    ImGuiWindowFlags window_flags = ImGuiWindowFlags_MenuBar | ImGuiWindowFlags_NoDocking;

    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("DockSpace Demo", 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("Object 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("Smoothing", &scene::smoothing, 0.0f, 1.0f);

    for (int i = 0; i < scene::primitiveData.size(); i++)
    {
        ImGui::RadioButton(GetName(scene::primitiveData[i]).c_str(), &scene::object, i);
    }

    PrimGUI(scene::object);
    ImGui::End();

    ImGui::Begin("Primitives");
    if (ImGui::Button("Sphere", ImVec2(ImGui::GetWindowSize().y * 0.5f, ImGui::GetWindowSize().y * 0.5f)))
    {
        AddSphere();
        InitPrimitive();
    }ImGui::SameLine();
    if (ImGui::Button("Torus", ImVec2(ImGui::GetWindowSize().y * 0.5f, ImGui::GetWindowSize().y * 0.5f)))
    {
        AddTorus();
        InitPrimitive();
    }ImGui::SameLine();
    if (ImGui::Button("Cube", ImVec2(ImGui::GetWindowSize().y * 0.5f, ImGui::GetWindowSize().y * 0.5f)))
    {
        AddCube();
        InitPrimitive();
    }ImGui::SameLine();
    if (ImGui::Button("Rounded Cube", ImVec2(ImGui::GetWindowSize().y * 0.5f, ImGui::GetWindowSize().y * 0.5f)))
    {
        AddRoundedCube();
        InitPrimitive();
    }
    ImGui::End();    

    //End ImGui Frame
    ImGui::Render();
    ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
}

void idle()
{
    glEnable(GL_DEPTH_TEST);
}

void display(GLFWwindow* window)
{
    glClearColor(1.0f, 0.f, 1.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    glm::mat4 M = glm::rotate(scene::angle, glm::vec3(0.0f, 0.0f, 1.0f));
    glm::mat4 V = glm::lookAt(scene::camera.position(), scene::camera.front(), scene::camera.up());
    glm::mat4 P = glm::perspective(glm::pi<float>() / 2.0f, (float)window::size[0] / (float)window::size[1], 0.1f, 100.0f);

    glViewport(0, 0, window::size[0], window::size[1]);

    int P_loc = glGetUniformLocation(scene::shader, "P");
    if (P_loc != -1)
    {
        glUniformMatrix4fv(P_loc, 1, false, glm::value_ptr(P));
    }
    int V_loc = glGetUniformLocation(scene::shader, "V");
    if (V_loc != -1)
    {
        glUniformMatrix4fv(V_loc, 1, false, glm::value_ptr(V));
    }
    int M_loc = glGetUniformLocation(scene::shader, "M");
    if (M_loc != -1)
    {
        glUniformMatrix4fv(M_loc, 1, false, glm::value_ptr(M));
    }
    int cam_pos_loc = glGetUniformLocation(scene::shader, "cam_pos");
    if (cam_pos_loc != -1)
    {
        glUniform3fv(cam_pos_loc, 1, glm::value_ptr(scene::camera.position()));
    }
    int window_width_loc = glGetUniformLocation(scene::shader, "window_width");
    if (window_width_loc != -1)
    {
        glUniform1i(window_width_loc, window::size[0]);
    }
    int window_height_loc = glGetUniformLocation(scene::shader, "window_height");
    if (window_height_loc != -1)
    {
        glUniform1i(window_height_loc, window::size[1]);
    }
    int smoothing_loc = glGetUniformLocation(scene::shader, "smoothing");
    if (smoothing_loc != -1)
    {
        glUniform1f(smoothing_loc, scene::smoothing);
    }
    int sphere_center_loc = glGetUniformLocation(scene::shader, "sphere_center");
    if (sphere_center_loc != -1)
    {
        glUniform3fv(sphere_center_loc, 1, glm::value_ptr(scene::sphere_center));
    }

    glUniform3fv(glGetUniformLocation(scene::shader, "light_pos"), 1, glm::value_ptr(scene::Lp));
    glUniform3fv(glGetUniformLocation(scene::shader, "la"), 1, glm::value_ptr(scene::La));
    glUniform3fv(glGetUniformLocation(scene::shader, "ld"), 1, glm::value_ptr(scene::Ld));
    glUniform3fv(glGetUniformLocation(scene::shader, "ls"), 1, glm::value_ptr(scene::Ls));

    UpdatePrimitive();

    glBindVertexArray(VAO);
    glDrawArrays(GL_TRIANGLE_STRIP, 0, sizeof(canvas) / 3);

    draw_gui(window);

    glfwSwapBuffers(window);
}

void keyboard_callback(GLFWwindow* window, int key, int scancode, int action, int mods)
{
    if (action == GLFW_PRESS)
    {
        switch (key)
        {
        case GLFW_KEY_ESCAPE:
            glfwSetWindowShouldClose(window, GLFW_TRUE);
            break;
        case GLFW_KEY_LEFT_ALT:
            mouse::altPressed = true;
            break;
        case GLFW_KEY_RIGHT_ALT:
            mouse::altPressed = true;
            break;
        }
    }

    if (action == GLFW_RELEASE)
    {
        switch (key)
        {
        case GLFW_KEY_LEFT_ALT:
            mouse::altPressed = false;
            break;
        case GLFW_KEY_RIGHT_ALT:
            mouse::altPressed = false;
            break;
        }
    }
}

void orbit_camera()
{
    scene::yaw += (float)mouse::xoffset;
    scene::pitch += (float)mouse::yoffset;

    scene::camera.orbit(scene::yaw, scene::pitch);
}

void pan_camera()
{
    float translateSensitivity = 0.02f;

    scene::camera.pan((float)mouse::xoffset * -translateSensitivity, (float)mouse::yoffset * translateSensitivity);
}

void zoom_camera()
{
    scene::camera.zoom((float)mouse::yoffset * 0.01f);
}

void cursor_offset(double xcurrent, double ycurrent)
{
    mouse::xoffset = xcurrent - mouse::xlast;
    mouse::yoffset = ycurrent - mouse::ylast;
    mouse::xlast = xcurrent;
    mouse::ylast = ycurrent;

    mouse::xoffset *= mouse::sensitivity;
    mouse::yoffset *= mouse::sensitivity;
}

//This function gets called when the mouse moves over the window.
void cursor_pos_callback(GLFWwindow* window, double x, double y)
{
    if (mouse::altPressed)
    {
        cursor_offset(x, y);

        if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS) orbit_camera();
        if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_MIDDLE) == GLFW_PRESS) pan_camera();
        if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_RIGHT) == GLFW_PRESS) zoom_camera();
    }
}

//This function gets called when a mouse button is pressed.
void mouse_button_callback(GLFWwindow* window, int button, int action, int mods)
{
    //std::cout << "button : " << button << ", action: " << action << ", mods: " << mods << std::endl;
    if (action == GLFW_PRESS) glfwGetCursorPos(window, &mouse::xlast, &mouse::ylast);
}

void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
    scene::camera.zoom((float)yoffset * -0.1f);
}

void window_size_callback(GLFWwindow* window, int width, int height)
{
    window::size[0] = width;
    window::size[1] = height;
}

void init()
{
    glewInit();
    scene::shader = CreateShader("shaders/parade_vs.glsl", "shaders/parade_fs.glsl");
    glUseProgram(scene::shader);

    AddRoundedCube();

    InitCanvas();
    InitPrimitive();
}

int main()
{
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    GLFWwindow* window = glfwCreateWindow(window::size[0], window::size[1], "Parade", nullptr, nullptr);
    if (window == nullptr)
    {
        std::cout << "Failed to create window" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);

    glfwSetKeyCallback(window, keyboard_callback);
    glfwSetCursorPosCallback(window, cursor_pos_callback);
    glfwSetMouseButtonCallback(window, mouse_button_callback);
    glfwSetScrollCallback(window, scroll_callback);
    glfwSetWindowSizeCallback(window, window_size_callback);

    init();

    IMGUI_CHECKVERSION();
    ImGui::CreateContext();
    ImGui_ImplGlfw_InitForOpenGL(window, true);
    ImGui_ImplOpenGL3_Init("#version 150");

    while (!glfwWindowShouldClose(window))
    {
        idle();
        display(window);

        glfwPollEvents();
    }

    ImGui_ImplOpenGL3_Shutdown();
    ImGui_ImplGlfw_Shutdown();
    ImGui::DestroyContext();

    return 0;
}