#ifndef OPENMW_MWRENDER_POSTPROCESSOR_H
#define OPENMW_MWRENDER_POSTPROCESSOR_H

#include <array>
#include <vector>
#include <string>
#include <unordered_map>

#include <osg/Texture2D>
#include <osg/Group>
#include <osg/FrameBufferObject>
#include <osg/Camera>

#include <osgViewer/Viewer>

#include <components/fx/stateupdater.hpp>
#include <components/fx/technique.hpp>
#include <components/debug/debuglog.hpp>

#include "pingpongcanvas.hpp"
#include "transparentpass.hpp"

#include <memory>

namespace osgViewer
{
    class Viewer;
}

namespace Stereo
{
    class MultiviewFramebuffer;
}

namespace VFS
{
    class Manager;
}

namespace Shader
{
    class ShaderManager;
}

namespace MWRender
{
    class RenderingManager;
    class PingPongCull;
    class PingPongCanvas;
    class TransparentDepthBinCallback;

    class PostProcessor : public osg::Group
    {
    public:
        using FBOArray = std::array<osg::ref_ptr<osg::FrameBufferObject>, 5>;
        using TextureArray = std::array<osg::ref_ptr<osg::Texture>, 5>;
        using TechniqueList = std::vector<std::shared_ptr<fx::Technique>>;

        enum TextureIndex
        {
            Tex_Scene,
            Tex_Scene_LDR,
            Tex_Depth,
            Tex_OpaqueDepth,
            Tex_Normal
        };

        enum FBOIndex
        {
            FBO_Primary,
            FBO_Multisample,
            FBO_FirstPerson,
            FBO_OpaqueDepth,
            FBO_Intercept
        };

        enum TextureUnits
        {
            Unit_LastShader = 0,
            Unit_LastPass,
            Unit_Depth,
            Unit_EyeAdaptation,
            Unit_Normals,
            Unit_NextFree
        };

        enum Status
        {
            Status_Error,
            Status_Toggled,
            Status_Unchanged
        };

        PostProcessor(RenderingManager& rendering, osgViewer::Viewer* viewer, osg::Group* rootNode, const VFS::Manager* vfs);

        ~PostProcessor();

        void traverse(osg::NodeVisitor& nv) override;

        osg::ref_ptr<osg::FrameBufferObject> getFbo(FBOIndex index, unsigned int frameId) { return mFbos[frameId][index]; }

        osg::ref_ptr<osg::Texture> getTexture(TextureIndex index, unsigned int frameId) { return mTextures[frameId][index]; }

        osg::ref_ptr<osg::FrameBufferObject> getPrimaryFbo(unsigned int frameId) { return mFbos[frameId][FBO_Multisample] ? mFbos[frameId][FBO_Multisample] : mFbos[frameId][FBO_Primary]; }

        osg::ref_ptr<fx::StateUpdater> getStateUpdater() { return mStateUpdater; }

        const TechniqueList& getTechniques() { return mTechniques; }

        const TechniqueList& getTemplates() const { return mTemplates; }

        osg::ref_ptr<PingPongCanvas> getCanvas() { return mPingPongCanvas; }

        const auto& getTechniqueMap() const { return mTechniqueFileMap; }

        void resize();

        Status enableTechnique(std::shared_ptr<fx::Technique> technique, std::optional<int> location = std::nullopt);

        Status disableTechnique(std::shared_ptr<fx::Technique> technique, bool dirty = true);

        bool getSupportsNormalsRT() const { return mNormalsSupported; }

        template <class T>
        void setUniform(std::shared_ptr<fx::Technique> technique, const std::string& name, const T& value)
        {
            if (!isEnabled())
                return;

            auto it = technique->findUniform(name);

            if (it == technique->getUniformMap().end())
                return;

            if ((*it)->mStatic)
            {
                Log(Debug::Warning) << "Attempting to set a configration variable [" << name << "] as a uniform";
                return;
            }

            (*it)->setValue(value);
        }

        std::optional<size_t> getUniformSize(std::shared_ptr<fx::Technique> technique, const std::string& name)
        {
            auto it = technique->findUniform(name);

            if (it == technique->getUniformMap().end())
                return std::nullopt;

            return (*it)->getNumElements();
        }

        bool isTechniqueEnabled(const std::shared_ptr<fx::Technique>& technique) const;

        void setExteriorFlag(bool exterior) { mExteriorFlag = exterior; }

        void setUnderwaterFlag(bool underwater) { mUnderwater = underwater; }

        void toggleMode();

        std::shared_ptr<fx::Technique> loadTechnique(const std::string& name, bool loadNextFrame=false);

        bool isEnabled() const { return mUsePostProcessing && mEnabled; }

        bool softParticlesEnabled() const {return mSoftParticles; }

        bool getHDR() const { return mHDR; }

        void disable();

        void enable(bool usePostProcessing = true);

        void setRenderTargetSize(int width, int height) { mWidth = width; mHeight = height; }

        void disableDynamicShaders();

        int renderWidth() const;
        int renderHeight() const;

        void triggerShaderReload();

        bool mEnableLiveReload;

        void loadChain();
        void saveChain();


    private:

        void populateTechniqueFiles();

        size_t frame() const { return mViewer->getFrameStamp()->getFrameNumber(); }

        void createObjectsForFrame(size_t frameId);

        void createTexturesAndCamera(size_t frameId);

        void reloadMainPass(fx::Technique& technique);

        void dirtyTechniques();

        void update(size_t frameId);

        void reloadIfRequired();

        void updateLiveReload();

        void cull(size_t frameId, osgUtil::CullVisitor* cv);

        osg::ref_ptr<osg::Group> mRootNode;
        osg::ref_ptr<osg::Camera> mHUDCamera;

        std::array<TextureArray, 2> mTextures;
        std::array<FBOArray, 2> mFbos;

        TechniqueList mTechniques;
        TechniqueList mTemplates;
        TechniqueList mQueuedTemplates;

        std::unordered_map<std::string, boost::filesystem::path> mTechniqueFileMap;

        int mSamples;

        bool mDirty;
        size_t mDirtyFrameId;

        RenderingManager& mRendering;
        osgViewer::Viewer* mViewer;
        const VFS::Manager* mVFS;

        bool mTriggerShaderReload;
        bool mReload;
        bool mEnabled;
        bool mUsePostProcessing;
        bool mSoftParticles;
        bool mDisableDepthPasses;

        size_t mLastFrameNumber;
        float mLastSimulationTime;

        bool mExteriorFlag;
        bool mUnderwater;
        bool mHDR;
        bool mNormals;
        bool mPrevNormals;
        bool mNormalsSupported;
        bool mPassLights;
        bool mPrevPassLights;
        bool mUBO;
        int mGLSLVersion;

        osg::ref_ptr<fx::StateUpdater> mStateUpdater;
        osg::ref_ptr<PingPongCull> mPingPongCull;
        osg::ref_ptr<PingPongCanvas> mPingPongCanvas;
        osg::ref_ptr<TransparentDepthBinCallback> mTransparentDepthPostPass;
    
        int mWidth;
        int mHeight;

        fx::DispatchArray mTemplateData;
    };
}

#endif
