#include "renderer_gl/shader_bindings.h"

#include <array>
#include <cstring>
#include <string_view>

namespace pp::renderer::gl {

namespace {

constexpr std::uint16_t djb2_hash(std::string_view text) noexcept
{
    std::uint16_t value = 5381U;
    for (std::size_t i = text.size(); i > 0U; --i) {
        value = static_cast<std::uint16_t>(text[i - 1U]) + static_cast<std::uint16_t>(33U * value);
    }
    return value;
}

}

std::span<const OpenGlAttributeBinding> panopainter_shader_attribute_bindings() noexcept
{
    static constexpr std::array<OpenGlAttributeBinding, 5> bindings {
        OpenGlAttributeBinding { .name = "pos", .location = 0 },
        OpenGlAttributeBinding { .name = "uvs", .location = 1 },
        OpenGlAttributeBinding { .name = "uvs2", .location = 2 },
        OpenGlAttributeBinding { .name = "col", .location = 3 },
        OpenGlAttributeBinding { .name = "nor", .location = 3 },
    };

    return bindings;
}

pp::foundation::Status validate_shader_attribute_bindings(
    std::span<const OpenGlAttributeBinding> bindings) noexcept
{
    if (bindings.empty()) {
        return pp::foundation::Status::invalid_argument("shader attribute binding catalog is empty");
    }

    for (std::size_t i = 0; i < bindings.size(); ++i) {
        if (bindings[i].name == nullptr || bindings[i].name[0] == '\0') {
            return pp::foundation::Status::invalid_argument("shader attribute binding has no name");
        }

        for (std::size_t j = i + 1; j < bindings.size(); ++j) {
            if (bindings[j].name != nullptr && std::strcmp(bindings[i].name, bindings[j].name) == 0) {
                return pp::foundation::Status::invalid_argument("shader attribute binding name is duplicated");
            }
        }
    }

    return pp::foundation::Status::success();
}

std::span<const OpenGlUniformName> panopainter_shader_uniform_names() noexcept
{
    static constexpr std::array<OpenGlUniformName, 43> uniforms {
        OpenGlUniformName { .name = "mvp", .id = djb2_hash("mvp") },
        OpenGlUniformName { .name = "tex", .id = djb2_hash("tex") },
        OpenGlUniformName { .name = "tex_alpha", .id = djb2_hash("tex_alpha") },
        OpenGlUniformName { .name = "tex_fg", .id = djb2_hash("tex_fg") },
        OpenGlUniformName { .name = "tex_bg", .id = djb2_hash("tex_bg") },
        OpenGlUniformName { .name = "tex_mix", .id = djb2_hash("tex_mix") },
        OpenGlUniformName { .name = "tex_mix_alpha", .id = djb2_hash("tex_mix_alpha") },
        OpenGlUniformName { .name = "tex_mask", .id = djb2_hash("tex_mask") },
        OpenGlUniformName { .name = "tex_dual", .id = djb2_hash("tex_dual") },
        OpenGlUniformName { .name = "tex_stroke", .id = djb2_hash("tex_stroke") },
        OpenGlUniformName { .name = "tex_pattern", .id = djb2_hash("tex_pattern") },
        OpenGlUniformName { .name = "pattern_offset", .id = djb2_hash("pattern_offset") },
        OpenGlUniformName { .name = "pattern_alpha", .id = djb2_hash("pattern_alpha") },
        OpenGlUniformName { .name = "mix_alpha", .id = djb2_hash("mix_alpha") },
        OpenGlUniformName { .name = "opacity", .id = djb2_hash("opacity") },
        OpenGlUniformName { .name = "wet", .id = djb2_hash("wet") },
        OpenGlUniformName { .name = "lock", .id = djb2_hash("lock") },
        OpenGlUniformName { .name = "col", .id = djb2_hash("col") },
        OpenGlUniformName { .name = "tof", .id = djb2_hash("tof") },
        OpenGlUniformName { .name = "tsz", .id = djb2_hash("tsz") },
        OpenGlUniformName { .name = "alpha", .id = djb2_hash("alpha") },
        OpenGlUniformName { .name = "mask", .id = djb2_hash("mask") },
        OpenGlUniformName { .name = "resolution", .id = djb2_hash("resolution") },
        OpenGlUniformName { .name = "highlight", .id = djb2_hash("highlight") },
        OpenGlUniformName { .name = "blend_mode", .id = djb2_hash("blend_mode") },
        OpenGlUniformName { .name = "dual_blend_mode", .id = djb2_hash("dual_blend_mode") },
        OpenGlUniformName { .name = "noise", .id = djb2_hash("noise") },
        OpenGlUniformName { .name = "dir", .id = djb2_hash("dir") },
        OpenGlUniformName { .name = "use_dual", .id = djb2_hash("use_dual") },
        OpenGlUniformName { .name = "use_pattern", .id = djb2_hash("use_pattern") },
        OpenGlUniformName { .name = "light_dir", .id = djb2_hash("light_dir") },
        OpenGlUniformName { .name = "mode", .id = djb2_hash("mode") },
        OpenGlUniformName { .name = "ambient", .id = djb2_hash("ambient") },
        OpenGlUniformName { .name = "pattern_invert", .id = djb2_hash("pattern_invert") },
        OpenGlUniformName { .name = "pattern_scale", .id = djb2_hash("pattern_scale") },
        OpenGlUniformName { .name = "pattern_bright", .id = djb2_hash("pattern_bright") },
        OpenGlUniformName { .name = "pattern_contr", .id = djb2_hash("pattern_contr") },
        OpenGlUniformName { .name = "pattern_depth", .id = djb2_hash("pattern_depth") },
        OpenGlUniformName { .name = "patt_blend_mode", .id = djb2_hash("patt_blend_mode") },
        OpenGlUniformName { .name = "colorize", .id = djb2_hash("colorize") },
        OpenGlUniformName { .name = "dual_alpha", .id = djb2_hash("dual_alpha") },
        OpenGlUniformName { .name = "use_fragcoord", .id = djb2_hash("use_fragcoord") },
        OpenGlUniformName { .name = "draw_outline", .id = djb2_hash("draw_outline") },
    };

    return uniforms;
}

std::uint16_t shader_uniform_id(std::string_view name) noexcept
{
    return djb2_hash(name);
}

pp::foundation::Status validate_shader_uniform_names(
    std::span<const OpenGlUniformName> uniforms) noexcept
{
    if (uniforms.empty()) {
        return pp::foundation::Status::invalid_argument("shader uniform catalog is empty");
    }

    for (std::size_t i = 0; i < uniforms.size(); ++i) {
        if (uniforms[i].name == nullptr || uniforms[i].name[0] == '\0') {
            return pp::foundation::Status::invalid_argument("shader uniform has no name");
        }

        if (uniforms[i].id != shader_uniform_id(uniforms[i].name)) {
            return pp::foundation::Status::invalid_argument("shader uniform hash does not match its name");
        }

        for (std::size_t j = i + 1; j < uniforms.size(); ++j) {
            if (uniforms[j].name != nullptr && std::strcmp(uniforms[i].name, uniforms[j].name) == 0) {
                return pp::foundation::Status::invalid_argument("shader uniform name is duplicated");
            }

            if (uniforms[i].id == uniforms[j].id) {
                return pp::foundation::Status::invalid_argument("shader uniform hash is duplicated");
            }
        }
    }

    return pp::foundation::Status::success();
}

}
