/******************************************************************************
*       SOFA, Simulation Open-Framework Architecture, development version     *
*                (c) 2006-2017 INRIA, USTL, UJF, CNRS, MGH                    *
*                                                                             *
* This program is free software; you can redistribute it and/or modify it     *
* under the terms of the GNU Lesser General Public License as published by    *
* the Free Software Foundation; either version 2.1 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 Lesser General Public License *
* for more details.                                                           *
*                                                                             *
* You should have received a copy of the GNU Lesser General Public License    *
* along with this program. If not, see <http://www.gnu.org/licenses/>.        *
*******************************************************************************
* Authors: The SOFA Team and external contributors (see Authors.txt)          *
*                                                                             *
* Contact information: contact@sofa-framework.org                             *
******************************************************************************/
//
// C++ Implementation: OrderIndependentTransparencyManager
//
// Description:
//
//
// Author: The SOFA team </www.sofa-framework.org>, (C) 2007
//
// Copyright: See COPYING file that comes with this distribution
//
//

#ifndef SOFA_NO_OPENGL
#include <sofa/helper/system/gl.h>
#endif


#include <SofaOpenglVisual/OglModel.h>
#include <SofaOpenglVisual/OrderIndependentTransparencyManager.h>
#include <sofa/core/visual/VisualParams.h>
#include <sofa/simulation/VisualVisitor.h>
#include <sofa/core/ObjectFactory.h>
#include <sofa/helper/system/FileRepository.h>
#include <sofa/core/visual/DrawToolGL.h>


namespace sofa
{

namespace component
{

namespace visualmodel
{

using namespace helper::gl;
using namespace simulation;
using namespace core::objectmodel;
using namespace core::visual;

SOFA_DECL_CLASS(OrderIndependentTransparencyManager)
//Register OrderIndependentTransparencyManager in the Object Factory
int OrderIndependentTransparencyManagerClass = core::RegisterObject("OrderIndependentTransparencyManager")
        .add< OrderIndependentTransparencyManager >()
        ;

class VisualOITDrawVisitor : public Visitor
{
public:
    VisualOITDrawVisitor(VisualParams* params, GLSLShader* oitShader)
        : Visitor(params)
        , visualParams(params)
        , defaultOITShader(oitShader)
        , nodeOITShaders()
    {

    }

    Result processNodeTopDown(Node* node);
    void processNodeBottomUp(Node* node);

    void fwdVisualModel(Node* node, VisualModel* vm);
    void bwdVisualModel(Node* node, VisualModel* vm);
    void processVisualModel(Node* node, VisualModel* vm);

    virtual const char* getCategoryName() const { return "visual"; }
    virtual const char* getClassName() const { return "VisualOITDrawVisitor"; }

protected:
    GLSLShader* getGLSLShader(const TagSet& tagSet) const;

private:
    typedef std::pair<OglOITShader*, TagSet> TaggedShader;
    std::list<VisualOITDrawVisitor::TaggedShader> findShaders(Node* node, const TagSet& tagSet) const;

private:
    VisualParams* visualParams;
    GLSLShader* defaultOITShader;

