#ifndef OPENMW_MWRENDER_POSTPROCESSOR_H
#define OPENMW_MWRENDER_POSTPROCESSOR_H

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

#include <filesystem>

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

#include <osgViewer/Viewer>

#include <components/debug/debuglog.hpp>
#include <components/fx/stateupdater.hpp>
#include <components/fx/technique.hpp>
#include <components/misc/strings/algorithm.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 DistortionCallback;

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

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

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

        enum TextureUnits
        {
            Unit_LastShader = 0,
            Unit_LastPass,
            Unit_Depth,
            Unit_EyeAdaptation,
            Unit_Normals,
            Unit_Distortion,
            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<osg::Camera> getHUDCamera() { return mHUDCamera; }

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

        const TechniqueList& getTechniques() { return mTechniques; }

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

        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; }

        void disable();

        void enable();

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

        void disableDynamicShaders();

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

        void triggerShaderReload();

        bool mEnableLiveReload = false;

        void loadChain();
        void saveChain();

    private:
        void populateTechniqueFiles();

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

        void createObjectsForFrame(size_t frameId);

        void dirtyTechniques(bool dirtyAttachments = false);

        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;
        TechniqueList mInternalTechniques;

        std::unordered_map<std::string, std::filesystem::path, Misc::StringUtils::CiHash, Misc::StringUtils::CiEqual>
            mTechniqueFileMap;

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

        size_t mDirtyFrameId = 0;
        size_t mLastFrameNumber = 0;
        float mLastSimulationTime = 0.f;

        bool mDirty = false;
        bool mReload = true;
        bool mTriggerShaderReload = false;
        bool mUsePostProcessing = false;

        bool mUBO = false;
        bool mHDR = false;
        bool mNormals = false;
        bool mUnderwater = false;
        bool mPassLights = false;
        bool mPrevNormals = false;
        bool mExteriorFlag = false;
        bool mNormalsSupported = false;
        bool mPrevPassLights = false;

        int mGLSLVersion;
        int mWidth;
        int mHeight;
        int mSamples;

        osg::ref_ptr<fx::StateUpdater> mStateUpdater;
        osg::ref_ptr<PingPongCull> mPingPongCull;
        std::array<osg::ref_ptr<PingPongCanvas>, 2> mCanvases;
        osg::ref_ptr<TransparentDepthBinCallback> mTransparentDepthPostPass;
        osg::ref_ptr<DistortionCallback> mDistortionCallback;

        fx::DispatchArray mTemplateData;
    };
}

#endif
