#include <glshader.hpp>

#include <algorithm>
#include <array>
#include <cerrno>
#include <cstring>
#include <fstream>
#include <ios>
#include <iterator>
#include <list>
#include <memory>
#include <regex>
#include <sstream>
#include <stdexcept>
#include <string>
#include <tuple>
#include <utility>
#include <vector>

#include <GL/glew.h>

#include <str.hpp>


using Here = std::tuple<std::string, int, std::string>;


constexpr auto max_length_workaround = 4096;


// NOLINTNEXTLINE
#define GLSHADER_INIT_(NAME, INIT) decltype(NAME) NAME INIT;
GLSHADER_INIT_(Shader::root_, {})
GLSHADER_INIT_(Shader::defines_, {})
GLSHADER_INIT_(Shader::verts_, {})
GLSHADER_INIT_(Shader::frags_, {})
GLSHADER_INIT_(Shader::uniform_buffers_, {})


template<typename Type>
static Type get_integer_(GLenum name, bool supported = true)
{
    auto data = GLint{};
    if (supported)
        glGetIntegerv(name, &data);
    return (Type)data;
}


void Shader::error_(
    std::string const & error,
    std::string const & error_hint
)
{
    auto gl_error = glGetError();
    if (gl_error != GL_NO_ERROR)
        throw std::runtime_error{STR(
            error << "; " <<
            "got error " << (
                STR_COND(gl_error, GL_INVALID_ENUM)
                STR_COND(gl_error, GL_INVALID_VALUE)
                STR_COND(gl_error, GL_INVALID_OPERATION)
                STR_COND(gl_error, GL_OUT_OF_MEMORY)
                STR(std::hex << std::showbase << gl_error)
            ) <<
            (error_hint.empty() ? "" : STR(" (" << error_hint << ")")) <<
            "."
        )};
}


static void info_log_action_(
    std::string const & error,
    void (action)(GLuint object),
    GLuint object,
    GLenum status_enum,
    void (GLAPIENTRY * getObjectiv)(
        GLuint object, GLenum pname, GLint * params
    ),
    void (GLAPIENTRY * getObjectInfoLog)(
        GLuint object, GLsizei max_length, GLsizei * length, GLchar * info_log
    )
)
{
    // Perform action.
    action(object);

    // Check status.
    auto status = GLint{};
    getObjectiv(object, status_enum, &status);
    if (status)
        return;

    // Get info log length.
    auto info_log_length = GLint{};
    getObjectiv(object, GL_INFO_LOG_LENGTH, &info_log_length);

    // Get info log content.
    // NOLINTNEXTLINE
    auto info_log = std::unique_ptr<GLchar[]>(new GLchar[info_log_length]);
    if (info_log_length)
        getObjectInfoLog(object, info_log_length, nullptr, &info_log[0]);

    // Throw.
    throw std::runtime_error{STR(
        error << (!info_log_length ? "." : STR(":\n" << &info_log[0]))
    )};
}


