#include "pch.h"
#include "log.h"
#include "texture.h"
#include "util.h"
#include "app.h"
#include "legacy_gl_framebuffer_dispatch.h"
#include "legacy_gl_sampler_dispatch.h"
#include "legacy_gl_texture_dispatch.h"
#include "renderer_gl/opengl_capabilities.h"

#include <cstdint>

std::map<uint16_t, Texture2D> TextureManager::m_textures;
std::array<int, 6> TextureCube::m_faces_map {
    static_cast<int>(pp::renderer::gl::cube_face_texture_target(0U)),
    static_cast<int>(pp::renderer::gl::cube_face_texture_target(1U)),
    static_cast<int>(pp::renderer::gl::cube_face_texture_target(2U)),
    static_cast<int>(pp::renderer::gl::cube_face_texture_target(3U)),
    static_cast<int>(pp::renderer::gl::cube_face_texture_target(4U)),
    static_cast<int>(pp::renderer::gl::cube_face_texture_target(5U)),
};

TextureCube::TextureCube(TextureCube&& other) noexcept
{
    other.m_faces = std::move(other.m_faces); 
    m_cubetex_id = other.m_cubetex_id;      other.m_cubetex_id = 0;
    m_resolution = other.m_resolution;      other.m_resolution = 0;
}

TextureCube::~TextureCube()
{
    destroy();
}

void TextureCube::operator=(TextureCube&& other) noexcept
{
    other.m_faces = std::move(other.m_faces);
    m_cubetex_id = other.m_cubetex_id;      other.m_cubetex_id = 0;
    m_resolution = other.m_resolution;      other.m_resolution = 0;
}

bool TextureCube::create(int resolution) noexcept
{
    App::I->render_task([this, resolution]
    {
        destroy();
        m_resolution = resolution;
        const auto format = pp::renderer::gl::texture_format_for_channel_count(4U);
        const auto texture = pp::legacy::gl_texture::allocate_texture_cube(
            pp::renderer::gl::OpenGlTextureCubeAllocation {
                .resolution = m_resolution,
                .internal_format = static_cast<std::int32_t>(format.internal_format),
                .pixel_format = format.pixel_format,
                .component_type = pp::renderer::gl::unsigned_byte_component_type(),
            },
            "TextureCube::create()");
        if (texture == 0U)
        {
            return;
        }
        m_cubetex_id = static_cast<GLuint>(texture);
    });
    return m_cubetex_id != 0;
}

void TextureCube::destroy() noexcept
{
    if (m_cubetex_id)
    {
        App::I->render_task([f=m_faces, id=m_cubetex_id]
        {
            std::array<std::uint32_t, 7> texture_ids {};
            for (std::size_t i = 0U; i < f.size(); ++i)
                texture_ids[i] = static_cast<std::uint32_t>(f[i]);
            texture_ids[f.size()] = static_cast<std::uint32_t>(id);
            pp::legacy::gl_texture::delete_texture_objects(
                texture_ids,
                "TextureCube::destroy()");
        });
        m_cubetex_id = 0;
        m_faces.fill(0);
        m_resolution = 0;
    }
}

void TextureCube::bind() const noexcept
{
    assert(App::I->is_render_thread());
    pp::legacy::gl_texture::bind_texture_cube(
        static_cast<std::uint32_t>(m_cubetex_id),
        "TextureCube::bind()");
}

bool TextureManager::load(const char* path, bool generate_mipmaps)
{
    uint16_t id = const_hash(path);
    auto t = m_textures.find(id);
    if (t == m_textures.end() || !m_textures[id].ready())
    {
        if (!m_textures[id].load(path))
            return false;
    }
    if (generate_mipmaps && !m_textures[id].has_mips)
        m_textures[id].create_mipmaps();
    return true;
}

void TextureManager::assign(uint16_t id, GLuint tex, int w, int h, GLuint internal_format, GLuint format)
{
    m_textures[id].assign(tex, w, h, internal_format, format);
}

void TextureManager::assign(uint16_t id, GLuint tex, int w, int h)
{
    assign(
        id,
        tex,
        w,
        h,
        pp::renderer::gl::rgba8_internal_format(),
        pp::renderer::gl::rgba_pixel_format());
}

Texture2D& TextureManager::get(uint16_t id)
{
    return m_textures[id];
}

void TextureManager::invalidate()
{
    for (auto& t : m_textures)
    {
        t.second.destroy();
    }
    m_textures.clear();
}

