#include "Cubed/texture_manager.hpp"

#include "Cubed/config.hpp"
#include "Cubed/constants.hpp"
#include "Cubed/map_table.hpp"
#include "Cubed/tools/cubed_assert.hpp"
#include "Cubed/tools/log.hpp"
#include "Cubed/tools/shader_tools.hpp"

namespace {
constexpr int BLOCK_SIZE = 16;
constexpr int BLOCK_NORMAL_SIZE = 128;
constexpr int CROSS_PLANE_SIZE = 16;
constexpr int BLOCK_ITEM_SIZE = 16;
constexpr int UI_SIZE = 16;
constexpr int BLOCK_STATUS_SIZE = 16;

unsigned char* generate_flat_normal_map(int width = BLOCK_NORMAL_SIZE,
                                        int height = BLOCK_NORMAL_SIZE) {
    unsigned char* data = new unsigned char[width * height * 4];
    for (int i = 0; i < width * height; i++) {
        data[i * 4 + 0] = 128; // R -> X = 0
        data[i * 4 + 1] = 128; // G -> Y = 0
        data[i * 4 + 2] = 255; // B -> Z = 1
        data[i * 4 + 3] = 255; // A
    }
    return data;
}

} // namespace

namespace Cubed {

TextureManager::TextureManager() {}

TextureManager::~TextureManager() { delet_texture(); }

void TextureManager::delet_texture() {
    if (m_init) {
        glDeleteTextures(1, &m_texture_array);
        glDeleteTextures(1, &m_block_status_array);
        glDeleteTextures(1, &m_cross_plane_array);
        glDeleteTextures(1, &m_normal_texture_array);
        for (auto& id : m_item_textures) {
            glDeleteTextures(1, &id);
        }
        Logger::info("Successfully delete all texture");
    }
}

GLuint TextureManager::get_block_status_array() const {
    return m_block_status_array;
}

GLuint TextureManager::get_texture_array() const { return m_texture_array; }

GLuint TextureManager::get_cross_plane_array() const {
    return m_cross_plane_array;
}
GLuint TextureManager::get_ui_array() const { return m_ui_array; }

GLuint TextureManager::get_pbr_texture() const {
    return m_normal_texture_array;
}

const std::vector<GLuint>& TextureManager::item_textures() const {
    return m_item_textures;
}

void TextureManager::load_block_status(unsigned id) {

    ASSERT_MSG(id < MAX_BLOCK_STATUS, "Exceed the max status sum limit");
    std::string path = "texture/status/" + std::to_string(id) + ".png";
    unsigned char* image_data = nullptr;
    image_data = (Tools::load_image_data(path));
    glBindTexture(GL_TEXTURE_2D_ARRAY, m_block_status_array);
    glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, id, BLOCK_STATUS_SIZE,
                    BLOCK_STATUS_SIZE, 1, GL_RGBA, GL_UNSIGNED_BYTE,
                    image_data);
    Tools::delete_image_data(image_data);
}

void TextureManager::load_block_texture(unsigned id) {
    ASSERT_MSG(id < BlockManager::sums(), "Exceed the max block sum limit");
    std::string name{BlockManager::name_form_id(id)};
    // air don`t need texture
    if (id == 0) {
        return;
    }

    if (BlockManager::is_cross_plane(id)) {
        load_cross_plane_texture(id);
        return;
    }

    unsigned char* image_data[6];

    std::string block_texture_path = "texture/block/" + name;
    image_data[0] = (Tools::load_image_data(block_texture_path + "/front.png"));
    image_data[1] = (Tools::load_image_data(block_texture_path + "/right.png"));
    image_data[2] = (Tools::load_image_data(block_texture_path + "/back.png"));
    image_data[3] = (Tools::load_image_data(block_texture_path + "/left.png"));
    image_data[4] = (Tools::load_image_data(block_texture_path + "/top.png"));
    image_data[5] = (Tools::load_image_data(block_texture_path + "/base.png"));

    glBindTexture(GL_TEXTURE_2D_ARRAY, m_texture_array);
    Tools::check_opengl_error();
    for (int i = 0; i < 6; i++) {
        glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, id * 6 + i, BLOCK_SIZE,
                        BLOCK_SIZE, 1, GL_RGBA, GL_UNSIGNED_BYTE,
                        image_data[i]);
        Tools::check_opengl_error();
        Tools::delete_image_data(image_data[i]);
    }
}

