#include <gltexture.hpp>

#include <algorithm>
#include <ostream>
#include <utility>

#include <globject.hpp>

// NOLINTNEXTLINE
#define STR_EXCEPTION GLObject<>::Exception
#include <str.hpp>


/// Special member functions


GLTexture::GLTexture(
    // NOLINTNEXTLINE
    std::string object_label,
    GLenum      target,
    GLenum      binding,
    GLenum      internal_format,
    GLenum      wrap,
    GLenum      min_filter,
    GLenum      mag_filter
)
:
    GLObject(STR_JOIN(" ", it, it, {
        str_target_(target),
        str_internal_format_(internal_format),
        std::move(object_label)
    })),
    target_    {target},
    binding_   {binding},
    min_filter_{min_filter},
    unit_      {0}
{
    // check_internal_format_(internal_format);
    unit(true);
    if (wrap)
    {
        glTexParameteri(target_, GL_TEXTURE_WRAP_S, (GLint)wrap);
        glTexParameteri(target_, GL_TEXTURE_WRAP_T, (GLint)wrap);
        glTexParameteri(target_, GL_TEXTURE_WRAP_R, (GLint)wrap);
    }
    if (min_filter)
        glTexParameteri(target_, GL_TEXTURE_MIN_FILTER, (GLint)min_filter);
    if (mag_filter)
        glTexParameteri(target_, GL_TEXTURE_MAG_FILTER, (GLint)mag_filter);
    if (min_filter_mipmap_())
        if (!supported({3, 0}, "GL_ARB_framebuffer_object"))
            glTexParameteri(target_, GL_GENERATE_MIPMAP, GL_TRUE);
    if (target_ == GL_TEXTURE_2D)
        // NOLINTNEXTLINE
        if (supported({4, 6}, "GL_ARB_texture_filter_anisotropic"))
            // TODO(rcrnstn): Remove the `_EXT` suffix when the headers are
            // updated.
            glTexParameterf(target_, GL_TEXTURE_MAX_ANISOTROPY_EXT, anisotropy_);
}


/// Core


GLOBJECT_THREAD(GLTexture::anisotropy_, {1.0F})


GLint GLTexture::unit(bool force_active) const
try
{
    auto static const unit_count = (GLuint)integer(
        GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS
    );
    auto static units       = std::vector<GLuint>(unit_count);
    auto static textures    = std::vector<GLuint>(unit_count);
    auto static targets     = std::vector<GLenum>(unit_count);
    auto static unit_next   = GLuint{1};
    auto static unit_active = GLuint{0};

    if (!unit_ || textures[unit_] != object())
    {
        if (unit_next < unit_count)
            units.back() = unit_ = unit_next++;
        else
            unit_ = units.back();
        std::rotate(units.begin(), units.end() - 1, units.end());
    }

    if (force_active || unit_active != unit_)
    {
        glActiveTexture(GL_TEXTURE0 + unit_);
        unit_active = unit_;
    }

    if (textures[unit_] != object())
    {
        GLOBJECT_DEBUG_IF(1)
            check_unit_active_();
        GLOBJECT_DEBUG_IF(1)
        {
            glBindTexture(targets[unit_], 0);
            targets[unit_] = target_;
        }
        glBindTexture(target_, object());
        textures[unit_] = object();
    }
    GLOBJECT_DEBUG_IF(1)
        check_unit_texture_();

    return (GLint)unit_;
}
catch (...)
{
    fail_action_("allocate texture unit for");
}


bool GLTexture::min_filter_mipmap_() const
{
    switch (min_filter_)
    {
        case GL_NEAREST_MIPMAP_NEAREST:
        case GL_NEAREST_MIPMAP_LINEAR:
        case GL_LINEAR_MIPMAP_NEAREST:
        case GL_LINEAR_MIPMAP_LINEAR:
            return true;
        default:
            return false;
    }
}


/// Path


GLOBJECT_THREAD(GLTexture::prefix_, {"assets/textures"})


/// Check


void GLTexture::check_unit_active_() const
{
    auto unit_active = (GLuint)integer(GL_ACTIVE_TEXTURE) - GL_TEXTURE0;
    if (unit_active != unit_)
        STR_THROW(
            "Expected active unit " << unit_        << ", " <<
            "got "                  << unit_active  << "."
        );
}


void GLTexture::check_unit_texture_() const
{
    auto unit_texture = (GLuint)integer(binding_);
    if (unit_texture != object())
        STR_THROW(
            "Expected unit "  << unit_        << " "   <<
            "to be bound to " << debug_name() << ", "  <<
            "got "            << unit_texture << "."
        );
}


void GLTexture::check_data_size_(std::size_t data_size) const
{
    if (data_size != data_size_())
        STR_THROW(
            "Expected data size " << data_size_() << ", " <<
            "got "                << data_size    << "."
        );
}


/// String


std::string GLTexture::str_target_(GLenum target)
{
    switch (target)
    {
        STR_CASE(GL_TEXTURE_1D)
        STR_CASE(GL_TEXTURE_2D)
        STR_CASE(GL_TEXTURE_3D)
        STR_CASE(GL_TEXTURE_1D_ARRAY)
        STR_CASE(GL_TEXTURE_2D_ARRAY)
        STR_CASE(GL_TEXTURE_RECTANGLE)
        STR_CASE(GL_TEXTURE_CUBE_MAP)
        STR_CASE(GL_TEXTURE_CUBE_MAP_ARRAY)
        STR_CASE(GL_TEXTURE_BUFFER)
        STR_CASE(GL_TEXTURE_2D_MULTISAMPLE)
        STR_CASE(GL_TEXTURE_2D_MULTISAMPLE_ARRAY)
        default:
            return str_enum_(target);
    }
}