Image Texture2D::get_image() const noexcept
{
    Image ret;
    ret.create(m_width, m_height);
    App::I->render_task([&]
    {
        const auto readback = pp::renderer::gl::readback_opengl_texture_2d(
            pp::renderer::gl::OpenGlTexture2DReadback {
                .texture_id = static_cast<std::uint32_t>(m_tex),
                .width = m_width,
                .height = m_height,
                .format = pp::renderer::gl::rgba8_readback_format(),
                .pixels = ret.m_data.get(),
            },
            pp::legacy::gl_framebuffer::texture_2d_readback_dispatch());
        if (!readback.ok()) {
            LOG("Texture2D::get_image() failed because: %s", readback.status().message);
            return;
        }

        if (!readback.value().pixels_read) {
            LOG("Texture2D::get_image() failed because: %s",
                pp::renderer::gl::framebuffer_status_name(readback.value().framebuffer_status));
        }
    });
    return ret;
}

Texture2D::Texture2D(Texture2D&& other) noexcept
{
    m_tex = other.m_tex;
    m_width = other.m_width;
    m_height = other.m_height;
    m_format = other.m_format;
    m_iformat = other.m_iformat;
    has_mips = other.has_mips;
    other.m_tex = false;
}

void Texture2D::operator=(Texture2D&& other) noexcept
{
    m_tex = other.m_tex;
    m_width = other.m_width;
    m_height = other.m_height;
    m_format = other.m_format;
    m_iformat = other.m_iformat;
    has_mips = other.has_mips;
    other.m_tex = false;
}

bool Texture2D::create(int width, int height)
{
    return create(
        width,
        height,
        static_cast<GLint>(pp::renderer::gl::rgba8_internal_format()),
        static_cast<GLint>(pp::renderer::gl::rgba_pixel_format()));
}

bool Texture2D::create(int width, int height, GLint internal_format)
{
    return create(
        width,
        height,
        internal_format,
        static_cast<GLint>(pp::renderer::gl::rgba_pixel_format()));
}

bool Texture2D::create(int width, int height, GLint internal_format, GLint format, const uint8_t* data)
{
    App::I->render_task([=]
    {
        destroy();
        const auto component_type = pp::renderer::gl::texture_upload_type_for_internal_format(
            static_cast<std::uint32_t>(internal_format));
        const auto texture = pp::legacy::gl_texture::allocate_texture_2d(
            pp::renderer::gl::OpenGlTexture2DAllocation {
                .width = width,
                .height = height,
                .internal_format = internal_format,
                .pixel_format = static_cast<std::uint32_t>(format),
                .component_type = component_type,
                .data = data,
            },
            "Texture2D::create()");
        if (texture == 0U) {
            return;
        }

        m_width = width;
        m_height = height;
        m_format = format;
        m_iformat = internal_format;
        m_tex = static_cast<GLuint>(texture);
    });
    return true;
}
bool Texture2D::create(const Image& img)
{
    const auto format = pp::renderer::gl::texture_format_for_channel_count(static_cast<std::uint32_t>(img.comp));
    if (format.channel_count == 0U)
        return false;

    return create(
        img.width,
        img.height,
        static_cast<GLint>(format.internal_format),
        static_cast<GLint>(format.pixel_format),
        img.data());
}

void Texture2D::create_mipmaps()
{
    App::I->render_task([this]
    {
        if (!pp::legacy::gl_texture::generate_texture_2d_mipmaps(
            static_cast<std::uint32_t>(m_tex),
            "Texture2D::create_mipmaps()")) {
            return;
        }
        has_mips = true;
    });
}

void Texture2D::assign(GLuint tex, int w, int h, GLuint internal_format, GLuint format)
{
    m_tex = tex;
    m_width = w;
    m_height = h;
    m_format = format;
    m_iformat = internal_format;
}

void Texture2D::assign(GLuint tex, int w, int h)
{
    assign(
        tex,
        w,
        h,
        pp::renderer::gl::rgba8_internal_format(),
        pp::renderer::gl::rgba_pixel_format());
}

bool Texture2D::load(std::string filename)
{
    LOG("load texture %s", filename.c_str());
    Image img;
    if (!img.load(filename))
        return false;
    return create(img);
}

bool Texture2D::load_file(std::string filename)
{
    LOG("load texture %s", filename.c_str());
    Image img;
    if (!img.load_file(filename))
        return false;
    return create(img);
}

