#include "pch.h"
#include "log.h"
#include "rtt.h"
#include "util.h"
#include "app.h"
#include "renderer_gl/opengl_capabilities.h"

#include <cstdint>

namespace {

[[nodiscard]] GLenum texture_2d_target() noexcept
{
    return static_cast<GLenum>(pp::renderer::gl::texture_2d_target());
}

[[nodiscard]] GLenum renderbuffer_target() noexcept
{
    return static_cast<GLenum>(pp::renderer::gl::renderbuffer_target());
}

[[nodiscard]] GLenum framebuffer_target() noexcept
{
    return static_cast<GLenum>(pp::renderer::gl::framebuffer_target());
}

void query_opengl_integer(std::uint32_t name, std::int32_t* value) noexcept
{
    glGetIntegerv(static_cast<GLenum>(name), reinterpret_cast<GLint*>(value));
}

void bind_opengl_framebuffer(std::uint32_t target, std::uint32_t framebuffer) noexcept
{
    glBindFramebuffer(static_cast<GLenum>(target), static_cast<GLuint>(framebuffer));
}

void blit_opengl_framebuffer(
    std::int32_t source_x0,
    std::int32_t source_y0,
    std::int32_t source_x1,
    std::int32_t source_y1,
    std::int32_t destination_x0,
    std::int32_t destination_y0,
    std::int32_t destination_x1,
    std::int32_t destination_y1,
    std::uint32_t mask,
    std::uint32_t filter) noexcept
{
    glBlitFramebuffer(
        static_cast<GLint>(source_x0),
        static_cast<GLint>(source_y0),
        static_cast<GLint>(source_x1),
        static_cast<GLint>(source_y1),
        static_cast<GLint>(destination_x0),
        static_cast<GLint>(destination_y0),
        static_cast<GLint>(destination_x1),
        static_cast<GLint>(destination_y1),
        static_cast<GLbitfield>(mask),
        static_cast<GLenum>(filter));
}

void read_opengl_pixels(
    std::int32_t x,
    std::int32_t y,
    std::int32_t width,
    std::int32_t height,
    std::uint32_t pixel_format,
    std::uint32_t component_type,
    void* pixels) noexcept
{
    glReadPixels(
        static_cast<GLint>(x),
        static_cast<GLint>(y),
        static_cast<GLsizei>(width),
        static_cast<GLsizei>(height),
        static_cast<GLenum>(pixel_format),
        static_cast<GLenum>(component_type),
        pixels);
}

}

RTT& RTT::operator=(RTT&& other)
{
    int_fmt = other.int_fmt;
    texID = other.texID;
    fboID = other.fboID;
    rboID = other.rboID;
    w = other.w;
    h = other.h;
    bound = other.bound;

    other.int_fmt = 0;
    other.texID = 0;
    other.fboID = 0;
    other.rboID = 0;
    other.w = 0;
    other.h = 0;
    other.bound = false;

    return *this;
}

RTT::RTT()
{
    int_fmt = 0;
    fboID = 0;
    rboID = 0;
    w = 0;
    h = 0;
    bound = false;
}

RTT::RTT(RTT&& other)
{
    int_fmt = other.int_fmt;
    texID = other.texID;
    fboID = other.fboID;
    rboID = other.rboID;
    w = other.w;
    h = other.h;
    bound = other.bound;

    other.int_fmt = 0;
    other.texID = 0;
    other.fboID = 0;
    other.rboID = 0;
    other.w = 0;
    other.h = 0;
    other.bound = false;
}

RTT::~RTT()
{
#ifdef _DEBUG
    if (valid())
        LOG("RTT not destroyed");
#endif // _DEBUG
    destroy();
}

bool RTT::resize(int width, int height)
{
    bool ret = false;
    RTT new_rtt;
    
    App::I->render_task([&]
    {
        ret = new_rtt.create(width, height, -1, int_fmt, rboID != 0);
        if (!ret)
        {
            LOG("failed to resize rtt from %dx%d to %dx%d", w, h, width, height);
            return;
        }

        const auto status = pp::renderer::gl::blit_opengl_framebuffer(
            pp::renderer::gl::OpenGlFramebufferBlit {
                .source_framebuffer = static_cast<std::uint32_t>(fboID),
                .destination_framebuffer = static_cast<std::uint32_t>(new_rtt.fboID),
                .source_rect = pp::renderer::gl::OpenGlFramebufferRect { .x1 = w, .y1 = h },
                .destination_rect = pp::renderer::gl::OpenGlFramebufferRect {
                    .x1 = new_rtt.w,
                    .y1 = new_rtt.h,
                },
                .mask = pp::renderer::gl::framebuffer_color_buffer_mask(),
                .filter = pp::renderer::gl::framebuffer_blit_filter(true),
            },
            pp::renderer::gl::OpenGlFramebufferBlitDispatch {
                .get_integer = query_opengl_integer,
                .bind_framebuffer = bind_opengl_framebuffer,
                .blit_framebuffer = blit_opengl_framebuffer,
            });
        if (!status.ok()) {
            LOG("RTT::resize blit failed because: %s", status.message);
            ret = false;
            return;
        }

        destroy();
    });

    if (!ret)
        return false;
    
    oldRFboID = 0;
    oldDFboID = 0;
    *this = std::move(new_rtt);

    return true;
}

