#include "glad/gl.h"
#include "GLFW/glfw3.h"

#include "spdlog/spdlog.h"

#include "glm/glm.hpp"
#include "glm/ext/matrix_clip_space.hpp"
#include "glm/ext/matrix_transform.hpp"

#include "entt/entt.hpp"

#define STB_IMAGE_IMPLEMENTATION
#include "Components.h"
#include "FreeCamera.h"
#include "imgui.h"
#include "imgui_impl_glfw.h"
#include "imgui_impl_opengl3.h"
#include "InputManager.h"
#include "stb_image.h"

#include "Model.h"
#include "ModelManager.h"
#include "Scene.h"
#include "ShaderManager.h"
#include "ShaderProgram.h"
#include "Texture.h"
#include "TextureManager.h"

#include "fmt/base.h"

GLFWwindow* gWindow = nullptr;
int gWindowWidth = 1920;
int gWindowHeight = 1080;

int glVersionMajor = 0;
int glVersionMinor = 0;

float gAspectRatio = 0.f;

float gMouseSensitivity = 0.1f;

FreeCamera gCamera {};

Scene gScene{};

void glfw_error_callback(int error, const char* description);
void glfw_key_callback(GLFWwindow* window, int key, int scancode, int action, int mods);
void glfw_framebuffer_size_callback(GLFWwindow* window, int width, int height);

void init_glfw();
void create_main_window();
void load_gl();
void load_imgui();
void init_camera();
void load_default_textures();
void load_shaders();
void load_inputs();
void load_default_models();
void loop();
void draw_ui();

auto xAxis = glm::vec3{1.f, 0.f, 0.f};
auto yAxis = glm::vec3{0.f, 1.f, 0.f};
auto zAxis = glm::vec3{0.f, 0.f, 1.f};

int main() {
    spdlog::info("b_engine v0.0.3 start");

    init_glfw();

    create_main_window();

    load_gl();

    load_imgui();

    init_camera();

    stbi_set_flip_vertically_on_load(true);

    load_default_textures();

    load_shaders();

    load_inputs();

    load_default_models();

    entt::entity dirLight = gScene.create_game_object();
    auto& [direction, ambient, diffuse, specular] = gScene.attach_component<Components::DirectionalLight>(dirLight);
    direction = {1, -1, 1};
    ambient = {0.3f, 0.3f, 0.3f};
    diffuse = {0.5f, 0.5f, 0.5f};
    specular = {1.0f, 1.0f, 1.0f};

    auto& tag = gScene.fetch_component<Components::Tag>(dirLight);
    tag.name = "directional light";

    loop();

    ShaderManager::shaders.clear();
    TextureManager::textures.clear();
    ModelManager::models.clear();

    glfwDestroyWindow(gWindow);
    glfwTerminate();

    return 0;
}

void glfw_error_callback(int error, const char* description) {
    spdlog::error("glfw error: {}", description);
}

void glfw_framebuffer_size_callback(GLFWwindow *window, int width, int height) {
    glViewport(0, 0, width, height);
    gAspectRatio = (float)width / (float)height;
    gCamera.update_aspect_ratio(gAspectRatio);
}

void init_glfw()
{
    if (!glfwInit()) {
        spdlog::error("could not initialize glfw");
        std::exit(1);
    }

    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    glfwWindowHint(GLFW_SAMPLES, 4);

    glfwSetErrorCallback(glfw_error_callback);
}