static std::string source_(
    std::string const & error,
    std::string const & path,
    std::string const & root,
    Shader::Defines const & defines,
    std::string extension_behavior = {},
    std::list<Here> included_by = {}
)
{
    // Set here error.
    auto const here_error = [&](std::list<Here> const & include_here)
    {
        return STR_JOIN(
            "\n",
            it,
            std::get<0>(it) << ":" <<
            std::get<1>(it) << ": " <<
            std::get<2>(it),
            include_here
        );
    };

    // Set include helper.
    auto const include_here = [&](Here const & here)
    {
        auto include_here = included_by;
        include_here.push_front(here);
        return include_here;
    };

    // Set full path.
    auto path_full = std::string{};
    {
        auto istream = std::istringstream(path);
        auto part    = std::string{};
        auto parts   = std::vector<std::string>{};
        parts.reserve((size_t)std::count(path.begin(), path.end(), '/') + 1);
        while (std::getline(istream, part, '/'))
        {
            if (part == ".." && !parts.empty())
                parts.pop_back();
            if (part != ".." && part != ".")
                parts.push_back(std::move(part));
        }
        path_full = STR_JOIN('/', it, it, parts);
    }
    if (!root.empty())
        path_full = STR(root << "/" << path_full);

    // Define and open input stream.
    auto istream = std::ifstream{path_full};
    if (!istream)
        throw std::runtime_error{STR(
            error << "; " <<
            "could not open file '" << path_full << "':\n" <<
            here_error(included_by) << (included_by.empty() ? "" : ":\n") <<
            std::strerror(errno)
        )};

    // Define output stream.
    auto ostream = std::ostringstream{};

    // Define parse regexes.
    static auto const re_ignored   = std::regex{R"(\s*//.*$)"};
    static auto const re_words     = std::regex{R"((\w+(?:\s+\w+)*))"};
    static auto const re_spec      = std::regex{R"((\w+)\s*:\s*(\w+))"};
    static auto const re_quoted    = std::regex{R"((["<])([^">]*)([">]))"};
    static auto const re_version   = std::regex{R"(\s*#\s*version\s*(.*))"};
    static auto const re_extension = std::regex{R"(\s*#\s*extension\s*(.*))"};
    static auto const re_include   = std::regex{R"(\s*#\s*include\s*(.*))"};

    // Parse.
    auto version_number    = 0;
    auto extension_enabled = false;
    auto line_number       = 0;
    auto line              = std::string{};
    auto match             = std::smatch{};
    auto here              = [&]()
    {
        return Here{path_full, line_number, line};
    };
    while (++line_number, std::getline(istream, line))
    {
        // Remove ignored.
        auto const content = std::regex_replace(line, re_ignored, "");

        // Output `#line`.
        auto const line_number_offset = version_number < 330 ? -1 : 0;
        if (GLEW_ARB_shading_language_include)
            if (!extension_behavior.empty() && extension_behavior != "disable")
                ostream
                    << "#line" << " "
                    << line_number + line_number_offset << " "
                    << "\"" << path_full << "\""
                    << "\n";

        // Process version.
        if (std::regex_match(content, match, re_version))
        {
            // Parse.
            auto const words = match.str(1);
            if (!std::regex_match(words, match, re_words))
                throw std::runtime_error{STR(
                    error << "; " <<
                    "malformed #version:\n" <<
                    here_error(include_here(here()))
                )};
            auto const version = match.str(1);

            // Check for errors.
            if (version_number)
                throw std::runtime_error{STR(
                    error << "; " <<
                    "found repeated #version:\n" <<
                    here_error(include_here(here()))
                )};
            if (!included_by.empty())
                throw std::runtime_error{STR(
                    error <<  "; " <<
                    "found #version in #include:\n" <<
                    here_error(include_here(here()))
                )};

            // Process.
            version_number = std::stoi(version);

            // Output.
            ostream << line << "\n";
            if (GLEW_ARB_shading_language_include)
            {
                if (extension_behavior.empty())
                {
                    extension_behavior = "enable";
                    ostream
                        << "#extension GL_ARB_shading_language_include : "
                        << extension_behavior << "\n";
                }
            }
            for (auto const & define : defines)
                ostream
                    << "#define "
                    << define.first << " "
                    << define.second << "\n";
        }

        // Process extension.
        else if (std::regex_match(content, match, re_extension))
        {
            // Parse.
            auto const spec = match.str(1);
            if (!std::regex_match(spec, match, re_spec))
                throw std::runtime_error{STR(
                    error << "; " <<
                    "malformed #extension:\n" <<
                    here_error(include_here(here()))
                )};
            auto const extension = match.str(1);
            auto const behavior  = match.str(2);

            if (extension == "GL_ARB_shading_language_include")
            {
                // Check for errors.
                if (!included_by.empty())
                    throw std::runtime_error{STR(
                        error <<  "; " <<
                        "found #extension GL_ARB_shading_language_include " <<
                        "in #include:\n" <<
                        here_error(include_here(here()))
                    )};

                // Process.
                extension_enabled = behavior != "disable";
                extension_behavior = behavior;
                line = "";
            }

            // Output.
            ostream << line << "\n";
        }

        // Process include.
        else if (std::regex_match(content, match, re_include))
        {
            // Parse.
            auto const quoted = match.str(1);
            if (!std::regex_match(quoted, match, re_quoted))
                throw std::runtime_error{STR(
                    error << "; " <<
                    "malformed #include:\n" <<
                    here_error(include_here(here()))
                )};
            auto const quote_open   = match.str(1);
            auto const include_path = match.str(2);
            auto const quote_close  = match.str(3);

            // Check for errors.
            if (!(
                (quote_open == "\"" && quote_close == "\"") ||
                (quote_open == "<"  && quote_close == ">" )
            ))
                throw std::runtime_error{STR(
                    error << "; " <<
                    "mismatched #include quotes '" << quote_open << "' and '"
                    << quote_close << "':\n" <<
                    here_error(include_here(here()))
                )};
            if (!extension_enabled && included_by.empty())
                throw std::runtime_error{STR(
                    error << "; " <<
                    "#include found but #extension " <<
                    "GL_ARB_shading_language_include not enabled:\n" <<
                    here_error(include_here(here()))
                )};

            // Process.
            auto source = std::string{};
            if (included_by.end() == std::find(
                included_by.begin(), included_by.end(), here()
            ))
            {
                auto include_path_full = include_path;
                if (quote_open == "\"")
                {
                    auto const pos = path.rfind('/');
                    if (pos != path.npos && pos != 0)
                        include_path_full = STR(
                            path.substr(0, pos + 1) << include_path
                        );
                }
                source = source_(
                    error,
                    include_path_full, root,
                    defines,
                    extension_behavior,
                    include_here(here())
                );
            }

            // Output.
            ostream << source << "\n";
        }

        // Non-processed line.
        else
        {
            // Output.
            ostream << line << "\n";
        }
    }

    // Check for version.
    if (!version_number && included_by.empty())
        throw std::runtime_error{STR(
            error << "; " <<
            "found no #version."
        )};

    // Return.
    return ostream.str();
}