void RTT::destroy()
{
    if (!valid())
        return;
    App::I->render_task_async([rboID=rboID, texID=texID, fboID=fboID]
    {
        if (rboID)
        {
            glDeleteRenderbuffers(1, &rboID);
        }
        if (texID)
        {
            //unbindTexture();
            glDeleteTextures(1, &texID);
            //LOG("TEX rtt destroy %d", texID)
        }
        if (fboID)
        {
            //unbindFramebuffer();
            glDeleteFramebuffers(1, &fboID);
            //LOG("RTT DESTROY %d", fboID);
        }
    });
    texID = 0;
    fboID = 0;
    rboID = 0;
//    w = 0;
//    h = 0;
}

void RTT::copy(const RTT & source)
{
    if (!valid() || !source.valid())
        return;
    App::I->render_task([&]
    {
        const auto status = pp::renderer::gl::blit_opengl_framebuffer(
            pp::renderer::gl::OpenGlFramebufferBlit {
                .source_framebuffer = static_cast<std::uint32_t>(source.fboID),
                .destination_framebuffer = static_cast<std::uint32_t>(fboID),
                .source_rect = pp::renderer::gl::OpenGlFramebufferRect { .x1 = source.w, .y1 = source.h },
                .destination_rect = pp::renderer::gl::OpenGlFramebufferRect { .x1 = w, .y1 = h },
                .mask = pp::renderer::gl::framebuffer_color_buffer_mask(),
                .filter = pp::renderer::gl::framebuffer_blit_filter(true),
            },
            pp::renderer::gl::OpenGlFramebufferBlitDispatch {
                .get_integer = query_opengl_integer,
                .bind_framebuffer = bind_opengl_framebuffer,
                .blit_framebuffer = blit_opengl_framebuffer,
            });
        if (!status.ok())
            LOG("RTT::copy blit failed because: %s", status.message);
    });
}

void RTT::copy(const RTT& source, const glm::vec4& rect)
{
    if (!valid() || !source.valid())
        return;
    App::I->render_task([&]
    {
        auto r = rect_intersection(rect, { 0, 0, w, h });
        const auto status = pp::renderer::gl::blit_opengl_framebuffer(
            pp::renderer::gl::OpenGlFramebufferBlit {
                .source_framebuffer = static_cast<std::uint32_t>(source.fboID),
                .destination_framebuffer = static_cast<std::uint32_t>(fboID),
                .source_rect = pp::renderer::gl::OpenGlFramebufferRect {
                    .x0 = static_cast<std::int32_t>(r.x),
                    .y0 = static_cast<std::int32_t>(r.y),
                    .x1 = static_cast<std::int32_t>(r.z),
                    .y1 = static_cast<std::int32_t>(r.w),
                },
                .destination_rect = pp::renderer::gl::OpenGlFramebufferRect {
                    .x0 = static_cast<std::int32_t>(r.x),
                    .y0 = static_cast<std::int32_t>(r.y),
                    .x1 = static_cast<std::int32_t>(r.z),
                    .y1 = static_cast<std::int32_t>(r.w),
                },
                .mask = pp::renderer::gl::framebuffer_color_buffer_mask(),
                .filter = pp::renderer::gl::framebuffer_blit_filter(false),
            },
            pp::renderer::gl::OpenGlFramebufferBlitDispatch {
                .get_integer = query_opengl_integer,
                .bind_framebuffer = bind_opengl_framebuffer,
                .blit_framebuffer = blit_opengl_framebuffer,
            });
        if (!status.ok())
            LOG("RTT::copy region blit failed because: %s", status.message);
    });
}

RTT RTT::clone() const noexcept
{
    RTT dup;
    dup.create(w, h);
    dup.copy(*this);
    return dup;
}

bool RTT::create(int width, int height)
{
    return create(
        width,
        height,
        -1,
        static_cast<GLint>(pp::renderer::gl::rgba8_internal_format()));
}

