#include <glshader.hpp>

#include <algorithm>
#include <array>
#include <cerrno>
#include <cstring>
#include <fstream>
#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>;


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


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


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
)
{
    // Set here error.
    auto const here_error = [](Here const & here)
    {
        return STR(
            std::get<0>(here) << ":" <<
            std::get<1>(here) << ": " <<
            std::get<2>(here)
        );
    };

    // Set full path.
    auto path_full = path;
    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" <<
            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_version = std::regex{R"(\s*#\s*version\s*(.*))"};

    // Parse.
    auto version_number = 0;
    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, "");

        // 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(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(here())
                )};

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

            // Output.
            ostream << line << "\n";
            for (auto const & define : defines)
                ostream
                    << "#define "
                    << define.first << " "
                    << define.second << "\n";
        }

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

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

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


Shader::Shader(Paths const & paths)
:
    program_{0},
    program_name_{STR(
        "shader program " << STR_JOIN(", ", it, "'" << it << "'", paths)
    )}
{
    // 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
            );
        }

        // 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);
    }
    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_)}
{
    other.program_ = 0;
}


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


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);
}


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."
        )};
}