/*
 *  <Short Description>
 *  Copyright (C) 2023  Brett Terpstra
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
#define STB_IMAGE_IMPLEMENTATION
#define STB_IMAGE_RESIZE_IMPLEMENTATION
#define STB_IMAGE_WRITE_IMPLEMENTATION
#define STB_PERLIN_IMPLEMENTATION

#include <blt/gfx/stb/stb_image_write.h>

#if defined(__GNUC__) || defined(__GNUG__)
    #pragma GCC diagnostic push
    #pragma GCC diagnostic ignored "-Warray-bounds"
    
    #include <blt/gfx/stb/stb_image_resize2.h>
    
    #pragma GCC diagnostic pop
#else
    #include <blt/gfx/stb/stb_image_resize2.h>
#endif

#include <blt/gfx/stb/stb_image.h>
#include <blt/gfx/stb/stb_perlin.h>
#include <blt/gfx/texture.h>
#include <blt/std/hashmap.h>

struct format_t
{
    GLenum format;
    GLenum type;
};

blt::hashmap_t<GLint, format_t> make_format_table()
{
    blt::hashmap_t<GLint, format_t> map;
    
    map[GL_R8] = {GL_RED, GL_UNSIGNED_BYTE};
    map[GL_R8_SNORM] = {GL_RED, GL_BYTE};
    map[GL_R16F] = {GL_RED, GL_FLOAT};
    map[GL_R32F] = {GL_RED, GL_FLOAT};
    map[GL_R8UI] = {GL_RED_INTEGER, GL_UNSIGNED_BYTE};
    map[GL_R8I] = {GL_RED_INTEGER, GL_BYTE};
    map[GL_R16UI] = {GL_RED_INTEGER, GL_UNSIGNED_SHORT};
    map[GL_R16I] = {GL_RED_INTEGER, GL_SHORT};
    map[GL_R32UI] = {GL_RED_INTEGER, GL_UNSIGNED_INT};
    map[GL_R32I] = {GL_RED_INTEGER, GL_INT};
    map[GL_RG8] = {GL_RG, GL_UNSIGNED_BYTE};
    map[GL_RG8_SNORM] = {GL_RG, GL_BYTE};
    map[GL_RG16F] = {GL_RG, GL_FLOAT};
    map[GL_RG32F] = {GL_RG, GL_FLOAT};
    map[GL_RG8UI] = {GL_RG_INTEGER, GL_UNSIGNED_BYTE};
    map[GL_RG8I] = {GL_RG_INTEGER, GL_BYTE};
    map[GL_RG16UI] = {GL_RG_INTEGER, GL_UNSIGNED_SHORT};
    map[GL_RG16I] = {GL_RG_INTEGER, GL_SHORT};
    map[GL_RG32UI] = {GL_RG_INTEGER, GL_UNSIGNED_INT};
    map[GL_RG32I] = {GL_RG_INTEGER, GL_INT};
    map[GL_RGB8] = {GL_RGB, GL_UNSIGNED_BYTE};
    map[GL_SRGB8] = {GL_RGB, GL_UNSIGNED_BYTE};
    map[GL_RGB565] = {GL_RGB, GL_UNSIGNED_BYTE};
    map[GL_RGB8_SNORM] = {GL_RGB, GL_BYTE};
    map[GL_R11F_G11F_B10F] = {GL_RGB, GL_FLOAT};
    map[GL_RGB9_E5] = {GL_RGB, GL_FLOAT};
    map[GL_RGB16F] = {GL_RGB, GL_FLOAT};
    map[GL_RGB32F] = {GL_RGB, GL_FLOAT};
    map[GL_RGB8UI] = {GL_RGB_INTEGER, GL_UNSIGNED_BYTE};
    map[GL_RGB8I] = {GL_RGB_INTEGER, GL_BYTE};
    map[GL_RGB16UI] = {GL_RGB_INTEGER, GL_UNSIGNED_SHORT};
    map[GL_RGB16I] = {GL_RGB_INTEGER, GL_RGB16I};
    map[GL_RGB32UI] = {GL_RGB_INTEGER, GL_UNSIGNED_INT};
    map[GL_RGB32I] = {GL_RGB_INTEGER, GL_INT};
    map[GL_RGBA8] = {GL_RGBA, GL_UNSIGNED_BYTE};
    map[GL_SRGB8_ALPHA8] = {GL_RGBA, GL_UNSIGNED_BYTE};
    map[GL_RGBA8_SNORM] = {GL_RGBA, GL_BYTE};
    map[GL_RGB5_A1] = {GL_RGBA, GL_UNSIGNED_BYTE};
    map[GL_RGBA4] = {GL_RGBA, GL_UNSIGNED_BYTE};
    map[GL_RGB10_A2] = {GL_RGBA, GL_UNSIGNED_INT_2_10_10_10_REV};
    map[GL_RGBA16F] = {GL_RGBA, GL_FLOAT};
    map[GL_RGBA32F] = {GL_RGBA, GL_FLOAT};
    map[GL_RGBA8UI] = {GL_RGBA_INTEGER, GL_UNSIGNED_BYTE};
    map[GL_RGBA8I] = {GL_RGBA_INTEGER, GL_BYTE};
    map[GL_RGB10_A2UI] = {GL_RGBA_INTEGER, GL_UNSIGNED_INT_2_10_10_10_REV};
    map[GL_RGBA16UI] = {GL_RGBA_INTEGER, GL_UNSIGNED_SHORT};
    map[GL_RGBA16I] = {GL_RGBA_INTEGER, GL_SHORT};
    map[GL_RGBA32I] = {GL_RGBA_INTEGER, GL_INT};
    map[GL_RGBA32UI] = {GL_RGBA_INTEGER, GL_UNSIGNED_INT};
    map[GL_DEPTH_COMPONENT16] = {GL_DEPTH_COMPONENT, GL_UNSIGNED_SHORT};
    map[GL_DEPTH_COMPONENT24] = {GL_DEPTH_COMPONENT, GL_UNSIGNED_INT};
    map[GL_DEPTH_COMPONENT32F] = {GL_DEPTH_COMPONENT, GL_FLOAT};
    map[GL_DEPTH24_STENCIL8] = {GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8};
    map[GL_DEPTH32F_STENCIL8] = {GL_DEPTH_STENCIL, GL_FLOAT_32_UNSIGNED_INT_24_8_REV};
    map[GL_STENCIL_INDEX8] = {GL_STENCIL_INDEX, GL_UNSIGNED_BYTE};
    
    return map;
}

blt::hashmap_t<GLint, format_t> internal_to_texture = make_format_table();

blt::gfx::texture_file::texture_file(const std::string& path, const std::string& name, blt::i32 desired_channels):
        m_name(name.empty() ? path : name), m_path(path), desired_channels(desired_channels)
{
    stbi_set_flip_vertically_on_load(true);
    m_texture.m_data = stbi_load(
            m_path.c_str(),
            &m_texture.m_width, &m_texture.m_height,
            &m_texture.m_channels,
            desired_channels);
    if (desired_channels != 0)
        m_texture.m_channels = desired_channels;
}

blt::gfx::texture_file& blt::gfx::texture_file::resize(int target_width, int target_height)
{
    if (target_width == 0)
        target_width = m_texture.width();
    if (target_height == 0)
        target_height = m_texture.height();
    if (target_width == m_texture.width() && target_height == m_texture.height())
        return *this;
    // since we will be replacing the loaded data pointer, is it wise to use the allocator
    // that matches with what stb image uses, which is malloc, since we unload with stbi_image_free -> (free)
    auto* output_Data = (unsigned char*) malloc(
            target_width * target_height * m_texture.channels()
    );
    
    // resize the texture
    if (stbir_resize_uint8_linear(
            // input
            m_texture.m_data, m_texture.width(), m_texture.height(), 0,
            // output
            output_Data, target_width, target_height, 0,
            // channels
            static_cast<stbir_pixel_layout>(m_texture.channels())
    ) == nullptr)
    {
        BLT_WARN("Error resizing block texture image!");
    }
    
    // free up the old data
    stbi_image_free(m_texture.m_data);
    m_texture.m_data = output_Data;
    m_texture.m_width = target_width;
    m_texture.m_height = target_height;
    
    return *this;
}

void blt::gfx::texture_gl::setDefaults(GLint type, GLint wrap_type) const
{
    glTexParameteri(textureBindType, GL_TEXTURE_WRAP_S, wrap_type);
    glTexParameteri(textureBindType, GL_TEXTURE_WRAP_T, wrap_type);
    // nearest preserves the pixely look
#ifdef __EMSCRIPTEN__
    glTexParameteri(textureBindType, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(textureBindType, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
#else
//    glTexParameteri(textureBindType, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR);
//    glTexParameteri(textureBindType, GL_TEXTURE_MAG_FILTER, GL_NEAREST_MIPMAP_LINEAR);
    glTexParameteri(textureBindType, GL_TEXTURE_MIN_FILTER, type);
    glTexParameteri(textureBindType, GL_TEXTURE_MAG_FILTER, type);
#endif
#ifdef GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT
    // Anisotropy helps preserve textures at oblique angles
                float a = 0;
                glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &a);
                glTexParameterf(textureBindType, GL_TEXTURE_MAX_ANISOTROPY_EXT, a);
#endif
}

void blt::gfx::texture_gl2D::resize(int width, int height)
{
    m_width = width;
    m_height = height;
    bind();
    //glTexStorage2D(textureBindType, 4, textureColorMode, m_width, m_height);
    auto format = internal_to_texture[textureColorMode];
    glTexImage2D(textureBindType, 0, textureColorMode, m_width, m_height, 0, format.format, format.type, nullptr);
    unbind();
}

blt::gfx::texture_gl2D::texture_gl2D(int width, int height, GLint colorMode): texture_gl(width, height, GL_TEXTURE_2D, colorMode)
{
    bind();
    setDefaults(GL_LINEAR, GL_CLAMP_TO_EDGE);
    resize(width, height);
    //glTexStorage2D(textureBindType, 4, colorMode, width, height);
}

blt::gfx::texture_gl2D::texture_gl2D(const blt::gfx::texture_data& data):
        texture_gl(data.width(), data.height(), GL_TEXTURE_2D, data.channels() == 4 ? GL_RGBA8 : GL_RGB8)
{
    bind();
    setDefaults(GL_NEAREST);
    glTexStorage2D(textureBindType, 4, textureColorMode, data.width(), data.height());
    upload((void*) data.data(), data.channels() == 4 ? GL_RGBA : GL_RGB, 0, 0, 0, data.width(), data.height());
    bind();
    generateMipmaps();
    unbind();
}

void blt::gfx::texture_gl2D::upload(void* data, GLint dataColorMode, int level, int x_offset, int y_offset, int sub_width, int sub_height,
                                    GLint dataMode) const
{
    if (sub_width < 0)
        sub_width = m_width;
    if (sub_height < 0)
        sub_height = m_height;
    bind();
    glTexSubImage2D(textureBindType, level, x_offset, y_offset, sub_width, sub_height, dataColorMode, dataMode, data);
    generateMipmaps();
    unbind();
}

void blt::gfx::texture_gl2D::upload(const blt::gfx::texture_data& file_data) const
{
    upload((void*) file_data.data(), file_data.channels() == 4 ? GL_RGBA : GL_RGB, 0, 0, 0, file_data.width(), file_data.height());
}

void blt::gfx::texture_gl2D::upload(void* data, int width, int height, GLint dataColorMode, GLint dataMode)
{
    bind();
    glTexImage2D(textureBindType, 0, textureColorMode, width, height, 0, dataColorMode, dataMode, data);
    generateMipmaps();
    unbind();
}



void blt::gfx::gl_texture2D_array::upload(void* data, int index, GLint dataColorMode, int level, int x_offset, int y_offset, int sub_width,
                                          int sub_height) const
{
    if (sub_width < 0)
        sub_width = m_width;
    if (sub_height < 0)
        sub_height = m_height;
    bind();
    glTexSubImage3D(textureBindType, level, x_offset, y_offset, index, sub_width, sub_height, 1, dataColorMode, GL_UNSIGNED_BYTE, data);
    generateMipmaps();
    unbind();
}

blt::gfx::texture_gl2D_multisample::texture_gl2D_multisample(int width, int height, blt::i32 samples, GLint colorMode):
        texture_gl(width, height, GL_TEXTURE_2D_MULTISAMPLE, colorMode), samples(samples)
{
    bind();
    setDefaults(GL_LINEAR, GL_CLAMP_TO_EDGE);
    //glTexImage2DMultisample(textureBindType, samples, colorMode, width, height, GL_TRUE);
    glTexStorage2DMultisample(textureBindType, samples, colorMode, width, height, GL_TRUE);
}

void blt::gfx::texture_gl2D_multisample::resize(int width, int height)
{
    if (m_width == width && m_height == height)
        return;
    m_width = width;
    m_height = height;
    bind();
    //glTexImage2DMultisample(textureBindType, samples, textureColorMode, width, height, GL_TRUE);
    glTexStorage2DMultisample(textureBindType, samples, textureColorMode, width, height, GL_TRUE);
}

blt::gfx::gl_texture2D_array::gl_texture2D_array(int width, int height, int layers, GLint colorMode):
        texture_gl(width, height, GL_TEXTURE_2D_ARRAY, colorMode), m_layers(layers)
{
    bind();
    setDefaults(GL_LINEAR);
    // 6+ mipmaps is about where I stop noticing any difference (size is 4x4 pixels, so that makes sense)
    glTexStorage3D(textureBindType, 6, colorMode, width, height, layers);
    BLT_DEBUG("Creating 2D Texture Array with ID: %d", textureID);
}
