#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 "FreeCamera.h"
#include "InputManager.h"
#include "stb_image.h"

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

GLFWwindow* gWindow = nullptr;
int gWindowWidth = 800;
int gWindowHeight = 600;

int glVersionMajor = 0;
int glVersionMinor = 0;

float gAspectRatio = 0.f;

float gMouseSensitivity = 0.1f;

FreeCamera gCamera {};

entt::registry gRegistry{};

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 init_camera();
void load_default_textures();
void load_shaders();
void load_inputs();
void load_default_models();
void loop();

std::shared_ptr<Model> load_model(std::string_view _path);

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

    init_glfw();

    create_main_window();

    load_gl();

    init_camera();

    stbi_set_flip_vertically_on_load(true);

    load_default_textures();

    load_shaders();

    load_inputs();

    load_default_models();

    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.2", 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 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() {
    std::shared_ptr<Texture> defaultDiffuse = TextureManager::textures["default_diffuse"];
    std::shared_ptr<Texture> defaultSpecular = TextureManager::textures["default_specular"];
    std::shared_ptr<ShaderProgram> shader = ShaderManager::shaders["phong_shader"];

    const auto vette = ModelManager::models["vette"];
    auto model = glm::identity<glm::mat4>();
    model = glm::scale(model, glm::vec3(.1f, .1f, .1f));

    const auto& activeModel = vette;

    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(CAMERA_MOVEMENT::FORWARD, .05);
        }

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

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

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

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

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

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

        // draw the stuff
        if (shader && activeModel) {
            shader->bind();
            shader->setMat4("projection", gCamera.projection());
            shader->setMat4("view", gCamera.view());
            shader->setMat4("model", model);
            shader->setVec3("viewPosition", gCamera.position());
            shader->setVec3("lightPosition", glm::vec3{-2.f, 0, 2.0f});
            shader->setVec3("lightDirection", glm::vec3(1, -1, 1));
            shader->setVec3("lightAmbient",   glm::vec3(0.3f, 0.3f, 0.3f));
            shader->setVec3("lightDiffuse",   glm::vec3(0.5f, 0.5f, 0.5f));
            shader->setVec3("lightSpecular",  glm::vec3(1.0f, 1.0f, 1.0f));

            /*for (const auto& mesh: activeModel->meshes) {
                unsigned int materialId = 0;
                if (materialId >= activeModel->materials.size()) {
                    materialId = 0;
                }

                const std::shared_ptr<Material> mat = activeModel->materials[materialId];

                shader->setVec3("phongAmbient",  mat->phong.ambient);
                shader->setVec3("phongDiffuse",  mat->phong.diffuse);
                shader->setVec3("phongSpecular", mat->phong.specular);
                shader->setFloat("phongShininess", mat->phong.shininess);

                glActiveTexture(GL_TEXTURE0);
                auto diffuse = mat->diffuse;
                if (!diffuse) {
                    diffuse = defaultDiffuse;
                }
                diffuse->bind();
                shader->setInt("diffuseMap", 0);

                glActiveTexture(GL_TEXTURE1);
                auto specular = mat->specular;
                if (!specular) {
                    specular = defaultSpecular;
                }
                specular->bind();
                shader->setInt("specularMap", 1);

                glBindVertexArray(mesh.get()->vao);
                glDrawElements(GL_TRIANGLES, mesh->numIndices, GL_UNSIGNED_INT, 0);
            }*/

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

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