#include <cstddef>
#include <string>
#include <vector>

#include <GL/glew.h>
#include <glm/glm.hpp>

#include <glfbo3d.hpp>
#include <glshader.hpp>
#include <gltest.hpp>


constexpr auto size = 256;
constexpr auto fbo_size_divisor = 2;


static void draw(GLuint texture0)
{
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    glViewport(0, 0, size, size);
    glActiveTexture(GL_TEXTURE0 + 0);
    glBindTexture(GL_TEXTURE_3D, texture0);
    auto draw = Shader({
        "draw.vert",
        "draw.geom", "quad.geom",
        "draw.frag",
    });
    draw
        .use()
        .uniform("texture0", 0);
    auto t = 0.0F;
    gltest_set_time(t);
    while ((t = gltest_get_time()) < 1)
    {
        draw
            .uniform("z", t * 2 - 1, false)
            .validate();
        glDrawArrays(GL_TRIANGLES, 0, 3);
        gltest_swap_buffers();
    }
}


GLTEST(3, 2, size, size, glfbo3d)
{
    // Settings.
    Shader::root("tests/shaders");
    auto fbo_size = glm::ivec3(size / fbo_size_divisor);

    // Create and bind vertex array.
    auto vertex_array = GLuint{};
    glGenVertexArrays(1, &vertex_array);
    glBindVertexArray(vertex_array);

    // Create texture.
    auto texture0 = GLuint{};
    glGenTextures(1, &texture0);
    glBindTexture(GL_TEXTURE_3D, texture0);
    glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexImage3D(
        GL_TEXTURE_3D, 0, GL_RGBA32F,
        fbo_size.x, fbo_size.y, fbo_size.z, 0,
        GL_RGBA, GL_FLOAT, nullptr
    );

    // Create framebuffer.
    auto framebuffer = GLuint{};
    glGenFramebuffers(1, &framebuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
    glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture0, 0);

    // Set with CPU and draw.
    auto data = std::vector<glm::vec4>{};
    data.reserve((size_t)(fbo_size.x * fbo_size.y * fbo_size.z));
    for (int z = 0; z < fbo_size.z; ++z)
        for (int y = 0; y < fbo_size.y; ++y)
            for (int x = 0; x < fbo_size.x; ++x)
                data.emplace_back(
                    x / (fbo_size.x / 8) % 2 ^ // NOLINT
                    y / (fbo_size.y / 4) % 2 ^ // NOLINT
                    z / (fbo_size.z / 2) % 2   // NOLINT
                );
    glBindTexture(GL_TEXTURE_3D, texture0);
    glTexSubImage3D(
        GL_TEXTURE_3D, 0,
        0, 0, 0, fbo_size.x, fbo_size.y, fbo_size.z,
        GL_RGBA, GL_FLOAT, &data[0]
    );
    draw(texture0);

    // Set with GPU and draw.
    glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
    glViewport(0, 0, fbo_size.x, fbo_size.y);
    auto instance_count = fbo_size.z;
    auto set = Shader({
        "set.vert",
        "set.geom", "quad.geom",
        "set.frag",
    });
    set
        .use()
        .uniform("instance_count", instance_count)
        .validate();
    glDrawArraysInstanced(GL_TRIANGLES, 0, 3, instance_count);
    draw(texture0);

    // Delete.
    glDeleteFramebuffers(1, &framebuffer);
    glDeleteTextures(1, &texture0);
    glDeleteVertexArrays(1, &vertex_array);
}