#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>

float vertices[] = {
    -0.5f, -0.5f, 0.0f,
     0.5f, -0.5f, 0.0f,
     0.0f,  0.5f, 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[] = { 800, 600 };
}

namespace scene
{
    Camera camera;
    unsigned int shader = -1;

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

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

    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

    glm::vec3 ka = { 0.25f, 0.25f, 0.25f };  // Object ambient color
    glm::vec3 kd = { 193.0 / 255.0, 41.0 / 255.0, 46.0 / 255.0 };   // Object diffuse color
    glm::vec3 ks = { 1.0f, 1.0f, 1.0f };    // Object specular color
    int specular_exponent = 10;

    float smoothing = 0.0f;
    glm::vec3 sphere_center = { 0.0, 0.0, 0.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;
}

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);

}

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

    //Draw Gui
    ImGui::Begin("Debug Window");
    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);
    ImGui::SliderFloat3("Sphere Position", glm::value_ptr(scene::sphere_center), -2.0f, 2.0f);
    ImGui::End();
    
    static bool show_test = false;
    if (show_test)
    {
        ImGui::ShowDemoWindow(&show_test);
    }
    
    //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));

    glUniform3fv(glGetUniformLocation(scene::shader, "ka"), 1, glm::value_ptr(scene::ka));
    glUniform3fv(glGetUniformLocation(scene::shader, "kd"), 1, glm::value_ptr(scene::kd));
    glUniform3fv(glGetUniformLocation(scene::shader, "ks"), 1, glm::value_ptr(scene::ks));
    glUniform1i(glGetUniformLocation(scene::shader, "spec_exponent"), scene::specular_exponent);


    glBindVertexArray(VAO);
    glDrawArrays(GL_TRIANGLE_STRIP, 0, sizeof(vertices) / 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);

    InitCanvas();
}

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;
}