void create_main_window()
{
    gWindow = glfwCreateWindow(gWindowWidth, gWindowHeight, "b_engine v0.0.3", nullptr, nullptr);
    if (!gWindow) {
        spdlog::error("failed to create glfw window");
        std::exit(1);
    }

    glfwMakeContextCurrent(gWindow);

    glfwSetKeyCallback(gWindow, InputManager::key_callback);
    glfwSetCursorPosCallback(gWindow, InputManager::mouse_pos_callback);

    glfwSetFramebufferSizeCallback(gWindow, glfw_framebuffer_size_callback);

#ifndef GLFW_CONTEXT_DEBUG
    glfwSetInputMode(gWindow, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
#endif
}

void load_gl()
{
    int version = gladLoadGL(glfwGetProcAddress);
    glVersionMajor = GLAD_VERSION_MAJOR(version);
    glVersionMinor = GLAD_VERSION_MINOR(version);
    spdlog::info("gl version {}.{}", glVersionMajor, glVersionMinor);

    if (gWindow)
    {
        int fbWidth, fbHeight;
        glfwGetFramebufferSize(gWindow, &fbWidth, &fbHeight);
        gAspectRatio = (float)fbWidth / (float)fbHeight;
    }

    glEnable(GL_DEPTH_TEST);
    glEnable(GL_MULTISAMPLE);
}

void load_imgui() {
    IMGUI_CHECKVERSION();
    ImGui::CreateContext();

    ImGui::StyleColorsDark();
    ImGui_ImplGlfw_InitForOpenGL(gWindow, true);
    ImGui_ImplOpenGL3_Init("#version 330");
}

void init_camera() {
    gCamera = {{5.f, 5.f, 5.f}, 0, 0, gAspectRatio};
    gCamera.rotate_yaw(-135.f);
    gCamera.rotate_pitch(-35.f);

    InputManager::add_mouse_listener([](float xoff, float yoff) {
        gCamera.rotate_yaw(xoff * gMouseSensitivity);
        gCamera.rotate_pitch(yoff * gMouseSensitivity);
    });
}

void load_default_textures() {
    spdlog::info("creating default textures");

    unsigned char defaultDiffuseData[4] = {static_cast<unsigned char>(255), 255, 255, 255};
    TextureManager::textures["default_diffuse"] = TextureManager::load_from_data(reinterpret_cast<unsigned char*>(&defaultDiffuseData), 1, 1, 4);

    unsigned char defaultSpecularData[4] = {static_cast<unsigned char>(64), 64, 64, 255};
    TextureManager::textures["default_specular"] = TextureManager::load_from_data(reinterpret_cast<unsigned char*>(&defaultSpecularData), 1, 1, 4);
}

void load_shaders() {
    spdlog::info("compiling shaders");
    ShaderManager::shaders["phong_shader"] = ShaderManager::load("./resources/standard.vert", "./resources/standard.frag");
    if (ShaderManager::shaders["phong_shader"] == nullptr)
    {
        spdlog::error("failed to compile phong shader");
        std::exit(1);
    }
}

void load_inputs() {
    InputManager::generate_input_action("move_forward", {{GLFW_KEY_W, KEY_DOWN}});
    InputManager::generate_input_action("move_backward", {{GLFW_KEY_S, KEY_DOWN}});
    InputManager::generate_input_action("move_right", {{GLFW_KEY_D, KEY_DOWN}});
    InputManager::generate_input_action("move_left", {{GLFW_KEY_A, KEY_DOWN}});
    InputManager::generate_input_action("move_up", {{GLFW_KEY_SPACE, KEY_DOWN}});
    InputManager::generate_input_action("move_down", {{GLFW_KEY_LEFT_SHIFT, KEY_DOWN}});
    InputManager::generate_input_action("exit_application", {{GLFW_KEY_ESCAPE, KEY_DOWN}});
}

void load_default_models() {
    spdlog::info("loading default models");
    ModelManager::models["cube"] = ModelManager::load_from_file("./resources/cube.obj");
    ModelManager::models["vette"] = ModelManager::load_from_file("./resources/c4/C4Fixed.obj");
    ModelManager::models["male"] = ModelManager::load_from_file("./resources/male.obj");
}

void loop() {
    while (!glfwWindowShouldClose(gWindow)) {
        glfwPollEvents();

        // check inputs
        if (InputManager::check_action_performed("exit_application")) {
            glfwSetWindowShouldClose(gWindow, true);
            continue;
        }

        if (InputManager::check_action_performed("move_forward")) {
            gCamera.move(FORWARD, .05);
        }

        if (InputManager::check_action_performed("move_backward")) {
            gCamera.move(BACKWARD, .05);
        }

        if (InputManager::check_action_performed("move_left")) {
            gCamera.move(LEFT, .05);
        }

        if (InputManager::check_action_performed("move_right")) {
            gCamera.move(RIGHT, .05);
        }

        if (InputManager::check_action_performed("move_up")) {
            gCamera.move(UP, .05);
        }

        if (InputManager::check_action_performed("move_down")) {
            gCamera.move(DOWN, .05);
        }

        draw_ui();

        // gl frame prep
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());

        const auto shader = ShaderManager::shaders["phong_shader"];
        shader->bind();
        shader->setMat4("projection", gCamera.projection());
        shader->setMat4("view", gCamera.view());
        shader->setVec3("viewPosition", gCamera.position());

        gScene.draw_scene(shader.get());

        glBindVertexArray(0);
        ShaderProgram::unbind();

        // gl end frame stuff
        glfwSwapBuffers(gWindow);
    }
}

void draw_ui() {
    ImGui_ImplOpenGL3_NewFrame();
    ImGui_ImplGlfw_NewFrame();
    ImGui::NewFrame();
    {
        if (ImGui::Begin("Scene")) {
            ImGui::Text("Scene");
            const auto root = gScene.get_root();
            auto entity = gScene.fetch_component<Components::Relationship>(root).first_child;
            while (entity != entt::null) {
                const auto&[name] = gScene.fetch_component<Components::Tag>(entity);
                std::string text = fmt::format("    {0}", name);
                ImGui::Text((char*)text.data());
                entity = gScene.fetch_component<Components::Relationship>(entity).next_sibling;
            }
            ImGui::End();
        }

        if (ImGui::BeginMainMenuBar()) {
            if (ImGui::BeginMenu("File")) {
                if (ImGui::MenuItem("New Scene", "Ctrl+N")) {
                    /* Handle New File logic */
                }
                if (ImGui::MenuItem("Open Scene", "Ctrl+O")) {
                    /* Handle Open logic */
                }
                ImGui::Separator(); // Adds a visual line between sections
                if (ImGui::MenuItem("Save", "Ctrl+S")) {
                    /* Handle Save logic */
                }
                if (ImGui::MenuItem("Exit", "Alt+F4")) {
                    /* Handle Exit logic */
                }
                ImGui::EndMenu();
            }

            if (ImGui::BeginMenu("Scene")) {
                if (ImGui::MenuItem("New Scene Object")) {
                    gScene.create_game_object();
                    // Do other stuff here, open this entity in the entity editor, etc...
                }
                ImGui::EndMenu();
            }
            ImGui::EndMainMenuBar();
        }
    }
    ImGui::Render();
}