bool RTT::create(int width, int height, int tex)
{
    return create(
        width,
        height,
        tex,
        static_cast<GLint>(pp::renderer::gl::rgba8_internal_format()));
}

bool RTT::create(int width, int height, int tex/* = -1*/, GLint internal_format, bool depth_buffer /*= false*/)
{
    GLenum status = 0;
    App::I->render_task([&]
    {
        // Destroy any previously created object
        destroy();

        w = width;
        h = height;
        int_fmt = internal_format;

        if (tex == -1)
        {
            glGenTextures(1, &texID);
            //LOG("TEX rtt create %d", texID);
        }
        else
        {
            texID = tex;
        }

        const auto ifmt = static_cast<GLenum>(
            pp::renderer::gl::texture_upload_type_for_internal_format(static_cast<std::uint32_t>(internal_format)));

        glBindTexture(texture_2d_target(), texID);
        if (tex == -1)
            glTexImage2D(
                texture_2d_target(),
                0,
                internal_format,
                width,
                height,
                0,
                static_cast<GLenum>(pp::renderer::gl::rgba_pixel_format()),
                ifmt,
                0);
        for (const auto parameter : pp::renderer::gl::default_render_target_texture_parameters())
        {
            glTexParameterf(
                texture_2d_target(),
                static_cast<GLenum>(parameter.name),
                static_cast<GLfloat>(parameter.value));
        }
        glBindTexture(texture_2d_target(), 0);

        // Create a renderbuffer object to store depth info
        if (depth_buffer)
        {
            glGenRenderbuffers(1, &rboID);
            glBindRenderbuffer(renderbuffer_target(), rboID);
            glRenderbufferStorage(
                renderbuffer_target(),
                static_cast<GLenum>(pp::renderer::gl::depth_component24_format()),
                width,
                height);
            glBindRenderbuffer(renderbuffer_target(), 0);
        }

        GLint oldFboID;
        glGetIntegerv(static_cast<GLenum>(pp::renderer::gl::draw_framebuffer_binding_query()), &oldFboID);

        // Create a framebuffer object
        glGenFramebuffers(1, &fboID);
        glBindFramebuffer(framebuffer_target(), fboID);
        //LOG("RTT CREATE %d - tex %d", fboID, texID);

        // Attach the texture to FBO color attachment point
        glFramebufferTexture2D(
            framebuffer_target(),
            static_cast<GLenum>(pp::renderer::gl::framebuffer_color_attachment()),
            texture_2d_target(),
            texID,
            0);
        if (depth_buffer)
        {
            // Attach the renderbuffer to depth attachment point
            glFramebufferRenderbuffer(
                framebuffer_target(),
                static_cast<GLenum>(pp::renderer::gl::framebuffer_depth_attachment()),
                renderbuffer_target(),
                rboID);
        }

        // Check FBO status
        status = glCheckFramebufferStatus(framebuffer_target());
        if (status != static_cast<GLenum>(pp::renderer::gl::framebuffer_complete_status()))
            LOG("RTT::create failed because: %s",
                pp::renderer::gl::framebuffer_status_name(static_cast<std::uint32_t>(status)));

        // Switch back to window-system-provided framebuffer
        glBindFramebuffer(framebuffer_target(), oldFboID);
        oldRFboID = 0;
        oldDFboID = 0;
    });

    return status == static_cast<GLenum>(pp::renderer::gl::framebuffer_complete_status());
}

void RTT::bindFramebuffer()
{
    assert(App::I->is_render_thread());
#ifdef _DEBUG
    if (bound)
    {
        LOG("framebuffer bound twice!");
#ifdef _WIN32
        __debugbreak();
#endif
    }
#endif // _DEBUG
    const auto binding = pp::renderer::gl::bind_opengl_framebuffer_for_draw_read(
        static_cast<std::uint32_t>(fboID),
        pp::renderer::gl::OpenGlFramebufferBindDispatch {
            .get_integer = query_opengl_integer,
            .bind_framebuffer = bind_opengl_framebuffer,
        });
    if (!binding.ok()) {
        LOG("RTT::bindFramebuffer() failed because: %s", binding.status().message);
        return;
    }

    oldDFboID = static_cast<GLint>(binding.value().draw_framebuffer);
    oldRFboID = static_cast<GLint>(binding.value().read_framebuffer);
    bound = true;
}