template<typename Function>
static void for_variable_(
    GLuint program,
    GLenum count_enum,
    GLenum max_length_enum,
    Function function
)
{
    // Get count.
    auto count = GLuint{};
    glGetProgramiv(program, count_enum, (GLint *)&count);

    // Get max length.
    auto max_length = GLsizei{};
    glGetProgramiv(program, max_length_enum, &max_length);

    // Work around driver bugs.
    if (max_length == 0 && count != 0)
        max_length = max_length_workaround;

    // Allocate and call function.
    // NOLINTNEXTLINE
    auto name = std::unique_ptr<GLchar[]>(new GLchar[max_length]);
    for (auto index = GLuint{0}; index < count; ++index)
        function(index, max_length, &name[0]);
}


Shader::Shader(Paths const & paths)
:
    program_{0},
    program_name_{STR(
        "shader program " << STR_JOIN(", ", it, "'" << it << "'", paths)
    )},
    uniforms_{},
    uniform_blocks_{}
{
    // Get label limits.
    static auto const max_label_length = get_integer_<GLsizei>(
        GL_MAX_LABEL_LENGTH, GLEW_VERSION_4_3 || GLEW_KHR_debug
    );

    try
    {
        // Create program.
        program_ = glCreateProgram();
        if (!program_)
            throw std::runtime_error{STR(
                "Failed to create " << program_name_ << "."
            )};

        // Label program.
        if (GLEW_VERSION_4_3 || GLEW_KHR_debug)
            glObjectLabel(
                GL_PROGRAM,
                program_,
                std::min(max_label_length, (GLsizei)program_name_.length()),
                program_name_.c_str()
            );

        // Process shader paths.
        auto shaders = std::vector<GLuint>{};
        shaders.reserve(paths.size());
        for (auto const & path : paths)
        {
            // Set shader name.
            auto const shader_name = STR(
                "shader '" << path << "' of " << program_name_
            );

            // Infer shader type from path extension.
            auto const type_error = STR(
                "Failed to infer type of " << shader_name
            );
            auto const type_pos = path.rfind('.');
            if (type_pos == path.npos)
                throw std::runtime_error{STR(
                    type_error << "; " <<
                    "no file extension."
                )};
            auto const type_name = path.substr(type_pos + 1);
            auto const type =
                type_name == "vert" ? GL_VERTEX_SHADER :
                type_name == "tesc" ? GL_TESS_CONTROL_SHADER :
                type_name == "tese" ? GL_TESS_EVALUATION_SHADER :
                type_name == "geom" ? GL_GEOMETRY_SHADER :
                type_name == "frag" ? GL_FRAGMENT_SHADER :
                type_name == "comp" ? GL_COMPUTE_SHADER :
                GLenum{0};
            if (!type)
                throw std::runtime_error{STR(
                    type_error << "; " <<
                    "unknown file extension '" << type_name << "'."
                )};

            // Create, attach, and flag shader for deletion when detached.
            auto const shader = glCreateShader(type);
            if (!shader)
                throw std::runtime_error{STR(
                    "Failed to create " << type_name << " shader for " <<
                    shader_name << "."
                )};
            shaders.push_back(shader);
            glAttachShader(program_, shader);
            glDeleteShader(shader);

            // Label shader.
            if (GLEW_VERSION_4_3 || GLEW_KHR_debug)
                glObjectLabel(
                    GL_SHADER,
                    shader,
                    std::min(max_label_length, (GLsizei)shader_name.length()),
                    shader_name.c_str()
                );

            // Set shader source.
            auto const source_error = STR("Failed to source " << shader_name);
            auto const source = source_(source_error, path, root_, defines_);
            auto const sources = std::array<char const *, 1>{{
                source.c_str()
            }};
            glShaderSource(shader, sources.size(), &sources[0], nullptr);

            // Compile shader.
            info_log_action_(
                STR("Failed to compile " << shader_name),
                glCompileShader, shader,
                GL_COMPILE_STATUS, glGetShaderiv, glGetShaderInfoLog
            );
        }

        // Set vertex input locations.
        for (auto const & vert : verts_)
            glBindAttribLocation(
                program_, vert.second, vert.first.c_str()
            );

        // Set fragment output locations.
        if (GLEW_VERSION_3_0)
            for (auto const & frag : frags_)
                glBindFragDataLocation(
                    program_, frag.second, frag.first.c_str()
                );

        // Link program.
        info_log_action_(
            STR("Failed to link " << program_name_),
            glLinkProgram, program_,
            GL_LINK_STATUS, glGetProgramiv, glGetProgramInfoLog
        );

        // Detach shaders.
        for (auto const & shader : shaders)
            glDetachShader(program_, shader);

        // Initialize vertex inputs.
        for_variable_(
            program_,
            GL_ACTIVE_ATTRIBUTES, GL_ACTIVE_ATTRIBUTE_MAX_LENGTH,
            [&](GLuint index, GLsizei max_length, GLchar * name)
            {
                GLint  size{};
                GLenum type{};
                glGetActiveAttrib(
                    program_, index, max_length,
                    nullptr, &size, &type, name
                );
                auto location = glGetAttribLocation(program_, name);
                if (location != -1 && verts_.find(name) == verts_.end())
                    throw std::runtime_error{STR(
                        "Failed to initialize vertex input '" << name <<
                        "' of " << program_name_ << "."
                    )};
            }
        );

        // Initialize uniforms.
        for_variable_(
            program_,
            GL_ACTIVE_UNIFORMS, GL_ACTIVE_UNIFORM_MAX_LENGTH,
            [&](GLuint index, GLsizei max_length, GLchar * name)
            {
                GLint  size{};
                GLenum type{};
                glGetActiveUniform(
                    program_, index, max_length,
                    nullptr, &size, &type, name
                );
                auto location = glGetUniformLocation(program_, name);
                if (location != -1)
                    uniforms_.emplace(name, Uniform{location, false});
            }
        );

        // Initialize uniform blocks.
        for_variable_(
            program_,
            GL_ACTIVE_UNIFORM_BLOCKS, GL_ACTIVE_UNIFORM_BLOCK_MAX_NAME_LENGTH,
            [&](GLuint index, GLsizei max_length, GLchar * name)
            {
                if (!(GLEW_VERSION_3_1 || GLEW_ARB_uniform_buffer_object))
                    throw std::runtime_error{STR(
                        "Failed to initialize uniform block; " <<
                        "ARB_uniform_buffer_object not available."
                    )};
                glGetActiveUniformBlockName(
                    program_, index, max_length,
                    nullptr, name
                );
                auto error = STR(
                    "Failed to initialize uniform block '" << name << "' of "
                    << program_name_
                );
                auto size = GLsizei{};
                glGetActiveUniformBlockiv(
                    program_, index, GL_UNIFORM_BLOCK_DATA_SIZE, (GLint *)&size
                );
                auto & uniform_buffer = *uniform_buffer_(
                    error, name, false, size
                );
                glUniformBlockBinding(program_, index, uniform_buffer.binding);
                uniform_blocks_.emplace(
                    name, UniformBlock{uniform_buffer}
                );
            }
        );
    }
    catch (...)
    {
        // Delete program (and detach and delete shaders).
        if (program_)
            glDeleteProgram(program_);
        throw;
    }
}