    std::stack<std::list<TaggedShader>> nodeOITShaders;

};

OrderIndependentTransparencyManager::OrderIndependentTransparencyManager()
    : depthScale(initData(&depthScale, 0.01f, "depthScale", "Depth scale"))
    , fbo()
    , accumulationShader()
    , compositionShader()
{

}

OrderIndependentTransparencyManager::~OrderIndependentTransparencyManager()
{

}

void OrderIndependentTransparencyManager::initVisual()
{
    GLSLShader::InitGLSL();

// accumulation shader

    std::string accumulationVertexShaderFilename = "shaders/orderIndependentTransparency/accumulation.vert";
    helper::system::DataRepository.findFile(accumulationVertexShaderFilename);

    std::string accumulationFragmentShaderFilename = "shaders/orderIndependentTransparency/accumulation.frag";
    helper::system::DataRepository.findFile(accumulationFragmentShaderFilename);

    accumulationShader.InitShaders(accumulationVertexShaderFilename, accumulationFragmentShaderFilename);

// composition shader

    std::string compositionVertexShaderFilename = "shaders/orderIndependentTransparency/composition.vert";
    helper::system::DataRepository.findFile(compositionVertexShaderFilename);

    std::string compositionFragmentShaderFilename = "shaders/orderIndependentTransparency/composition.frag";
    helper::system::DataRepository.findFile(compositionFragmentShaderFilename);

    compositionShader.InitShaders(compositionVertexShaderFilename, compositionFragmentShaderFilename);
    compositionShader.TurnOn();
    compositionShader.SetInt(compositionShader.GetVariable("AccumulationSampler"), 0);
    compositionShader.SetInt(compositionShader.GetVariable("RevealageSampler"), 1);
    compositionShader.TurnOff();
}

static void DrawFullScreenQuad()
{
    glDisable(GL_DEPTH_TEST);
    glDisable(GL_CULL_FACE);

    glMatrixMode(GL_PROJECTION);
    glPushMatrix();
    glLoadIdentity();
    glMatrixMode(GL_MODELVIEW);
    glPushMatrix();
    glLoadIdentity();

    glBegin(GL_QUADS);
    {
        glTexCoord2f(0.0f, 0.0f);
        glVertex2f(-1.0f, -1.0f);

        glTexCoord2f(1.0f, 0.0f);
        glVertex2f(1.0f, -1.0f);

        glTexCoord2f(1.0f, 1.0f);
        glVertex2f(1.0f, 1.0f);

        glTexCoord2f(0.0f, 1.0f);
        glVertex2f(-1.0f, 1.0f);
    }
    glEnd();

    glMatrixMode(GL_PROJECTION);
    glPopMatrix();
    glMatrixMode(GL_MODELVIEW);
    glPopMatrix();

    glEnable(GL_DEPTH_TEST);
    glEnable(GL_CULL_FACE);
}

bool OrderIndependentTransparencyManager::drawScene(VisualParams* visualParams)
{
    GLint viewport[4];
    glGetIntegerv(GL_VIEWPORT, viewport);
    if(0 == viewport[2] || 0 == viewport[3])
        return false;

// draw opaques normally

    glClear(GL_DEPTH_BUFFER_BIT);
    glDepthMask(GL_TRUE);

    drawOpaques(visualParams);

// draw transparents in a fbo

    GLint previousDrawFBO = 0;
    glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &previousDrawFBO);

    // init fbo

    fbo.init(viewport[2], viewport[3]);

    // copy current depth buffer to fbo render buffer

    fbo.copyDepth(previousDrawFBO);

    // bind fbo

    fbo.bind();

    // accumulation

    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);

    glEnable(GL_BLEND);
    glBlendEquation(GL_FUNC_ADD);
    glDepthMask(GL_FALSE);
    glBlendFuncSeparate(GL_ONE, GL_ONE, GL_ZERO, GL_ONE_MINUS_SRC_ALPHA);

    accumulationShader.TurnOn();
    accumulationShader.SetFloat(accumulationShader.GetVariable("DepthScale"), depthScale.getValue());
    drawTransparents(visualParams, &accumulationShader);
    accumulationShader.TurnOff();

    glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, previousDrawFBO);


// compose

    glEnable(GL_BLEND);
    glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);

    compositionShader.TurnOn();

    fbo.bindTextures();
    DrawFullScreenQuad();
    fbo.releaseTextures();

    compositionShader.TurnOff();

    glDisable(GL_BLEND);
    glDepthMask(GL_TRUE);

    return true;
}

void OrderIndependentTransparencyManager::drawOpaques(VisualParams* visualParams)
{
    Node* node = dynamic_cast<Node*>(getContext());
    if(!node)
        return;

    visualParams->pass() = VisualParams::Std;
    VisualDrawVisitor drawStandardVisitor(visualParams);
    drawStandardVisitor.setTags(getTags());
    node->execute(&drawStandardVisitor);
}

void OrderIndependentTransparencyManager::drawTransparents(VisualParams* visualParams, GLSLShader* oitShader)
{
    Node* node = dynamic_cast<Node*>(getContext());
    if(!node)
        return;

    visualParams->pass() = VisualParams::Transparent;
    VisualOITDrawVisitor drawTransparentVisitor(visualParams, oitShader);
    drawTransparentVisitor.setTags(getTags());
    node->execute(&drawTransparentVisitor);
}

