#ifndef STEREO_MANAGER_H
#define STEREO_MANAGER_H

#include <osg/Matrix>
#include <osg/Vec3>
#include <osg/Camera>
#include <osg/StateSet>
#include <osgUtil/CullVisitor>

#include <memory>
#include <array>

#include <components/stereo/types.hpp>
#include <components/shader/shadermanager.hpp>

namespace osg
{
    class FrameBufferObject;
    class Texture2D;
    class Texture2DArray;
}

namespace osgViewer
{
    class Viewer;
}

namespace SceneUtil
{
    class MWShadowTechnique;
}

namespace Stereo
{
    class MultiviewFramebuffer;
    class StereoFrustumManager;
    class MultiviewStereoStatesetUpdateCallback;

    bool getStereo();

    //! Class that provides tools for managing stereo mode
    class Manager
    {
    public:
        struct UpdateViewCallback
        {
            virtual ~UpdateViewCallback() = default;

            //! Called during the update traversal of every frame to update stereo views.
            virtual void updateView(View& left, View& right) = 0;
        };

        //! Gets the singleton instance
        static Manager& instance();

        Manager(osgViewer::Viewer* viewer);
        ~Manager();

        //! Called during update traversal
        void update();

        void initializeStereo(osg::GraphicsContext* gc);

        //! Callback that updates stereo configuration during the update pass
        void setUpdateViewCallback(std::shared_ptr<UpdateViewCallback> cb);

        //! Set the cull callback on the appropriate camera object
        void setCullCallback(osg::ref_ptr<osg::NodeCallback> cb);

        osg::Matrixd computeEyeProjection(int view, bool reverseZ) const;
        osg::Matrixd computeEyeViewOffset(int view) const;

        //! Sets up any definitions necessary for stereo rendering
        void shaderStereoDefines(Shader::ShaderManager::DefineMap& defines) const;

        const std::shared_ptr<MultiviewFramebuffer>& multiviewFramebuffer() { return mMultiviewFramebuffer; };

        //! Sets rendering resolution of each eye to eyeResolution. 
        //! Once set, there will no longer be any connection between rendering resolution and screen/window resolution.
        void overrideEyeResolution(const osg::Vec2i& eyeResolution);

        //! Notify stereo manager that the screen/window resolution has changed.
        void screenResolutionChanged();

        //! Get current eye resolution
        osg::Vec2i eyeResolution();

        //! The projection intended for rendering. When reverse Z is enabled, this is not the same as the camera's projection matrix, 
        //! and therefore must be provided to the manager explicitly.
        void setMasterProjectionMatrix(const osg::Matrixd& projectionMatrix) { mMasterProjectionMatrix = projectionMatrix; }

        //! Causes the subgraph represented by the node to draw to the full viewport.
        //! This has no effect if stereo is not enabled
        void disableStereoForNode(osg::Node* node);

        void setShadowTechnique(SceneUtil::MWShadowTechnique* shadowTechnique);

        /// Determine which view the cull visitor belongs to
        Eye getEye(const osgUtil::CullVisitor* cv) const;

    private:
        friend class MultiviewStereoStatesetUpdateCallback;
        void updateMultiviewStateset(osg::StateSet* stateset);
        void updateStereoFramebuffer();
        void setupBruteForceTechnique();
        void setupOVRMultiView2Technique();

        osg::ref_ptr<osgViewer::Viewer> mViewer;
        osg::ref_ptr<osg::Camera>       mMainCamera;
        osg::ref_ptr<osg::Callback>     mUpdateCallback;
        std::string                     mError;
        osg::Matrixd                     mMasterProjectionMatrix;
        std::shared_ptr<MultiviewFramebuffer> mMultiviewFramebuffer;
        bool                            mEyeResolutionOverriden;
        osg::Vec2i                      mEyeResolutionOverride;

        std::array<View, 2>         mView;
        std::array<osg::Matrixd, 2>  mViewOffsetMatrix;
        std::array<osg::Matrixd, 2>  mProjectionMatrix;
        std::array<osg::Matrixd, 2>  mProjectionMatrixReverseZ;

        std::unique_ptr<StereoFrustumManager> mFrustumManager;
        std::shared_ptr<UpdateViewCallback> mUpdateViewCallback;

        using Identifier = osgUtil::CullVisitor::Identifier;
        osg::ref_ptr<Identifier> mIdentifierMain = new Identifier();
        osg::ref_ptr<Identifier> mIdentifierLeft = new Identifier();
        osg::ref_ptr<Identifier> mIdentifierRight = new Identifier();
    };
}

#endif