Shader::Shader(Shader && other) noexcept
:
    program_       {other.program_},
    program_name_  {std::move(other.program_name_)},
    uniforms_      {std::move(other.uniforms_)},
    uniform_blocks_{std::move(other.uniform_blocks_)}
{
    other.program_ = 0;
}


Shader::~Shader()
{
    if (program_)
        glDeleteProgram(program_);
}


template<typename Uniforms, typename Set>
static void uniforms_validate_(
    std::string const & error,
    std::string const & uniform_type,
    Uniforms uniforms,
    Set set
)
{
    // Find.
    auto it = std::find_if(uniforms.begin(), uniforms.end(),
        [&](typename Uniforms::value_type const & it)
        {
            return !set(it.second);
        }
    );

    // Error if not found.
    if (it != uniforms.end())
        throw std::runtime_error{STR(
            error << "; " <<
            uniform_type << " '" << it->first << "' not set."
        )};
}


void Shader::validate_() const
{
    // Set error.
    auto const validate_error = STR("Failed to validate " << program_name_);

    // Validate program.
    info_log_action_(
        validate_error,
        glValidateProgram, program_,
        GL_VALIDATE_STATUS, glGetProgramiv, glGetProgramInfoLog
    );

    // Assert current.
    current_(validate_error);

    // Validate uniforms.
    uniforms_validate_(validate_error, "uniform", uniforms_,
        [](Uniform const & uniform)
        {
            return uniform.set;
        }
    );

    // Validate uniform blocks.
    uniforms_validate_(validate_error, "uniform block", uniform_blocks_,
        [](UniformBlock const & uniform_block)
        {
            return uniform_block.buffer.set;
        }
    );
}