void RTT::unbindFramebuffer()
{
    assert(App::I->is_render_thread());
    if (!bound)
        return;
    const auto status = pp::renderer::gl::restore_opengl_framebuffer_binding(
        pp::renderer::gl::OpenGlFramebufferBindingState {
            .draw_framebuffer = oldDFboID,
            .read_framebuffer = oldRFboID,
        },
        pp::renderer::gl::OpenGlFramebufferRestoreDispatch {
            .bind_framebuffer = bind_opengl_framebuffer,
        });
    if (!status.ok()) {
        LOG("RTT::unbindFramebuffer() failed because: %s", status.message);
        return;
    }

    oldRFboID = 0;
    oldDFboID = 0;
    bound = false;
}

void RTT::clear(glm::vec4 color)
{
    assert(App::I->is_render_thread());
    glClearColor(color.r, color.g, color.b, color.a);
    glClear(static_cast<GLbitfield>(
        pp::renderer::gl::framebuffer_color_buffer_mask()
        | pp::renderer::gl::framebuffer_depth_buffer_mask()));
}

void RTT::clear_mask(glm::bool4 mask, glm::vec4 color)
{
    assert(App::I->is_render_thread());
    // save old state
    std::array<GLboolean, 4> old_mask;
    glGetBooleanv(static_cast<GLenum>(pp::renderer::gl::color_write_mask_query()), old_mask.data());

    // clear with mask
    glColorMask(mask.r, mask.g, mask.b, mask.a);
    glClearColor(color.r, color.g, color.b, color.a);
    glClear(static_cast<GLbitfield>(pp::renderer::gl::framebuffer_color_buffer_mask()));

    // restore old state
    glColorMask(old_mask[0], old_mask[1], old_mask[2], old_mask[3]);
}

glm::ivec4 RTT::calc_bounds() const noexcept
{
    if (!valid())
        return glm::vec4(0);

    auto data = std::unique_ptr<glm::u8vec4[]>(reinterpret_cast<glm::u8vec4*>(readTextureData()));
    glm::ivec2 bbmin(w, h);
    glm::ivec2 bbmax(0);
    for (int y = 0; y < h; y++)
    {
        for (int x = 0; x < w; x++)
        {
            if (data[x + y * w].a > 0)
            {
                bbmin = glm::min(bbmin, { x, y });
                bbmax = glm::max(bbmax, { x + 1, y + 1 });
            }
        }
    }
    return { bbmin, bbmax };
}

uint8_t* RTT::readTextureData(uint8_t* buffer) const noexcept
{
    if (!valid())
        return nullptr;
    if (!buffer)
        buffer = createBuffer();
    App::I->render_task([&]
    {
        const auto readback = pp::renderer::gl::rgba8_readback_format();
        const auto status = pp::renderer::gl::readback_opengl_framebuffer(
            pp::renderer::gl::OpenGlFramebufferReadback {
                .framebuffer = static_cast<std::uint32_t>(fboID),
                .width = w,
                .height = h,
                .format = readback,
                .pixels = buffer,
            },
            pp::renderer::gl::OpenGlFramebufferReadbackDispatch {
                .get_integer = query_opengl_integer,
                .bind_framebuffer = bind_opengl_framebuffer,
                .read_pixels = read_opengl_pixels,
            });
        if (!status.ok())
            LOG("RTT::readTextureData() failed because: %s", status.message);
    });
    return buffer;
}

float* RTT::readTextureDataFloat(float* buffer) const noexcept
{
    if (!valid())
        return nullptr;
    if (!buffer)
        buffer = createBufferFloat();
    App::I->render_task([&]
    {
        const auto readback = pp::renderer::gl::rgba32f_readback_format();
        const auto status = pp::renderer::gl::readback_opengl_framebuffer(
            pp::renderer::gl::OpenGlFramebufferReadback {
                .framebuffer = static_cast<std::uint32_t>(fboID),
                .width = w,
                .height = h,
                .format = readback,
                .pixels = buffer,
            },
            pp::renderer::gl::OpenGlFramebufferReadbackDispatch {
                .get_integer = query_opengl_integer,
                .bind_framebuffer = bind_opengl_framebuffer,
                .read_pixels = read_opengl_pixels,
            });
        if (!status.ok())
            LOG("RTT::readTextureDataFloat() failed because: %s", status.message);
    });
    return buffer;
}

uint8_t* RTT::createBuffer() const noexcept
{
    const auto readback = pp::renderer::gl::rgba8_readback_format();
    return new uint8_t[pp::renderer::gl::readback_byte_count(
        readback,
        static_cast<std::uint32_t>(w),
        static_cast<std::uint32_t>(h))];
}

float * RTT::createBufferFloat() const noexcept
{
    const auto readback = pp::renderer::gl::rgba32f_readback_format();
    return new float[pp::renderer::gl::readback_byte_count(
                         readback,
                         static_cast<std::uint32_t>(w),
                         static_cast<std::uint32_t>(h))
                     / sizeof(float)];
}

