/*
 *  <Short Description>
 *  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/window.h>
#include <blt/gfx/renderer/postprocess.h>
#include <blt/gfx/renderer/shaders/postprocess.vert>
#include <blt/gfx/renderer/shaders/pp_screen.frag>
#include <blt/gfx/renderer/shaders/pp_outline_step.frag>
#include <blt/gfx/renderer/shaders/pp_gaussian_blur.frag>
#include <blt/gfx/renderer/shaders/pp_overlay_blur.frag>
#include <blt/gfx/renderer/shaders/pp_multiplier.frag>
#include <blt/gfx/renderer/shaders/pp_highlight.frag>
#include <blt/std/ranges.h>
#include <blt/math/log_util.h>

#define __EMSCRIPTEN__

float full_screen_vertices[20] = {
        1.0f, 1.0f, 0.0f, 1.0f, 1.0f,
        1.0f, -1.0f, 0.0f, 1.0f, 0.0f,
        -1.0f, -1.0f, 0.0f, 0.0f, 0.0f,
        -1.0f, 1.0f, 0.0f, 0.0f, 1.0f
};
const unsigned int full_screen_indices[6] = {
        0, 1, 3,
        1, 2, 3
};

template<typename T>
bool once()
{
    static bool ran = true;
    return std::exchange(ran, false);
}

namespace blt::gfx
{
    void pp_engine_t::create()
    {
        {
            vertex_buffer_t vertices_vbo;
            element_buffer_t indices_vbo;
            
            vertices_vbo.create();
            indices_vbo.create();
            
            vertices_vbo.allocate(sizeof(full_screen_vertices), full_screen_vertices);
            indices_vbo.allocate(sizeof(full_screen_indices), full_screen_indices);
            
            screen_vao = std::make_unique<vertex_array_t>();
            auto vb = vertex_array_t::make_vbo(vertices_vbo);
            screen_vao->bindVBO(vb, 0, 3, GL_FLOAT, 5 * sizeof(float), 0);
            screen_vao->bindVBO(vb, 1, 2, GL_FLOAT, 5 * sizeof(float), 3 * sizeof(float));
            screen_vao->bindElement(indices_vbo);
        }
#ifdef __EMSCRIPTEN__
        addStep(std::make_unique<pp_to_screen_step_t>());
#endif
        for (auto& step : steps)
            step->create();
    }
    
    pp_step_t& pp_engine_t::find_last_frame_buffer(size_t index)
    {
        for (i64 i = static_cast<i64>(index); i >= 0; i--)
        {
            if (steps[i]->requests(pp_request_t::BIND_BUFFER))
                return *steps[i];
        }
        BLT_FATAL("We reached the end trying to find a previous frame buffer, unable to do so! Please ensure this in-place step has a framebuffer!");
        std::exit(1);
    }
    
    void pp_engine_t::render()
    {
        screen_vao->bind();
        for (const auto& [index, step] : blt::enumerate(steps))
        {
            if (index == 0)
                continue;
            auto& previous = find_last_frame_buffer(index - 1);
            if (step->requests(pp_request_t::BIND_BUFFER))
                step->getBuffer().bind();
            else
                previous.getBuffer().bind();
            if (step->requests(pp_request_t::CLEAR_BUFFER))
                glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
            step->draw(previous.getBuffer());
            if (step->hasShader())
                step->getShader().bind();
            render_quad();
            step->post_draw(previous.getBuffer());
        }
        glBindTexture(GL_TEXTURE_2D, 0);
        frame_buffer_t::unbind();
#ifndef __EMSCRIPTEN__
        steps.back()->getBuffer().blitToScreen(getWindowWidth(), getWindowHeight());
#endif
    }
    
    void pp_engine_t::cleanup()
    {
        screen_vao = nullptr;
        for (auto& v : steps)
            v = nullptr;
        steps.clear();
    }
    
    std::unique_ptr<shader_t> pp_engine_t::createShader(std::string_view fragment, std::optional<std::reference_wrapper<template_engine_t>> engine)
    {
        std::unique_ptr<shader_t> shader;
        if (engine)
        {
            shader = shader_t::make_unique(engine, shader_postprocess_vert, std::string(fragment));
        } else
        {
            template_engine_t templateEngine;
            templateEngine.set("LAYOUT_STRING", "");
            shader = shader_t::make_unique(templateEngine, shader_postprocess_vert, std::string(fragment));
        }
        shader->bindAttribute(0, "vertex");
        shader->bindAttribute(1, "uv_in");
        return shader;
    }
    
    void pp_engine_t::bind()
    {
        steps.front()->getBuffer().bind();
        steps.front()->draw();
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
    }
    
    void pp_engine_t::render_quad()
    {
        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
    }
    
    void pp_in_place_t::create()
    {
        template_engine_t engine;
        engine.set("LAYOUT_STRING", "layout (location = ${IF(LAYOUT_LOCATION) { LAYOUT_LOCATION } ELSE { ~DISCARD }}) ");
        engine.set("LAYOUT_LOCATION", std::to_string(static_cast<int>(to) - static_cast<int>(frame_buffer_t::attachment_t::COLOR0)));
        shader_pass = pp_engine_t::createShader(shader_pp_screen_frag, engine);
        draw_buffer = frame_buffer_t::make_render_texture(getWindowWidth(), getWindowHeight());
    }
    
    void pp_in_place_t::post_draw(frame_buffer_t& previous)
    {
//        draw_buffer.blitTexture(previous, 0, 0, 0, 0, GL_NEAREST, from, to);
        handle_errors();
        previous.bind();
        GLenum buf[32];
        auto to_val = static_cast<GLenum>(to);
        auto size = to_val - GL_COLOR_ATTACHMENT0;
        for (GLenum i = 0; i < size; i++)
            buf[i] = GL_NONE;
        buf[size] = to_val;
        glDrawBuffers(static_cast<int>(size) + 1, buf);
        glClear(GL_COLOR_BUFFER_BIT);
        shader_pass->bind();
        glActiveTexture(GL_TEXTURE0);
        draw_buffer.getTexture(frame_buffer_t::attachment_t::COLOR0).bind();
        pp_engine_t::render_quad();
    }
    
    void pp_in_place_t::draw(frame_buffer_t& previous)
    {
        draw_buffer.updateBuffersStorage(getWindowWidth(), getWindowHeight());
        glActiveTexture(GL_TEXTURE0);
        previous.getTexture(from).bind();
        draw_buffer.bind();
        GLenum buf[]{static_cast<GLenum>(frame_buffer_t::attachment_t::COLOR0)};
        glDrawBuffers(1, buf);
        glClear(GL_COLOR_BUFFER_BIT);
    }
    
    void pp_to_screen_step_t::create()
    {
        shader = pp_engine_t::createShader(shader_pp_screen_frag);
    }
    
    void pp_to_screen_step_t::draw(frame_buffer_t& previous)
    {
        glActiveTexture(GL_TEXTURE0);
        previous.getTexture(frame_buffer_t::attachment_t::COLOR0).bind();
        GLenum buf[]{GL_BACK};
        glDrawBuffers(1, buf);
    }
    
    void pp_render_target_t::create()
    {
        draw_buffer = frame_buffer_t::make_render_texture(getWindowWidth(), getWindowHeight());
    }
    
    void pp_render_target_t::draw()
    {
        draw_buffer.updateBuffersStorage(getWindowWidth(), getWindowHeight());
        GLenum buf[]{static_cast<GLenum>(frame_buffer_t::attachment_t::COLOR0)};
        glDrawBuffers(1, buf);
    }
    
    void pp_outline_target_t::create()
    {
        draw_buffer.create();
        draw_buffer.bind();
        
        auto* texture = new texture_gl2D(getWindowWidth(), getWindowHeight(), GL_RGBA8);
        draw_buffer.attachTexture(texture, frame_buffer_t::attachment_t::COLOR0);
        
        auto* mask = new texture_gl2D(getWindowWidth(), getWindowHeight(), GL_RGBA8);
        draw_buffer.attachTexture(mask, frame_buffer_t::attachment_t::COLOR1);
        
        render_buffer_t depth_rbo = render_buffer_t::make_render_buffer(GL_DEPTH24_STENCIL8, getWindowWidth(), getWindowHeight());
        draw_buffer.attachRenderBuffer(depth_rbo, frame_buffer_t::attachment_t::DEPTH_STENCIL);
        
        if (!frame_buffer_t::validate())
            BLT_ERROR("Failed to create render texture framebuffer!");
        frame_buffer_t::unbind();
    }
    
    void pp_outline_target_t::draw()
    {
        draw_buffer.updateBuffersStorage(getWindowWidth(), getWindowHeight());
        const GLenum buffers[]{static_cast<GLenum>(frame_buffer_t::attachment_t::COLOR0), static_cast<GLenum>(frame_buffer_t::attachment_t::COLOR1)};
        glDrawBuffers(2, buffers);
    }
    
    void pp_outline_step_t::create()
    {
        draw_buffer = frame_buffer_t::make_render_texture(getWindowHeight(), getWindowHeight());
        shader = pp_engine_t::createShader(shader_pp_outline_step_frag);
        shader->setInt("albedo", 0);
        shader->setInt("mask", 1);
    }
    
    void pp_outline_step_t::draw(frame_buffer_t& previous)
    {
        draw_buffer.updateBuffersStorage(getWindowWidth(), getWindowHeight());
        glActiveTexture(GL_TEXTURE0);
        previous.getTexture(frame_buffer_t::attachment_t::COLOR0).bind();
        glActiveTexture(GL_TEXTURE1);
        previous.getTexture(frame_buffer_t::attachment_t::COLOR1).bind();
        shader->bind();
        //auto v = vec2(getWindowWidth(), getWindowHeight()) * (1.0 / blt::make_vec2(manager.getScale2D()));
        //BLT_TRACE_STREAM << v << '\n';
        //shader->setVec2("viewportSize", static_cast<f32>(v.x()), static_cast<f32>(v.y()));
        GLenum buf[]{static_cast<GLenum>(frame_buffer_t::attachment_t::COLOR0)};
        glDrawBuffers(1, buf);
    }
    
    void pp_blur_step_inplace_t::create()
    {
        pp_in_place_t::create();
        shader = pp_engine_t::createShader(shader_gaussian_blur_frag);
    }
    
    void pp_blur_step_inplace_t::draw(frame_buffer_t& previous)
    {
        pp_in_place_t::draw(previous);
        shader->bind();
        //auto v = vec2(getWindowWidth(), getWindowHeight()) * (1.0 / blt::make_vec2(manager.getScale2D()));
        auto v = vec2(getWindowWidth(), getWindowHeight());
        shader->setVec4i("size", {static_cast<i32>(v.x()), static_cast<i32>(v.y()), x_blur, y_blur});
    }
    
    void pp_multiplier_step_inplace_t::create()
    {
        pp_in_place_t::create();
        shader = pp_engine_t::createShader(shader_multiplier_frag);
    }
    
    void pp_multiplier_step_inplace_t::draw(frame_buffer_t& previous)
    {
        pp_in_place_t::draw(previous);
        shader->bind();
        shader->setVec4("multiplier", multiplier);
    }
    
    void pp_overlay_blur_step_t::create()
    {
        pp_in_place_t::create();
        shader = pp_engine_t::createShader(shader_overlay_blur_frag);
    }
    
    void pp_overlay_blur_step_t::draw(frame_buffer_t& previous)
    {
        pp_in_place_t::draw(previous);
        shader->bind();
        auto v = vec2(getWindowWidth(), getWindowHeight());
        shader->setVec4i("size", {static_cast<i32>(v.x()), static_cast<i32>(v.y()), x_blur, y_blur});
    }
    
    void pp_mouse_highlight_step_t::create()
    {
        pp_in_place_t::create();
        shader = pp_engine_t::createShader(shader_highlight_frag);
    }
    
    void pp_mouse_highlight_step_t::draw(frame_buffer_t& previous)
    {
        pp_in_place_t::draw(previous);
        shader->bind();
        auto v = vec2(getWindowWidth(), getWindowHeight());
        shader->setVec4("mousePos", blt::make_vec4(blt::vec2{getMouseX(), v.y() - getMouseY()}));
        shader->setVec4i("size", {static_cast<i32>(v.x()), static_cast<i32>(v.y()), 4, 4});
    }
}