void TextureManager::load_block_item_texture(unsigned id) {

    ASSERT_MSG(id < BlockManager::sums(), "Exceed the max block sum limit");
    std::string name{BlockManager::name_form_id(id)};

    std::string path = "texture/item/block/" + name + ".png";
    unsigned char* data = nullptr;
    data = Tools::load_image_data(path);
    GLuint texture;
    glGenTextures(1, &texture);
    glBindTexture(GL_TEXTURE_2D, texture);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, BLOCK_ITEM_SIZE, BLOCK_ITEM_SIZE,
                 0, GL_RGBA, GL_UNSIGNED_BYTE, data);

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
                    GL_LINEAR_MIPMAP_LINEAR);
    glGenerateMipmap(GL_TEXTURE_2D);
    if (m_aniso >= 1) {
        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY,
                        static_cast<GLfloat>(m_aniso));
    }
    m_item_textures.push_back(texture);
    Tools::delete_image_data(data);
}

void TextureManager::load_cross_plane_texture(unsigned id) {
    std::string path =
        "texture/block/" + BlockManager::name_form_id(id) + "/cross.png";
    unsigned char* image_data = Tools::load_image_data(path);
    glBindTexture(GL_TEXTURE_2D_ARRAY, m_cross_plane_array);
    glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0,
                    BlockManager::cross_plane_index(id), CROSS_PLANE_SIZE,
                    CROSS_PLANE_SIZE, 1, GL_RGBA, GL_UNSIGNED_BYTE, image_data);
    Tools::delete_image_data(image_data);
}

void TextureManager::load_ui_texture(unsigned id) {
    ASSERT_MSG(id < MAX_UI_NUM, "Exceed the max ui sum limit");

    std::string path = "texture/ui/" + std::to_string(id) + ".png";
    unsigned char* image_data = nullptr;
    image_data = (Tools::load_image_data(path));
    glBindTexture(GL_TEXTURE_2D_ARRAY, m_ui_array);
    glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, id, UI_SIZE, UI_SIZE, 1,
                    GL_RGBA, GL_UNSIGNED_BYTE, image_data);
    Tools::delete_image_data(image_data);
}

void TextureManager::load_pbr_texture(unsigned id) {

    if (id == 0) {
        return;
    }

    if (BlockManager::is_cross_plane(id)) {

        return;
    }

    std::string path = "normal/block/" + BlockManager::name_form_id(id);

    unsigned char* image_data[6];

    image_data[0] = (Tools::load_image_data(path + "/front_n.png", false));
    image_data[1] = (Tools::load_image_data(path + "/right_n.png", false));
    image_data[2] = (Tools::load_image_data(path + "/back_n.png", false));
    image_data[3] = (Tools::load_image_data(path + "/left_n.png", false));
    image_data[4] = (Tools::load_image_data(path + "/top_n.png", false));
    image_data[5] = (Tools::load_image_data(path + "/base_n.png", false));

    glBindTexture(GL_TEXTURE_2D_ARRAY, m_normal_texture_array);
    for (int i = 0; i < 6; i++) {
        unsigned char* data = image_data[i];
        bool is_fallback = false;
        if (!data) {
            is_fallback = true;
            data = generate_flat_normal_map();
        }
        glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, id * 6 + i,
                        BLOCK_NORMAL_SIZE, BLOCK_NORMAL_SIZE, 1, GL_RGBA,
                        GL_UNSIGNED_BYTE, data);
        if (is_fallback) {
            delete[] data;
        } else {
            Tools::delete_image_data(image_data[i]);
        }
    }
}