void Shader::current_(std::string const & error) const
{
    // Error if not current.
    if (get_integer_<GLuint>(GL_CURRENT_PROGRAM) != program_)
        throw std::runtime_error{STR(
            error << "; " <<
            "shader program not current."
        )};
}


Shader::Uniform * Shader::uniform_(
    std::string const & error,
    std::string const & name,
    bool required
)
{
    // Return if found.
    auto it = uniforms_.find(name);
    if (it != uniforms_.end())
    {
        auto & uniform = it->second;
        return &uniform;
    }

    // Error if required.
    if (required)
    {
        auto error_hint = std::string{};
        if (uniform_blocks_.find(name) != uniform_blocks_.end())
            error_hint = " (did you mean the uniform block?)";
        throw std::runtime_error{STR(
            error << "; " <<
            "uniform required but not found" << error_hint << "."
        )};
    }

    // Return.
    return nullptr;
}


Shader::UniformBlock * Shader::uniform_block_(
    std::string const & error,
    std::string const & name,
    bool required,
    GLsizeiptr size
)
{
    // Return if found.
    auto it = uniform_blocks_.find(name);
    if (it != uniform_blocks_.end())
    {
        auto & uniform_block = it->second;
        if (size != uniform_block.buffer.size)
            throw std::runtime_error{STR(
                error << "; " <<
                "expected size " << uniform_block.buffer.size << " but got " <<
                size << "."
            )};
        return &uniform_block;
    }

    // Error if required.
    if (required)
    {
        auto error_hint = std::string{};
        if (uniforms_.find(name) != uniforms_.end())
            error_hint = " (did you mean the uniform?)";
        throw std::runtime_error{STR(
            error << "; " <<
            "uniform block required but not found" << error_hint << "."
        )};
    }

    // Return.
    return nullptr;
}


