/************************************************************************
 *
 * Copyright (C) 2014-2020 IRCAD France
 * Copyright (C) 2014-2020 IHU Strasbourg
 *
 * This file is part of Sight.
 *
 * Sight 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 3 of the License, or
 * (at your option) any later version.
 *
 * Sight 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 Sight. If not, see <https://www.gnu.org/licenses/>.
 *
 ***********************************************************************/

#include "fwRenderOgre/compositor/MaterialMgrListener.hpp"

#include "fwRenderOgre/helper/Shading.hpp"
#include "fwRenderOgre/helper/Technique.hpp"

#include <fwCore/spyLog.hpp>

#include <OGRE/OgreHighLevelGpuProgram.h>
#include <OGRE/OgreHighLevelGpuProgramManager.h>
#include <OGRE/OgreTechnique.h>

namespace fwRenderOgre
{

namespace compositor
{

// ----------------------------------------------------------------------------

MaterialMgrListener::~MaterialMgrListener()
{
}

// ----------------------------------------------------------------------------

::Ogre::Technique* fwRenderOgre::compositor::MaterialMgrListener::handleSchemeNotFound(unsigned short /*_schemeIndex*/,
                                                                                       const ::Ogre::String& _schemeName,
                                                                                       ::Ogre::Material* _originalMaterial,
                                                                                       unsigned short /*_lodIndex*/,
                                                                                       const ::Ogre::Renderable*
                                                                                       /*_renderable*/)
{
    const auto mtlName = _originalMaterial->getName();

    // Volume rendering techniques don't handle OIT. Ignore them.
    if(::Ogre::StringUtil::startsWith(mtlName, "RTV_Mat") ||::Ogre::StringUtil::endsWith(mtlName, "RayEntryPoints"))
    {
        return nullptr;
    }

    // Don't know what is this for exactly, but we don't care so we filter this out
    if(_schemeName == "ShaderGeneratorDefaultScheme")
    {
        return nullptr;
    }

    auto tokens = ::Ogre::StringUtil::split(_schemeName, "/");

    if(tokens.size() < 2)
    {
        return nullptr;
    }

    // DepthPeeling, DualDepthPeeling, WeightedBlended or HybridTransparency
    const auto algoName = tokens[0];
    // peel, depthMap, transmittanceBlend, etc...
    const auto algoPassName = tokens[1];

    ::Ogre::Technique* newTech     = nullptr;
    ::Ogre::Technique* defaultTech = _originalMaterial->getTechnique(0);
    ::Ogre::Technique* depthTech   = _originalMaterial->getTechnique("depth");
    if(depthTech == nullptr)
    {
        SLM_ERROR( "Missing 'depth' technique for material '" + _originalMaterial->getName() +
                   "' Order Independent Transparency will probably not be supported.");
        depthTech = defaultTech;
    }

    // The R2VB material does not need to fill the OIT schemes, though Ogre get us here to know what to do
    // We simply return the main technique in this case
    const ::Ogre::Technique::Passes& defaultTechPasses = defaultTech->getPasses();
    for(const auto pass : defaultTechPasses)
    {
        if(  ::Ogre::StringUtil::startsWith(pass->getGeometryProgramName(), "R2VB/" ) )
        {
            return defaultTech;
        }
    }

    if(_schemeName == "DepthPeeling/depthMap" ||
       _schemeName == "HybridTransparency/backDepth")
    {
        newTech = ::fwRenderOgre::helper::Technique::copyToMaterial(depthTech, _schemeName, _originalMaterial);

        const ::Ogre::Technique::Passes& passes = newTech->getPasses();
        for(const auto pass : passes)
        {
            pass->setCullingMode(::Ogre::CULL_NONE);
            pass->setManualCullingMode(::Ogre::MANUAL_CULL_NONE);
            pass->setSceneBlending(::Ogre::SBT_REPLACE);
            pass->setFragmentProgram("DepthPeeling/depthMap_FP");

            if(algoName == "HybridTransparency")
            {
                pass->setDepthFunction(::Ogre::CMPF_GREATER_EQUAL);
            }
        }
    }
    else if( ::Ogre::StringUtil::startsWith(_schemeName, "DepthPeeling/peel", false) ||
             ::Ogre::StringUtil::startsWith(_schemeName, "CelShadingDepthPeeling/peel", false) ||
             ::Ogre::StringUtil::startsWith(_schemeName, "HybridTransparency/peel", false) )
    {
        newTech = ::fwRenderOgre::helper::Technique::copyToMaterial(defaultTech, _schemeName, _originalMaterial);

        const ::Ogre::Technique::Passes& passes = newTech->getPasses();
        for(const auto pass : passes)
        {
            pass->setDepthCheckEnabled(true);
            pass->setCullingMode(::Ogre::CULL_NONE);
            pass->setManualCullingMode(::Ogre::MANUAL_CULL_NONE);
            pass->setSceneBlending(::Ogre::SBT_REPLACE);

            if(algoName == "CelShadingDepthPeeling" && pass->getName() != "NormalsPass" &&
               pass->getName() != "EdgePass")
            {
                auto vpName  = pass->getVertexProgramName();
                auto newName = ::fwRenderOgre::helper::Shading::setTechniqueInProgramName(vpName, algoName);
                pass->setVertexProgram(newName);
            }

            // replace fragment program and build it if needed
            auto fpName  = pass->getFragmentProgramName();
            auto newName = ::fwRenderOgre::helper::Shading::setTechniqueInProgramName(fpName, algoName + "/peel");
            this->ensureFPCreated(newName, algoName, algoPassName, fpName);
            pass->setFragmentProgram(newName);

            auto numTexUnit = pass->getNumTextureUnitStates();
            ::Ogre::TextureUnitState* texState = pass->createTextureUnitState();
            texState->setTextureAddressingMode(::Ogre::TextureUnitState::TAM_CLAMP);
            texState->setTextureFiltering(::Ogre::TFO_NONE);
            texState->setContentType(::Ogre::TextureUnitState::CONTENT_COMPOSITOR);

            // Modify texture input according to the requested pass
            if(algoPassName == "peelInit")
            {
                texState->setCompositorReference(algoName, "peelInit", 0);
            }
            else if(algoPassName == "peelPing")
            {
                texState->setCompositorReference(algoName, "pongBuffer", 1);
            }
            else if(algoPassName == "peelPong")
            {
                texState->setCompositorReference(algoName, "pingBuffer", 1);
            }
            else
            {
                SLM_FATAL("DepthPeeling logical error");
            }

            auto params = pass->getFragmentProgramParameters();
            params->setNamedConstant("u_nearestDepthBuffer", numTexUnit);
            params->setNamedAutoConstant("u_viewport", ::Ogre::GpuProgramParameters::ACT_VIEWPORT_SIZE);
            params->setNamedAutoConstant("u_diffuse", ::Ogre::GpuProgramParameters::ACT_SURFACE_DIFFUSE_COLOUR);
        }
    }
    else if(_schemeName == "WeightedBlended/occlusionMap" ||
            _schemeName == "HybridTransparency/occlusionMap")
    {
        newTech = ::fwRenderOgre::helper::Technique::copyToMaterial(defaultTech, _schemeName, _originalMaterial);

        const ::Ogre::Technique::Passes& passes = newTech->getPasses();
        for(const auto pass : passes)
        {
            // replace fragment program and build it if needed
            auto fpName  = pass->getFragmentProgramName();
            auto newName =
                ::fwRenderOgre::helper::Shading::setTechniqueInProgramName(fpName, algoName+ "/occlusionMap");
            this->ensureFPCreated(newName, algoName, algoPassName, fpName);
            pass->setFragmentProgram(newName);

            pass->setCullingMode(::Ogre::CULL_NONE);
            pass->setManualCullingMode(::Ogre::MANUAL_CULL_NONE);
            pass->setSceneBlending(::Ogre::SBT_REPLACE);
        }
    }
    else if(_schemeName == "WeightedBlended/weightBlend" ||
            _schemeName == "HybridTransparency/weightBlend")
    {
        newTech = ::fwRenderOgre::helper::Technique::copyToMaterial(defaultTech, _schemeName, _originalMaterial);

        const ::Ogre::Technique::Passes& passes = newTech->getPasses();
        for(const auto pass : passes)
        {
            // replace fragment program and build it if needed
            auto fpName  = pass->getFragmentProgramName();
            auto newName = ::fwRenderOgre::helper::Shading::setTechniqueInProgramName(fpName, algoName+ "/weightBlend");
            this->ensureFPCreated(newName, algoName, algoPassName, fpName);
            pass->setFragmentProgram(newName);

            pass->setDepthCheckEnabled(false);
            pass->setCullingMode(::Ogre::CULL_NONE);
            pass->setManualCullingMode(::Ogre::MANUAL_CULL_NONE);
            pass->setSceneBlending(::Ogre::SBT_ADD);

            auto numTexUnit = pass->getNumTextureUnitStates();
            auto params     = pass->getFragmentProgramParameters();

            if(algoName == "HybridTransparency")
            {
                ::Ogre::TextureUnitState* texState = pass->createTextureUnitState();
                texState->setTextureAddressingMode(::Ogre::TextureUnitState::TAM_CLAMP);
                texState->setTextureFiltering(::Ogre::TFO_NONE);
                texState->setContentType(::Ogre::TextureUnitState::CONTENT_COMPOSITOR);
                texState->setCompositorReference(algoName, "pingBuffer", 1);

                params->setNamedConstant("u_frontDepthBuffer", numTexUnit++);
            }
            ::Ogre::TextureUnitState* texState = pass->createTextureUnitState();
            texState->setTextureAddressingMode(::Ogre::TextureUnitState::TAM_CLAMP);
            texState->setTextureFiltering(::Ogre::TFO_NONE);
            texState->setContentType(::Ogre::TextureUnitState::CONTENT_COMPOSITOR);
            texState->setCompositorReference(algoName, "occlusion", 0);

            params->setNamedConstant("u_occlusionDepthBuffer", numTexUnit);
            params->setNamedAutoConstant("u_viewport", ::Ogre::GpuProgramParameters::ACT_VIEWPORT_SIZE);
            params->setNamedAutoConstant("u_near", ::Ogre::GpuProgramParameters::ACT_NEAR_CLIP_DISTANCE);
            params->setNamedAutoConstant("u_far", ::Ogre::GpuProgramParameters::ACT_FAR_CLIP_DISTANCE);
        }
    }
    else if(_schemeName == "WeightedBlended/transmittanceBlend"||
            _schemeName == "HybridTransparency/transmittanceBlend")
    {
        newTech = ::fwRenderOgre::helper::Technique::copyToMaterial(defaultTech, _schemeName, _originalMaterial);

        const ::Ogre::Technique::Passes& passes = newTech->getPasses();
        for(const auto pass : passes)
        {
            // replace fragment program and build it if needed
            auto fpName  = pass->getFragmentProgramName();
            auto newName = ::fwRenderOgre::helper::Shading::setTechniqueInProgramName(fpName,
                                                                                      algoName+ "/transmittanceBlend");
            this->ensureFPCreated(newName, algoName, algoPassName, fpName);
            pass->setFragmentProgram(newName);

            pass->setDepthCheckEnabled(false);
            pass->setCullingMode(::Ogre::CULL_NONE);
            pass->setManualCullingMode(::Ogre::MANUAL_CULL_NONE);
            pass->setSceneBlending(::Ogre::SBF_ZERO, ::Ogre::SBF_ONE_MINUS_SOURCE_COLOUR);

            auto numTexUnit = pass->getNumTextureUnitStates();
            auto params     = pass->getFragmentProgramParameters();

            if(algoName == "HybridTransparency")
            {
                ::Ogre::TextureUnitState* texState = pass->createTextureUnitState();
                texState->setTextureAddressingMode(::Ogre::TextureUnitState::TAM_CLAMP);
                texState->setTextureFiltering(::Ogre::TFO_NONE);
                texState->setContentType(::Ogre::TextureUnitState::CONTENT_COMPOSITOR);
                texState->setCompositorReference(algoName, "pingBuffer", 1);
                params->setNamedConstant("u_frontDepthBuffer", numTexUnit++);
            }
            ::Ogre::TextureUnitState* texState = pass->createTextureUnitState();
            texState->setTextureAddressingMode(::Ogre::TextureUnitState::TAM_CLAMP);
            texState->setTextureFiltering(::Ogre::TFO_NONE);
            texState->setContentType(::Ogre::TextureUnitState::CONTENT_COMPOSITOR);
            texState->setCompositorReference(algoName, "occlusion", 0);

            params->setNamedConstant("u_occlusionDepthBuffer", numTexUnit);
            params->setNamedAutoConstant("u_viewport", ::Ogre::GpuProgramParameters::ACT_VIEWPORT_SIZE);
            params->setNamedAutoConstant("u_diffuse", ::Ogre::GpuProgramParameters::ACT_SURFACE_DIFFUSE_COLOUR);
        }
    }
    else if( ::Ogre::StringUtil::startsWith(_schemeName, "DualDepthPeeling/peelInit", false) )
    {
        newTech = ::fwRenderOgre::helper::Technique::copyToMaterial(depthTech, _schemeName, _originalMaterial);

        const ::Ogre::Technique::Passes& passes = newTech->getPasses();
        for(const auto pass : passes)
        {
            pass->setDepthCheckEnabled(false);
            pass->setCullingMode(::Ogre::CULL_NONE);
            pass->setManualCullingMode(::Ogre::MANUAL_CULL_NONE);
            pass->setSceneBlending(::Ogre::SBT_ADD);
            pass->setSceneBlendingOperation(::Ogre::SBO_MAX);

            pass->setFragmentProgram("DualDepthPeeling/peelInit_FP");
        }

    }
    else if( ::Ogre::StringUtil::startsWith(_schemeName, "DualDepthPeeling/peel", false) )
    {
        newTech = ::fwRenderOgre::helper::Technique::copyToMaterial(defaultTech, _schemeName, _originalMaterial);

        const ::Ogre::Technique::Passes& passes = newTech->getPasses();
        for(const auto pass : passes)
        {
            pass->setDepthCheckEnabled(false);
            pass->setCullingMode(::Ogre::CULL_NONE);
            pass->setManualCullingMode(::Ogre::MANUAL_CULL_NONE);
            pass->setSceneBlending(::Ogre::SBT_ADD);
            pass->setSceneBlendingOperation(::Ogre::SBO_MAX);

            // replace fragment program and build it if needed
            auto fpName  = pass->getFragmentProgramName();
            auto newName = ::fwRenderOgre::helper::Shading::setTechniqueInProgramName(fpName, algoName + "/peel");
            this->ensureFPCreated(newName, algoName, algoPassName, fpName);
            pass->setFragmentProgram(newName);

            std::string inputBuffer;
            if(algoPassName == "peelPing")
            {
                inputBuffer = "pongBuffer";
            }
            else if(algoPassName == "peelPong")
            {
                inputBuffer = "pingBuffer";
            }
            else
            {
                SLM_FATAL("DualDepthPeeling logical error");
            }

            auto numTexUnit = pass->getNumTextureUnitStates();

            // Modify texture input according to the requested pass
            for(size_t i = 0; i < 4; ++i)
            {
                ::Ogre::TextureUnitState* texState = pass->createTextureUnitState();
                texState->setTextureAddressingMode(::Ogre::TextureUnitState::TAM_CLAMP);
                texState->setTextureFiltering(::Ogre::TFO_NONE);
                texState->setContentType(::Ogre::TextureUnitState::CONTENT_COMPOSITOR);
                texState->setCompositorReference(algoName, inputBuffer, i == 3 ? 4 : i);
            }

            auto params = pass->getFragmentProgramParameters();
            params->setNamedConstant("u_nearestDepthBuffer", numTexUnit);
            params->setNamedConstant("u_farthestDepthBuffer", numTexUnit + 1);
            params->setNamedConstant("u_forwardColorBuffer", numTexUnit + 2);
            params->setNamedConstant("u_forwardAlphasBuffer", numTexUnit + 3);
            params->setNamedAutoConstant("u_viewport", ::Ogre::GpuProgramParameters::ACT_VIEWPORT_SIZE);
            params->setNamedAutoConstant("u_diffuse", ::Ogre::GpuProgramParameters::ACT_SURFACE_DIFFUSE_COLOUR);
        }
    }
    else
    {
        SLM_INFO("not found : " << _schemeName );
    }

    return newTech;
}

// ----------------------------------------------------------------------------

::Ogre::GpuProgramPtr MaterialMgrListener::ensureFPCreated(const std::string& _name,
                                                           const std::string& _algoName,
                                                           const std::string& _algoPassName,
                                                           const std::string& _baseName)
{
    // Determine shader source file and parameters
    std::string sourceFileName;
    ::fwRenderOgre::helper::Shading::GpuProgramParametersType parameters;

    // Set specific shader according to the algo and the pass
    if(_algoName == "DepthPeeling")
    {
        sourceFileName = "DepthPeelingPeel_FP.glsl";
        parameters.push_back(std::make_pair<std::string, std::string>("attach", "DepthPeelingCommon_FP"));
    }
    else if(_algoName == "DualDepthPeeling")
    {
        sourceFileName = "DualDepthPeelingPeel_FP.glsl";
        parameters.push_back(std::make_pair<std::string, std::string>("attach", "DepthPeelingCommon_FP"));
    }
    else if(_algoName == "HybridTransparency")
    {
        if(_algoPassName == "peel" || _algoPassName == "peelInit")
        {
            sourceFileName = "DepthPeelingPeel_FP.glsl";
            parameters.push_back(std::make_pair<std::string, std::string>("attach", "DepthPeelingCommon_FP"));
        }
        else
        {
            if(_algoPassName == "transmittanceBlend" )
            {
                sourceFileName = "WeightedBlended_Transmittance_Blend_FP.glsl";
                parameters.push_back(std::make_pair<std::string, std::string>("attach", "DepthPeelingCommon_FP"));
                parameters.push_back(std::make_pair<std::string, std::string>("preprocessor_defines", "HYBRID=1"));
            }
            else if (_algoPassName == "occlusionMap")
            {
                sourceFileName = "WeightedBlended_Occlusion_Map_FP.glsl";
            }
            else
            {
                sourceFileName = "WeightedBlended_Weight_Blend_FP.glsl";
                parameters.push_back(std::make_pair<std::string, std::string>("attach", "DepthPeelingCommon_FP"));
                parameters.push_back(std::make_pair<std::string, std::string>("preprocessor_defines", "HYBRID=1"));
            }
        }
    }
    else if(_algoName == "WeightedBlended")
    {
        if(_algoPassName == "transmittanceBlend" )
        {
            sourceFileName = "WeightedBlended_Transmittance_Blend_FP.glsl";
        }
        else if (_algoPassName == "occlusionMap")
        {
            sourceFileName = "WeightedBlended_Occlusion_Map_FP.glsl";
        }
        else
        {
            sourceFileName = "WeightedBlended_Weight_Blend_FP.glsl";
        }
    }
    else if(_algoName == "CelShadingDepthPeeling")
    {
        sourceFileName = "CelShadingDepthPeelingPeel_FP.glsl";
        parameters.push_back(std::make_pair<std::string, std::string>("attach", "DepthPeelingCommon_FP"));
    }
    else
    {
        SLM_FATAL("Unreachable code");
    }

    return ::fwRenderOgre::helper::Shading::createProgramFrom(_name, sourceFileName, parameters,
                                                              ::Ogre::GPT_FRAGMENT_PROGRAM, _baseName);
}

// ----------------------------------------------------------------------------

} // namespace compositor

} // namespace fwRenderOgre