void RTT::bindTexture()
{
    assert(App::I->is_render_thread());
    glBindTexture(texture_2d_target(), texID);
}

void RTT::unbindTexture()
{
    assert(App::I->is_render_thread());
    glBindTexture(texture_2d_target(), 0);
}

Image RTT::get_image() const noexcept
{
    Image ret;
    ret.create(w, h, readTextureData());
    return ret;
}

bool RTT::valid() const noexcept
{
    return texID || rboID || fboID;
}

//////////////////////////////////////////////////////////////////////////

PBO::PBO(PBO&& other) noexcept
{
    bound_slot = other.bound_slot;
    buffer_id = other.buffer_id;
    mapped_ptr = other.mapped_ptr;
    width = other.width;
    height = other.height;
    other.bound_slot = 0;
    other.buffer_id = 0;
    other.mapped_ptr = nullptr;
    other.width = 0;
    other.height = 0;
}

PBO& PBO::operator=(PBO&& other) noexcept
{
    bound_slot = other.bound_slot;
    buffer_id = other.buffer_id;
    mapped_ptr = other.mapped_ptr;
    width = other.width;
    height = other.height;
    other.bound_slot = 0;
    other.buffer_id = 0;
    other.mapped_ptr = nullptr;
    other.width = 0;
    other.height = 0;
    return *this;
}

PBO::~PBO() noexcept
{
    destroy();
}

bool PBO::create() noexcept
{
    App::I->render_task([this] {
        glGenBuffers(1, &buffer_id);
    });
    return true;
}

bool PBO::create(RTT& rtt) noexcept
{
    App::I->render_task([this, &rtt] {
        width = rtt.getWidth();
        height = rtt.getHeight();
        const auto readback = pp::renderer::gl::rgba8_readback_format();
        const auto buffer_target = static_cast<GLenum>(pp::renderer::gl::pixel_pack_buffer_target());
        rtt.bindFramebuffer();
        glGenBuffers(1, &buffer_id);
        glBindBuffer(buffer_target, buffer_id);
        glBufferData(
            buffer_target,
            static_cast<GLsizeiptr>(pp::renderer::gl::readback_byte_count(
                readback,
                static_cast<std::uint32_t>(width),
                static_cast<std::uint32_t>(height))),
            0,
            static_cast<GLenum>(pp::renderer::gl::pixel_buffer_stream_read_usage()));
        glReadPixels(
            0,
            0,
            width,
            height,
            static_cast<GLenum>(readback.pixel_format),
            static_cast<GLenum>(readback.component_type),
            0);
        glBindBuffer(buffer_target, 0);
        rtt.unbindFramebuffer();
    });
    return true;
}

void PBO::destroy() noexcept
{
    if (buffer_id)
    {
        App::I->render_task_async([id=buffer_id] {
            glDeleteBuffers(1, &id);
        });
        buffer_id = 0;
        bound_slot = 0;
        width = 0;
        height = 0;
        mapped_ptr = nullptr;
    }
}

/*
void PBO::bind_read() noexcept
{
    App::I->render_task([this] {
        const auto buffer_target = static_cast<GLenum>(pp::renderer::gl::pixel_unpack_buffer_target());
        glBindBuffer(buffer_target, buffer_id);
        bound_slot = buffer_target;
    });
}

void PBO::unbind() noexcept
{
    App::I->render_task([this] {
        glBindBuffer(bound_slot, buffer_id);
    });
}
*/

glm::uint8_t* PBO::map() noexcept
{
    App::I->render_task([this] {
        const auto readback = pp::renderer::gl::rgba8_readback_format();
        const auto buffer_target = static_cast<GLenum>(pp::renderer::gl::pixel_pack_buffer_target());
        glBindBuffer(buffer_target, buffer_id);
        mapped_ptr = (GLubyte*)glMapBufferRange(buffer_target, 0,
            static_cast<GLsizeiptr>(pp::renderer::gl::readback_byte_count(
                readback,
                static_cast<std::uint32_t>(width),
                static_cast<std::uint32_t>(height))),
            static_cast<GLbitfield>(pp::renderer::gl::pixel_buffer_map_read_access()));
        glBindBuffer(buffer_target, 0);
    });
    return mapped_ptr;
}

void PBO::unmap() noexcept
{
    App::I->render_task([this] {
        const auto buffer_target = static_cast<GLenum>(pp::renderer::gl::pixel_pack_buffer_target());
        glBindBuffer(buffer_target, buffer_id);
        glUnmapBuffer(buffer_target);
        glBindBuffer(buffer_target, 0);
    });
}