Shader::UniformBuffer * Shader::uniform_buffer_(
    std::string const & error,
    std::string const & name,
    bool required,
    GLsizeiptr size
)
{
    // Get uniform block / buffer limits.
    static auto const max_uniform_block_size = get_integer_<GLuint>(
        GL_MAX_UNIFORM_BLOCK_SIZE
    );
    static auto const max_uniform_buffer_bindings = get_integer_<GLuint>(
        GL_MAX_UNIFORM_BUFFER_BINDINGS
    );

    // Define next binding.
    static auto next_uniform_buffer_binding_ = GLuint{0};

    // Return if found.
    auto it = uniform_buffers_.find(name);
    if (it != uniform_buffers_.end())
    {
        auto & uniform_buffer = it->second;
        if (size != uniform_buffer.size)
            throw std::runtime_error{STR(
                error << "; " <<
                "expected size " << uniform_buffer.size << " but got " << size
                << "."
            )};
        return &uniform_buffer;
    }

    // Error if required.
    if (required)
        throw std::runtime_error{STR(
            error << "; " <<
            "uniform buffer required but not found."
        )};

    // Set create error.
    auto create_error = STR(
        error << ":\n" <<
        "Failed to create uniform buffer '" << name << "'"
    );

    // Check availability.
    if (!(GLEW_VERSION_3_1 || GLEW_ARB_uniform_buffer_object))
        throw std::runtime_error{STR(
            create_error << "; " <<
            "ARB_uniform_buffer_object not available."
        )};

    // Create storage.
    auto emplace = uniform_buffers_.emplace(name, UniformBuffer{});
    if (!emplace.second)
        throw std::runtime_error{STR(
            create_error << "; " <<
            "already exists."
        )};
    auto & uniform_buffer = emplace.first->second;

    // Check for errors.
    if (size > max_uniform_block_size)
        throw std::runtime_error{STR(
            create_error << "; " <<
            "buffer has size " << size << " but max size is " <<
            max_uniform_block_size << "."
        )};
    if (next_uniform_buffer_binding_ >= max_uniform_buffer_bindings)
        throw std::runtime_error{STR(
            create_error << "; " <<
            "buffer would have binding " << next_uniform_buffer_binding_ <<
            " but max bindings is " << max_uniform_buffer_bindings << "."
        )};

    // Generate and bind.
    glGenBuffers(1, &uniform_buffer.buffer);
    glBindBuffer(GL_UNIFORM_BUFFER, uniform_buffer.buffer),

    // Allocate size.
    uniform_buffer.size = size;
    error_(create_error, "unprocessed previous error");
    glBufferData(
        GL_UNIFORM_BUFFER,
        uniform_buffer.size,
        nullptr,
        GL_DYNAMIC_DRAW
    );
    error_(create_error);

    // Allocate binding and bind.
    uniform_buffer.binding = next_uniform_buffer_binding_++;
    glBindBufferBase(
        GL_UNIFORM_BUFFER,
        uniform_buffer.binding,
        uniform_buffer.buffer
    );

    // Mark as unset.
    uniform_buffer.set = false;

    // Return
    return &uniform_buffer;
}