OrderIndependentTransparencyManager::FrameBufferObject::FrameBufferObject()
    : id(0)
    , width(0)
    , height(0)
    , depthRenderbuffer(0)
    , accumulationTexture(0)
    , revealageTexture(0)
{

}

void OrderIndependentTransparencyManager::FrameBufferObject::init(int w, int h)
{
    if(0 == w || 0 == h)
        return;

    if(w == width && h == height)
        return;

    destroy();

    width = w;
    height = h;

// fbo

    glGenFramebuffersEXT(1, &id);
    glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, id);

// depth render buffer

    glGenRenderbuffersEXT(1, &depthRenderbuffer);
    glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, depthRenderbuffer);
    glRenderbufferStorageEXT(GL_RENDERBUFFER_EXT, GL_DEPTH24_STENCIL8_EXT, width, height);
    glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER_EXT, depthRenderbuffer);
    glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, 0);

// accumulation texture

    glGenTextures(1, &accumulationTexture);
    glBindTexture(GL_TEXTURE_RECTANGLE, accumulationTexture);
    glTexParameterf(GL_TEXTURE_RECTANGLE, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameterf(GL_TEXTURE_RECTANGLE, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
    glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
    glTexImage2D(GL_TEXTURE_RECTANGLE, 0, GL_RGBA16F, width, height, 0, GL_RGBA, GL_FLOAT, 0);
    glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_RECTANGLE, accumulationTexture, 0);
    glBindTexture(GL_TEXTURE_RECTANGLE, 0);

// revealage texture

    glGenTextures(1, &revealageTexture);
    glBindTexture(GL_TEXTURE_RECTANGLE, revealageTexture);
    glTexParameterf(GL_TEXTURE_RECTANGLE, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameterf(GL_TEXTURE_RECTANGLE, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
    glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
    glTexImage2D(GL_TEXTURE_RECTANGLE, 0, GL_R16F, width, height, 0, GL_RED, GL_FLOAT, 0);
    glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT1_EXT, GL_TEXTURE_RECTANGLE, revealageTexture, 0);
    glBindTexture(GL_TEXTURE_RECTANGLE, 0);

// bind draw buffers

    GLenum buffers[] = {GL_COLOR_ATTACHMENT0_EXT, GL_COLOR_ATTACHMENT1_EXT};
    glDrawBuffers(2, buffers);

    glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);
}

void OrderIndependentTransparencyManager::FrameBufferObject::destroy()
{
    glDeleteTextures(1, &revealageTexture);
    glDeleteTextures(1, &accumulationTexture);
    glDeleteRenderbuffers(1, &depthRenderbuffer);

    glDeleteFramebuffersEXT(1, &id);

    id = 0;
}

void OrderIndependentTransparencyManager::FrameBufferObject::copyDepth(GLuint fromFBO)
{
    glBindFramebuffer(GL_READ_FRAMEBUFFER, fromFBO);
    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, id);
    glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_DEPTH_BUFFER_BIT, GL_NEAREST);
}

void OrderIndependentTransparencyManager::FrameBufferObject::bind()
{
    glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, id);
}

void OrderIndependentTransparencyManager::FrameBufferObject::bindTextures()
{
    glActiveTexture(GL_TEXTURE1);
    glBindTexture(GL_TEXTURE_RECTANGLE, revealageTexture);
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_RECTANGLE, accumulationTexture);
}

void OrderIndependentTransparencyManager::FrameBufferObject::releaseTextures()
{
    glActiveTexture(GL_TEXTURE1);
    glBindTexture(GL_TEXTURE_RECTANGLE, 0);
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_RECTANGLE, 0);
}

GLSLShader* VisualOITDrawVisitor::getGLSLShader(const TagSet& tagSet) const
{
    if(!nodeOITShaders.empty())
    {
        const std::list<TaggedShader>& taggedShaders = nodeOITShaders.top();
        for(std::list<VisualOITDrawVisitor::TaggedShader>::const_iterator itTaggedShader = taggedShaders.begin(); itTaggedShader != taggedShaders.end(); ++itTaggedShader)
        {
            if(itTaggedShader->second.includes(tagSet))
                return itTaggedShader->first->accumulationShader();
        }
    }

    return defaultOITShader;
}