void TextureManager::init_block() {

    glGenTextures(1, &m_texture_array);
    glBindTexture(GL_TEXTURE_2D_ARRAY, m_texture_array);
    glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA, BLOCK_SIZE, BLOCK_SIZE,
                 BlockManager::sums() * 6, 0, GL_RGBA, GL_UNSIGNED_BYTE,
                 nullptr);

    glGenTextures(1, &m_cross_plane_array);
    glBindTexture(GL_TEXTURE_2D_ARRAY, m_cross_plane_array);
    glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA, CROSS_PLANE_SIZE,
                 CROSS_PLANE_SIZE, BlockManager::cross_plane_sum(), 0, GL_RGBA,
                 GL_UNSIGNED_BYTE, nullptr);
    glGenTextures(1, &m_normal_texture_array);
    glBindTexture(GL_TEXTURE_2D_ARRAY, m_normal_texture_array);
    glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA8, BLOCK_NORMAL_SIZE,
                 BLOCK_NORMAL_SIZE, BlockManager::sums() * 6, 0, GL_RGBA,
                 GL_UNSIGNED_BYTE, nullptr);
    for (unsigned i = 0; i < BlockManager::sums(); i++) {
        load_block_texture(i);
        load_block_item_texture(i);
        load_pbr_texture(i);
    }

    glBindTexture(GL_TEXTURE_2D_ARRAY, m_texture_array);
    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER,
                    GL_LINEAR_MIPMAP_LINEAR);
    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glGenerateMipmap(GL_TEXTURE_2D_ARRAY);
    if (m_aniso >= 1) {
        glTexParameterf(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAX_ANISOTROPY,
                        static_cast<GLfloat>(m_aniso));
    }

    glBindTexture(GL_TEXTURE_2D_ARRAY, m_cross_plane_array);
    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER,
                    GL_LINEAR_MIPMAP_LINEAR);
    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glGenerateMipmap(GL_TEXTURE_2D_ARRAY);
    if (m_aniso >= 1) {
        glTexParameterf(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAX_ANISOTROPY,
                        static_cast<GLfloat>(m_aniso));
    }

    glBindTexture(GL_TEXTURE_2D_ARRAY, m_normal_texture_array);
    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER,
                    GL_LINEAR_MIPMAP_LINEAR);
    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glGenerateMipmap(GL_TEXTURE_2D_ARRAY);
    if (m_aniso >= 1) {
        glTexParameterf(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAX_ANISOTROPY,
                        static_cast<GLfloat>(m_aniso));
    }

    Logger::info("Block Texture Load Success");
}
void TextureManager::init_ui() {
    glGenTextures(1, &m_ui_array);
    glBindTexture(GL_TEXTURE_2D_ARRAY, m_ui_array);
    glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA, UI_SIZE, UI_SIZE, MAX_UI_NUM,
                 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
    for (int i = 0; i < MAX_UI_NUM; i++) {
        load_ui_texture(i);
    }

    glBindTexture(GL_TEXTURE_2D_ARRAY, m_ui_array);
    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
}
void TextureManager::init_block_status() {
    glGenTextures(1, &m_block_status_array);
    glBindTexture(GL_TEXTURE_2D_ARRAY, m_block_status_array);
    glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA, BLOCK_STATUS_SIZE,
                 BLOCK_STATUS_SIZE, MAX_BLOCK_STATUS, 0, GL_RGBA,
                 GL_UNSIGNED_BYTE, nullptr);
    for (int i = 0; i < MAX_BLOCK_STATUS; i++) {
        load_block_status(i);
    }

    glBindTexture(GL_TEXTURE_2D_ARRAY, m_block_status_array);
    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER,
                    GL_LINEAR_MIPMAP_LINEAR);
    glGenerateMipmap(GL_TEXTURE_2D_ARRAY);

    if (m_aniso >= 1) {
        glTexParameterf(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAX_ANISOTROPY,
                        static_cast<GLfloat>(m_aniso));
    }
}
void TextureManager::init_texture() {
    glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY, &m_max_aniso);
    if (m_max_aniso > 0.0f) {
        Logger::info("Support anisotropic filtering max_aniso is {}",
                     m_max_aniso);
    }
    m_aniso = Config::get().get<int>("texture.aniso");
    m_aniso = std::min(static_cast<int>(m_max_aniso), m_aniso);
    Logger::info("Setting Texture Aniso is {}", m_aniso);
    MapTable::init_map();
    Logger::info("Map Init Success");

    init_block();
    init_block_status();
    init_ui();
    m_init = true;
}

void TextureManager::update() {
    if (m_need_reload) {
        hot_reload();
    }
}

void TextureManager::need_reload() { m_need_reload = true; }

void TextureManager::hot_reload() {
    delet_texture();

    init_texture();
    m_need_reload = false;
}

int TextureManager::max_aniso() const { return static_cast<int>(m_max_aniso); }

} // namespace Cubed