/*
 *  Copyright (C) 2024  Brett Terpstra
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
#include <blt/gfx/framebuffer.h>
#include "blt/std/assert.h"

namespace blt::gfx
{
    void render_buffer_t::create()
    {
        glGenRenderbuffers(1, &rboID);
    }
    
    void render_buffer_t::bind() const
    {
        glBindRenderbuffer(GL_RENDERBUFFER, rboID);
    }
    
    void render_buffer_t::unbind()
    {
        glBindRenderbuffer(GL_RENDERBUFFER, 0);
    }
    
    void render_buffer_t::destroy()
    {
        glDeleteRenderbuffers(1, &rboID);
    }
    
    void render_buffer_t::updateStorage(blt::i32 width, blt::i32 height)
    {
        width_ = width;
        height_ = height;
        samples = 0;
        glRenderbufferStorage(GL_RENDERBUFFER, storage_type, width, height);
    }
    
    void render_buffer_t::updateStorageMultiSampled(blt::i32 width, blt::i32 height, blt::i32 s)
    {
        width_ = width;
        height_ = height;
        samples = s;
        glRenderbufferStorageMultisample(GL_RENDERBUFFER, samples, storage_type, width, height);
    }
    
    void render_buffer_t::setStorage(GLuint s_type, blt::i32 width, blt::i32 height)
    {
        storage_type = s_type;
        updateStorage(width, height);
    }
    
    void render_buffer_t::setStorageMultiSampled(GLuint s_type, blt::i32 width, blt::i32 height, blt::i32 s)
    {
        storage_type = s_type;
        updateStorageMultiSampled(width, height, s);
    }
    
    void render_buffer_t::resize(blt::i32 width, blt::i32 height)
    {
        if (samples > 0)
            updateStorageMultiSampled(width, height, samples);
        else
            updateStorage(width, height);
    }
    
    render_buffer_t render_buffer_t::make_render_buffer(GLuint storage_type, blt::i32 width, blt::i32 height)
    {
        render_buffer_t rbo{};
        rbo.create();
        rbo.bind();
        rbo.setStorage(storage_type, width, height);
        render_buffer_t::unbind();
        return rbo;
    }
    
    render_buffer_t render_buffer_t::make_render_buffer(GLuint storage_type, blt::i32 width, blt::i32 height, blt::i32 samples)
    {
        render_buffer_t rbo{};
        rbo.create();
        rbo.bind();
        rbo.setStorageMultiSampled(storage_type, width, height, samples);
        render_buffer_t::unbind();
        return rbo;
    }
    
    void frame_buffer_t::create(frame_buffer_t::draw_t type)
    {
        generic_bind_type = static_cast<GLuint>(type);
        glGenFramebuffers(1, &fboID);
    }
    
    void frame_buffer_t::bind(draw_t type) const
    {
        glBindFramebuffer(static_cast<GLuint>(type), fboID);
    }
    
    void frame_buffer_t::unbind()
    {
        glBindFramebuffer(GL_FRAMEBUFFER, 0);
    }
    
    void frame_buffer_t::destroy()
    {
        if (fboID == 0)
            return;
        glDeleteFramebuffers(1, &fboID);
        for (auto& v : render_buffers)
            v.destroy();
        for (auto* texture : texture_buffers)
            delete texture;
    }
    
    bool frame_buffer_t::validate()
    {
        return glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE;
    }
    
    void frame_buffer_t::attachTexture(blt::gfx::texture_gl* texture, attachment_t attachment, int level)
    {
        if (width_ == -1)
        {
            width_ = texture->getWidth();
            height_ = texture->getHeight();
        } else
            BLT_ASSERT_MSG(width_ == texture->getWidth() && height_ == texture->getHeight(), "Please ensure attached textures are the same size!");
        texture_buffers.push_back(texture);
        glFramebufferTexture2D(generic_bind_type, static_cast<GLuint>(attachment), texture->getBindType(), texture->getTextureID(), level);
    }
    
    void frame_buffer_t::attachRenderBuffer(render_buffer_t rbo, frame_buffer_t::attachment_t attachment)
    {
        if (width_ == -1)
        {
            width_ = rbo.width_;
            height_ = rbo.height_;
        } else
            BLT_ASSERT_MSG(width_ == rbo.width_ && height_ == rbo.height_, "Please ensure attached renderbuffer is the same size!");
        rbo.bind();
        render_buffers.push_back(rbo);
        glFramebufferRenderbuffer(generic_bind_type, static_cast<GLuint>(attachment), GL_RENDERBUFFER, rbo.rboID);
    }
    
    void frame_buffer_t::updateBuffersStorage(blt::i32 width, blt::i32 height)
    {
        if (width == width_ && height == height_)
            return;
        if (fboID == 0)
            return;
        
        width_ = width;
        height_ = height;
        for (auto& rbo : render_buffers)
        {
            rbo.bind();
            rbo.resize(width, height);
        }
        
        for (auto& texture : texture_buffers)
            texture->updateSize(width, height);
        
        if (!frame_buffer_t::validate())
            BLT_ERROR("Failed to update framebuffer storage size!");
    }
    
    void frame_buffer_t::blitTexture(const frame_buffer_t& draw, blt::i32 srcX, blt::i32 srcY, blt::i32 destX, blt::i32 destY, GLuint filter,
                                     attachment_t attachment_read, attachment_t attachment_write) const
    {
        glBindFramebuffer(GL_READ_FRAMEBUFFER, fboID);
        glBindFramebuffer(GL_DRAW_FRAMEBUFFER, draw.fboID);
        glReadBuffer(static_cast<GLenum>(attachment_read));
        GLenum buffers[] {static_cast<GLenum>(attachment_write)};
        glDrawBuffers(1, buffers);
        glBlitFramebuffer(srcX, srcY, width_, height_, destX, destY, draw.width_, draw.height_, GL_COLOR_BUFFER_BIT, filter);
    }
    
    void frame_buffer_t::blitDepth(const frame_buffer_t& draw, blt::i32 srcX, blt::i32 srcY, blt::i32 destX, blt::i32 destY, GLuint filter) const
    {
        glBindFramebuffer(GL_READ_FRAMEBUFFER, fboID);
        glBindFramebuffer(GL_DRAW_FRAMEBUFFER, draw.fboID);
        glReadBuffer(static_cast<GLenum>(attachment_t::DEPTH));
        GLenum buffers[] {static_cast<GLenum>(attachment_t::DEPTH)};
        glDrawBuffers(1, buffers);
        glBlitFramebuffer(srcX, srcY, width_, height_, destX, destY, draw.width_, draw.height_, GL_DEPTH_BUFFER_BIT, filter);
    }
    
    void frame_buffer_t::blitStencil(const frame_buffer_t& draw, blt::i32 srcX, blt::i32 srcY, blt::i32 destX, blt::i32 destY, GLuint filter) const
    {
        glBindFramebuffer(GL_READ_FRAMEBUFFER, fboID);
        glBindFramebuffer(GL_DRAW_FRAMEBUFFER, draw.fboID);
        glReadBuffer(static_cast<GLenum>(attachment_t::STENCIL));
        GLenum buffers[] {static_cast<GLenum>(attachment_t::STENCIL)};
        glDrawBuffers(1, buffers);
        glBlitFramebuffer(srcX, srcY, width_, height_, destX, destY, draw.width_, draw.height_, GL_STENCIL_BUFFER_BIT, filter);
    }
    
    void frame_buffer_t::blitToScreen(blt::i32 width, blt::i32 height) const
    {
        glBindFramebuffer(GL_READ_FRAMEBUFFER, fboID);
        glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
        //glReadBuffer(GL_COLOR_ATTACHMENT0);
        //glDrawBuffer(GL_COLOR_ATTACHMENT0);
        glReadBuffer(static_cast<GLenum>(attachment_t::COLOR0));
        GLenum buffers[] {GL_BACK};
        glDrawBuffers(1, buffers);
        glBlitFramebuffer(0, 0, width_, height_, 0, 0, width, height, GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT, GL_NEAREST);
    }
    
    frame_buffer_t frame_buffer_t::make_render_texture(blt::i32 width, blt::i32 height)
    {
        frame_buffer_t fbo;
        
        fbo.create();
        fbo.bind();
        
        auto* texture = new texture_gl2D(width, height, GL_RGBA8);
        fbo.attachTexture(texture, attachment_t::COLOR0);
        
        render_buffer_t depth_rbo = render_buffer_t::make_render_buffer(GL_DEPTH24_STENCIL8, width, height);
        fbo.attachRenderBuffer(depth_rbo, attachment_t::DEPTH_STENCIL);
        
        if (!frame_buffer_t::validate())
            BLT_ERROR("Failed to create render texture framebuffer!");
        frame_buffer_t::unbind();
        
        return fbo;
    }
    
    frame_buffer_t frame_buffer_t::make_multisample_render_texture(blt::i32 width, blt::i32 height, blt::i32 samples)
    {
        frame_buffer_t fbo;
        
        fbo.create();
        fbo.bind();
        
        auto* texture = new texture_gl2D_multisample(width, height, samples);
        fbo.attachTexture(texture, attachment_t::COLOR0);
        
        render_buffer_t depth_rbo = render_buffer_t::make_render_buffer(GL_DEPTH24_STENCIL8, width, height, samples);
        fbo.attachRenderBuffer(depth_rbo, attachment_t::DEPTH_STENCIL);
        
        if (!frame_buffer_t::validate())
            BLT_ERROR("Failed to create render texture framebuffer!");
        frame_buffer_t::unbind();
        
        return fbo;
    }
    
    frame_buffer_t frame_buffer_t::make_multisample_render_target(blt::i32 width, blt::i32 height, blt::i32 samples)
    {
        frame_buffer_t fbo;
        
        fbo.create();
        fbo.bind();
        
        render_buffer_t color_rbo = render_buffer_t::make_render_buffer(GL_RGBA8, width, height, samples);
        render_buffer_t depth_rbo = render_buffer_t::make_render_buffer(GL_DEPTH24_STENCIL8, width, height, samples);
        
        fbo.attachRenderBuffer(color_rbo, attachment_t::COLOR0);
        fbo.attachRenderBuffer(depth_rbo, attachment_t::DEPTH_STENCIL);
        
        if (!frame_buffer_t::validate())
            BLT_ERROR("Failed to create multi-sampled render texture framebuffer!");
        frame_buffer_t::unbind();
        
        return fbo;
    }
    
    frame_buffer_t frame_buffer_t::make_render_target(blt::i32 width, blt::i32 height)
    {
        frame_buffer_t fbo;
        
        fbo.create();
        fbo.bind();
        
        render_buffer_t color_rbo = render_buffer_t::make_render_buffer(GL_RGBA8, width, height);
        render_buffer_t depth_rbo = render_buffer_t::make_render_buffer(GL_DEPTH24_STENCIL8, width, height);
        
        fbo.attachRenderBuffer(color_rbo, attachment_t::COLOR0);
        fbo.attachRenderBuffer(depth_rbo, attachment_t::DEPTH_STENCIL);
        
        if (!frame_buffer_t::validate())
            BLT_ERROR("Failed to create render texture framebuffer!");
        frame_buffer_t::unbind();
        
        return fbo;
    }
    
    void frame_buffer_t::bindScreen(frame_buffer_t::draw_t type)
    {
        glBindFramebuffer(static_cast<i32>(type), 0);
    }
}