#include "graphics/opengl/program.hpp"

#include "glm/ext/vector_uint3.hpp"
#include "graphics/opengl/shader.hpp"
#include "logging/engine_logger.hpp"

#include <cstddef>
#include <glm/gtc/type_ptr.hpp>

#include <array>

using namespace kuiper::gl;

program::s_ptr program::make() noexcept {
    GLuint name = glCreateProgram();

    return std::make_shared<program>(name);
}

program::s_ptr program::from_shaders(std::initializer_list<shader::s_ptr> shaders) {
    auto prog = program::make();

    for (const auto& s : shaders) {
        prog->attach_shader(s);
    }

    if (!prog->link()) {
        std::array<char, 0x80> link_err_buf {};
        GLsizei                n_chars = 0;
        glGetProgramInfoLog(prog->id(), link_err_buf.size() - 1 /* null terminator */, &n_chars, link_err_buf.data());
        engine_logger::get().error("Failed to link program {}: {}", prog->id(), link_err_buf.data());

        return nullptr;
    }

    return prog;
}

void program::destroy() noexcept {
    if (m_id) {
        glDeleteProgram(m_id);
        m_id = 0;
    }

    return;
}

void program::use() const noexcept {
    return glUseProgram(m_id);
}

void program::attach_shader(const shader::s_ptr& shader) noexcept {
    return glAttachShader(m_id, shader->id());
}

bool program::link() noexcept {
    glLinkProgram(m_id);

    GLint link_status = GL_FALSE;
    glGetProgramiv(m_id, GL_LINK_STATUS, &link_status);

    return link_status == GL_TRUE;
}

// Uniforms

GLint program::uniform_location(const char* name) const noexcept {
    const auto loc = glGetUniformLocation(m_id, name);

    if (loc < 0)
        engine_logger::get().error("glGetUniformLocation failed for uniform with name: \"{}\"", name);

    return loc;
}

void program::set_uint(const char* uniform, std::uint32_t value) noexcept {
    const auto loc = uniform_location(uniform);

    use();
    glUniform1ui(loc, value);
}

void program::set_int(const char* uniform, std::int32_t value) noexcept {
    const auto loc = uniform_location(uniform);

    use();
    glUniform1i(loc, value);
}

void program::set_int_array(const char* uniform, const std::int32_t* value_arr, std::size_t len) noexcept {
    const auto loc = uniform_location(uniform);

    use();
    glUniform1iv(loc, static_cast<GLsizei>(len), value_arr);
}

void program::set_float(const char* uniform, float value) noexcept {
    const auto loc = uniform_location(uniform);

    use();
    glUniform1f(loc, value);

    return;
}

void program::set_vec3(const char* uniform, const glm::vec3& value) noexcept {
    const auto loc = uniform_location(uniform);

    use();
    glUniform3f(loc, value.x, value.y, value.z);

    return;
}

void program::set_uvec3(const char* uniform, const glm::uvec3& value) noexcept {
    const auto loc = uniform_location(uniform);

    use();
    glUniform3ui(loc, value.x, value.y, value.z);

    return;
}

void program::set_uvec2(const char* uniform, const glm::uvec2& value) noexcept {
    const auto loc = uniform_location(uniform);

    use();
    glUniform2ui(loc, value.x, value.y);

    return;
}

void program::set_mat3(const char* uniform, const glm::mat3& value) noexcept {
    const auto loc = uniform_location(uniform);

    use();
    glUniformMatrix3fv(loc, 1, GL_FALSE, glm::value_ptr(value));

    return;
}

void program::set_mat4(const char* uniform, const glm::mat4& value) noexcept {
    const auto loc = uniform_location(uniform);

    use();
    glUniformMatrix4fv(loc, 1, GL_FALSE, glm::value_ptr(value));

    return;
}