std::list<VisualOITDrawVisitor::TaggedShader> VisualOITDrawVisitor::findShaders(Node* node, const TagSet& tagSet) const
{
    std::list<TaggedShader> result;

    for(Node::Sequence<Shader>::iterator it = node->shaders.begin(); it != node->shaders.end(); ++it)
    {
        Shader* shader = *it;

        TagSet shaderTagSet = shader->getTags();
        if(shaderTagSet.includes(tagSet))
        {
            OglOITShader* oitShader = dynamic_cast<OglOITShader*>(shader);
            if(oitShader)
                result.push_back(TaggedShader({ oitShader, shaderTagSet }));
        }
    }

    if(!nodeOITShaders.empty())
        result.insert(result.end(), nodeOITShaders.top().begin(), nodeOITShaders.top().end());

    return result;
}

VisualOITDrawVisitor::Result VisualOITDrawVisitor::processNodeTopDown(Node* node)
{
#ifdef SOFA_SUPPORT_MOVING_FRAMES
    glPushMatrix();
    double glMatrix[16];
    node->getPositionInWorld().writeOpenGlMatrix(glMatrix);
    glMultMatrixd( glMatrix );
#endif

    nodeOITShaders.push(findShaders(node, subsetsToManage));

    for_each(this, node, node->visualModel, &VisualOITDrawVisitor::fwdVisualModel);
    for_each(this, node, node->visualModel, &VisualOITDrawVisitor::processVisualModel);

#ifdef SOFA_SUPPORT_MOVING_FRAMES
    glPopMatrix();
#endif

    return RESULT_CONTINUE;
}

void VisualOITDrawVisitor::processNodeBottomUp(Node* node)
{
    for_each(this, node, node->visualModel, &VisualOITDrawVisitor::bwdVisualModel);

    nodeOITShaders.pop();
}

void VisualOITDrawVisitor::fwdVisualModel(simulation::Node*, core::visual::VisualModel* vm)
{
#ifdef DEBUG_DRAW
    std::cerr << ">" << vm->getClassName() << "::fwdDraw() of " << vm->getName() << std::endl;
#endif
    vm->fwdDraw(visualParams);
#ifdef DEBUG_DRAW
    std::cerr << "<" << vm->getClassName() << "::fwdDraw() of " << vm->getName() << std::endl;
#endif
}

void VisualOITDrawVisitor::bwdVisualModel(simulation::Node*,core::visual::VisualModel* vm)
{
#ifdef DEBUG_DRAW
    std::cerr << ">" << vm->getClassName() << "::bwdDraw() of " << vm->getName() << std::endl;
#endif
    vm->bwdDraw(visualParams);
#ifdef DEBUG_DRAW
    std::cerr << "<" << vm->getClassName() << "::bwdDraw() of " << vm->getName() << std::endl;
#endif
}

void VisualOITDrawVisitor::processVisualModel(Node*, VisualModel* vm)
{
    if(!vm->getTags().includes(subsetsToManage))
        return;

    static_cast<core::visual::DrawToolGL*>(visualParams->drawTool())->setUpdateCapabilities(false);

    OglModel* oglModel = dynamic_cast<OglModel*>(vm);
    if(oglModel)
        oglModel->blendTransparency.setValue(false);

    GLSLShader* oitShader = getGLSLShader(vm->getTags());

    oitShader->TurnOn();
    oitShader->SetInt(oitShader->GetVariable("HasTexture"), vm->hasTexture() ? 1 : 0);

#ifdef DEBUG_DRAW
        std::cerr << ">" << vm->getClassName() << "::drawVisual() of " << vm->getName() << std::endl;
#endif
    vm->drawTransparent(visualParams);
#ifdef DEBUG_DRAW
        std::cerr << "<" << vm->getClassName() << "::drawVisual() of " << vm->getName() << std::endl;
#endif

    static_cast<core::visual::DrawToolGL*>(visualParams->drawTool())->setUpdateCapabilities(true);
}

} // namespace visualmodel

} // namespace component

} // namespace sofa