Shader & Shader::texture(
    std::string const & name,
    GLuint              texture,
    GLenum              target,
    bool                required
)
{
    // Get limits.
    static auto const max_combined_texture_image_units = get_integer_<GLuint>(
        GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS
    );

    // Define data.
    struct Unit
    {
        GLuint unit;
        GLenum target;
        GLuint texture;
    };
    using Units = std::list<Unit>;
    using Textures = std::unordered_map<GLuint, Units::iterator>;
    static auto units = Units{};
    static auto textures = Textures{};

    // Define helpers.
    static auto const unit_to_front = [&](Units::iterator const & unit_it)
    {
        if (unit_it != units.begin())
            units.splice(units.begin(), units, unit_it, std::next(unit_it));
    };
    static auto const target_name = [](GLenum target)
    {
        return
            STR_COND(target, GL_TEXTURE_1D)
            STR_COND(target, GL_TEXTURE_2D)
            STR_COND(target, GL_TEXTURE_3D)
            STR_COND(target, GL_TEXTURE_1D_ARRAY)
            STR_COND(target, GL_TEXTURE_2D_ARRAY)
            STR_COND(target, GL_TEXTURE_RECTANGLE)
            STR_COND(target, GL_TEXTURE_CUBE_MAP)
            STR_COND(target, GL_TEXTURE_CUBE_MAP_ARRAY)
            STR_COND(target, GL_TEXTURE_BUFFER)
            STR_COND(target, GL_TEXTURE_2D_MULTISAMPLE)
            STR_COND(target, GL_TEXTURE_2D_MULTISAMPLE_ARRAY)
            STR(std::hex << std::showbase << target);
    };

    // Check textures.
    auto texture_it = textures.find(texture);
    if (texture_it != textures.end() && texture_it->second->texture == texture)
    {
        // Check for errors.
        if (texture_it->second->target != target)
            throw std::runtime_error{STR(
                "Failed to set texture " << texture << "; " <<
                "expected target " << target_name(texture_it->second->target)
                << " but got " << target_name(target) << "."
            )};

        // Update units.
        unit_to_front(texture_it->second);
    }
    else
    {
        // Update units.
        if (units.size() < max_combined_texture_image_units)
        {
            units.emplace_front(Unit{(GLuint)units.size(), target, texture});
        }
        else
        {
            unit_to_front(std::prev(units.end()));
            units.front().target = target;
            units.front().texture = texture;
        }

        // Update textures.
        if (texture_it == textures.end())
            textures.emplace(texture, units.begin());
        else
            texture_it->second = units.begin();

        // Bind.
        auto const error = STR(
            "Failed to bind texture " << texture << " to target " <<
            target_name(target)
        );
        error_(error, "unprocessed previous error");
        glActiveTexture(GL_TEXTURE0 + units.front().unit);
        glBindTexture(target, texture);
        error_(error, "wrong target?");
    }

    // Set uniform.
    uniform(name, (GLint)units.front().unit, required);

    // Return.
    return *this;
}