/*
 * This file is part of vimix - video live mixer
 *
 * **Copyright** (C) 2019-2023 Bruno Herbelin <bruno.herbelin@gmail.com>
 *
 * 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 <glib.h>
#include <glm/ext/matrix_transform.hpp>
#include <glm/fwd.hpp>
#include <glm/geometric.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/matrix.hpp>

#include "defines.h"
#include "Log.h"
#include "FrameBuffer.h"
#include "Scene/Decorations.h"
#include "Resource.h"
#include "Visitor/Visitor.h"
#include "Session.h"
#include "Visitor/DrawVisitor.h"
#include "Canvas.h"
#include "CanvasSource.h"

#include "RenderSource.h"

std::vector< std::tuple<int, int, std::string> > RenderSource::ProvenanceMethod = {
    { 16, 12, "Recursive" }, 
    { 17, 5, "Entire scene", }, 
    { 17, 12, "Local scene" },
    { 2, 10, "Canvas" }
};

RenderSource::RenderSource(uint64_t id) : Source(id), session_(nullptr), runtime_(0), rendered_output_(nullptr),
    paused_(false), reset_(true), provenance_(RENDER_TEXTURE), canvas_index_(0)
{
    // set symbol
    symbol_ = new Symbol(Symbol::RENDER, glm::vec3(0.75f, 0.75f, 0.01f));
    symbol_->scale_.y = 1.5f;
}

RenderSource::~RenderSource()
{
    if (rendered_output_ != nullptr)
        delete rendered_output_;
}

Source::Failure RenderSource::failed() const
{
    if ( rendered_output_ != nullptr && session_ != nullptr ) {
        // the renreding output was created, but resolution changed
        if ( rendered_output_->resolution() != session_->frame()->resolution()
             // or alpha channel changed (e.g. inside SessionSource)
             || ( ( rendered_output_->flags() & FrameBuffer::FrameBuffer_alpha ) != ( session_->frame()->flags() & FrameBuffer::FrameBuffer_alpha ) )
             ) {
            return FAIL_RETRY;
        }
    }

    return FAIL_NONE;
}

uint RenderSource::texture() const
{
    if (rendered_output_ != nullptr)
        return rendered_output_->texture();
    else if (session_ && session_->frame())
        return session_->frame()->texture();
    else
        return Resource::getTextureBlack(); // getTextureTransparent ?
}

void RenderSource::init()
{
    if (session_ && session_->frame() && session_->frame()->texture() != Resource::getTextureBlack()) {

        FrameBuffer *fb = session_->frame();

        // use same flags than session frame, without multisampling
        FrameBuffer::FrameBufferFlags flag = fb->flags();
        flag &= ~FrameBuffer::FrameBuffer_multisampling;

        // create the frame buffer displayed by the source (all modes)
        if (rendered_output_ != nullptr)
            delete rendered_output_;
        rendered_output_ = new FrameBuffer( fb->resolution(), flag );

        // needs a first initialization (to get texture)
        fb->blit(rendered_output_);

        // set the texture index from internal framebuffer, apply it to the source texture surface
        texturesurface_->setTextureIndex( rendered_output_->texture() );

        // create Frame buffer matching size of output session
        FrameBuffer *renderbuffer = new FrameBuffer( fb->resolution() );

        // set the renderbuffer of the source and attach rendering nodes
        attach(renderbuffer);

        // deep update to reorder
        ++View::need_deep_update_;

        // done init
        Log::Info("Source '%s' linked to output (%d x %d).", name().c_str(), int(fb->resolution().x), int(fb->resolution().y) );
    }
}

void RenderSource::update(float dt)
{
    static glm::mat4 projection = glm::ortho(-1.f, 1.f, 1.f, -1.f, -SCENE_DEPTH, 1.f);

    Source::update(dt);

    if (session_ && session_->ready() && rendered_output_) {
        
        if ((active_ && !paused_) || reset_) {

            // simulate a rendering of the session in a framebuffer
            if (provenance_ >= RENDER_PROJECTION || reset_ ) {

                if (provenance_ == RENDER_CANVAS) {
                    
                    CanvasSurface *canvas = Canvas::manager().at(canvas_index_);

                    if (canvas != nullptr && canvas->frame()) {
                        canvas->frame()->blit(rendered_output_);
                    }
                }
                else if (provenance_ == RENDER_PROJECTION_SOURCE) {

                    // temporarily set the scene root transform to the inverse of the source transform
                    glm::vec3 rotation = groups_[View::RENDERING]->rotation_;
                    glm::vec3 scale = groups_[View::RENDERING]->scale_ ;
                    scale.z = 1.f;
                    glm::vec3 translation =  groups_[View::RENDERING]->translation_;
                    translation.z = 0.f;
                    session_->render_.scene.root()->transform_ = glm::inverse( GlmToolkit::transform(translation, rotation, scale) );

                    // add all sources below this one in the scene graph
                    std::vector<Node *> surfaces;
                    float threshold_z = groups_[View::RENDERING]->translation_.z;   
                    if (threshold_z < 0.f)
                        threshold_z = LAYER_FOREGROUND;
                    for (auto sit = session_->begin(); sit != session_->end(); ++sit) {
                        if ((*sit)->group(View::RENDERING)->translation_.z < threshold_z)
                            surfaces.push_back((*sit)->group(View::RENDERING));
                    }

                    // change projection to account for CROP (inverse transform)
                    glm::vec3 _c_s = glm::vec3(groups_[View::GEOMETRY]->crop_[0] - groups_[View::GEOMETRY]->crop_[1],
                            groups_[View::GEOMETRY]->crop_[2] - groups_[View::GEOMETRY]->crop_[3],
                            2.f) * 0.5f ;
                    glm::vec3 _t((_c_s.x + groups_[View::GEOMETRY]->crop_[1]),
                                 (_c_s.y + groups_[View::GEOMETRY]->crop_[3]), 0.f);
                    
                    glm::mat4 P = glm::scale(glm::translate(projection, _t), _c_s * glm::vec3(-1.f / rendered_output_->aspectRatio(), 1.f, 1.f));

                    // access to private RenderView in the session to call draw on selected surfaces
                    rendered_output_->begin();
                    DrawVisitor draw_surfaces(surfaces, P);
                    session_->render_.scene.accept(draw_surfaces);
                    rendered_output_->end();

                    // restore scene root transform
                    session_->render_.scene.root()->transform_ = glm::identity<glm::mat4>();
                }
                // RENDER_PROJECTION or reset
                else {

                    // temporarily exclude this RenderSource from the rendering
                    groups_[View::RENDERING]->visible_ = false;

                    // change projection to account for aspect ratio
                    glm::mat4 P = glm::scale(projection,
                                         glm::vec3(1.f / rendered_output_->aspectRatio(), 1.f, 1.f));

                    // access to private RenderView in the session to call draw on the root of the scene
                    rendered_output_->begin();
                    session_->render_.scene.root()->draw(glm::identity<glm::mat4>(), P);
                    rendered_output_->end();

                    // restore this RenderSource visibility
                    groups_[View::RENDERING]->visible_ = true;
                }
                // done reset
                reset_ = false;
            }
            // RENDER_TEXTURE
            // blit session frame to output (fastest)
            else if (!session_->frame()->blit(rendered_output_))
            {
                // if failed (which should not happen),
                // simulate a rendering of the session in a framebuffer
                glm::mat4 P  = glm::scale( projection, glm::vec3(1.f / rendered_output_->aspectRatio(), 1.f, 1.f));
                rendered_output_->begin();
                // access to private RenderView in the session to call draw on the root of the scene
                session_->render_.scene.root()->draw(glm::identity<glm::mat4>(), P);
                rendered_output_->end();
            }

            runtime_ = session_->runtime();
        }
    }

}

void RenderSource::play (bool on)
{
    // toggle state
    paused_ = !on;
}

void RenderSource::replay ()
{
    // request next frame to reset
    reset_ = true;
}

void RenderSource::reload ()
{
    // reset renderbuffer_
    if (renderbuffer_)
        delete renderbuffer_;
    renderbuffer_ = nullptr;

    // request next frame to reset
    reset_ = true;
}

glm::vec3 RenderSource::resolution() const
{
    if (rendered_output_ != nullptr)
        return rendered_output_->resolution();
    else if (session_ && session_->frame())
        return session_->frame()->resolution();
    else
        return glm::vec3(0.f);
}

void RenderSource::accept(Visitor& v)
{
    Source::accept(v);
    v.visit(*this);
}

glm::ivec2 RenderSource::icon() const
{
    return glm::ivec2(ICON_SOURCE_RENDER);
}

std::string RenderSource::info() const
{
    return "Display loopback";
}