void Texture2D::destroy()
{
    if (m_tex)
    {
        App::I->render_task_async([id = m_tex] 
        {
            pp::legacy::gl_texture::delete_texture_2d(
                static_cast<std::uint32_t>(id),
                "Texture2D::destroy()");
        });
        m_tex = 0;
    }
}

void Texture2D::bind() const
{
    assert(App::I->is_render_thread());
    pp::legacy::gl_texture::bind_texture_2d(
        static_cast<std::uint32_t>(m_tex),
        "Texture2D::bind()");
}

void Texture2D::unbind() const
{
    assert(App::I->is_render_thread());
    pp::legacy::gl_texture::bind_texture_2d(
        0U,
        "Texture2D::unbind()");
}

void Texture2D::update(const uint8_t* data)
{
    App::I->render_task([this, data]
    {
        pp::legacy::gl_texture::update_texture_2d(
            pp::renderer::gl::OpenGlTexture2DUpdate {
                .texture_id = static_cast<std::uint32_t>(m_tex),
                .width = m_width,
                .height = m_height,
                .pixel_format = static_cast<std::uint32_t>(m_format),
                .component_type = pp::renderer::gl::unsigned_byte_component_type(),
                .data = data,
            },
            "Texture2D::update()");
    });
}

glm::vec2 Texture2D::size() const
{
    return { m_width, m_height };
}

Texture2D::~Texture2D()
{
    destroy();
}

bool Sampler::create(GLint filter, GLint wrap)
{
    bool ret = false;
    App::I->render_task([this, &ret, filter, wrap]
    {
        const auto parameters = pp::renderer::gl::sampler_parameters_for_filter_wrap(
            static_cast<std::uint32_t>(filter),
            static_cast<std::uint32_t>(wrap));
        const auto sampler = pp::legacy::gl_sampler::create_sampler(
            parameters,
            "Sampler::create()");
        if (sampler == 0U)
        {
            ret = false;
            return;
        }
        id = static_cast<GLuint>(sampler);
        ret = true;
    });
    return ret;
}

bool Sampler::create()
{
    return create(
        static_cast<GLint>(pp::renderer::gl::linear_texture_filter()),
        static_cast<GLint>(pp::renderer::gl::clamp_to_edge_texture_wrap()));
}

bool Sampler::create(GLint filter)
{
    return create(filter, static_cast<GLint>(pp::renderer::gl::clamp_to_edge_texture_wrap()));
}

void Sampler::set(GLint filter, GLint wrap)
{
    App::I->render_task([=]
    {
        const auto parameters = pp::renderer::gl::sampler_parameters_for_filter_wrap(
            static_cast<std::uint32_t>(filter),
            static_cast<std::uint32_t>(wrap));
        pp::legacy::gl_sampler::set_sampler_parameters(
            static_cast<std::uint32_t>(id),
            parameters,
            "Sampler::set()");
    });
}

void Sampler::set()
{
    set(
        static_cast<GLint>(pp::renderer::gl::linear_texture_filter()),
        static_cast<GLint>(pp::renderer::gl::clamp_to_edge_texture_wrap()));
}

void Sampler::set(GLint filter)
{
    set(filter, static_cast<GLint>(pp::renderer::gl::clamp_to_edge_texture_wrap()));
}

void Sampler::set_filter(GLint filter_min, GLint filter_mag)
{
    App::I->render_task([=]
    {
        const auto parameters = pp::renderer::gl::sampler_filter_parameters(
            static_cast<std::uint32_t>(filter_min),
            static_cast<std::uint32_t>(filter_mag));
        pp::legacy::gl_sampler::set_sampler_parameters(
            static_cast<std::uint32_t>(id),
            parameters,
            "Sampler::set_filter()");
    });
}
void Sampler::set_border(glm::vec4 rgba)
{
    App::I->render_task([this, rgba]
    {
        pp::legacy::gl_sampler::set_sampler_border_color(
            static_cast<std::uint32_t>(id),
            pp::renderer::gl::sampler_border_color_parameter_name(),
            glm::value_ptr(rgba),
            "Sampler::set_border()");
    });
}
void Sampler::bind(int unit) const
{
    assert(App::I->is_render_thread());
    current_unit = unit;
    pp::legacy::gl_sampler::bind_sampler(
        static_cast<std::uint32_t>(unit),
        static_cast<std::uint32_t>(id),
        "Sampler::bind()");
}
void Sampler::unbind()
{
    assert(App::I->is_render_thread());
    pp::legacy::gl_sampler::bind_sampler(
        static_cast<std::uint32_t>(current_unit),
        0U,
        "Sampler::unbind()");
}

