// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2021 James Hogan <james@albanarts.com>

#ifndef OSGXR_OPENXR_SESSION
#define OSGXR_OPENXR_SESSION 1

#include "ManagedSpace.h"
#include "Path.h"
#include "System.h"

#include <osg/Geometry>
#include <osg/Referenced>
#include <osg/ref_ptr>
#include <osgViewer/GraphicsWindow>
#include <OpenThreads/Mutex>

#include <memory>
#include <set>

namespace osgXR {

namespace OpenXR {

class Action;
class ActionSet;
class CompositionLayer;

class Session : public osg::Referenced
{
    public:

        // GL context must not be bound in another thread
        Session(System *system, osgViewer::GraphicsWindow *window);
        // GL context must not be bound in another thread
        virtual ~Session();

        // GL context must not be bound in another thread
        void releaseGLObjects(osg::State *state = nullptr);

        // Error checking

        bool valid() const
        {
            return _session != XR_NULL_HANDLE;
        }

        bool check(XrResult result, const char *actionMsg) const;

        // Action set attachment

        /// Add an action set to the list.
        void addActionSet(ActionSet *actionSet);
        /**
         * Attach the added action sets to the OpenXR session.
         * @return true on success, false on failure.
         */
        bool attachActionSets();

        /// Get the current interaction profile for the given subaction path.
        Path getCurrentInteractionProfile(const Path &subactionPath) const;

        /// Get a list of bound source paths for an action.
        bool getActionBoundSources(Action *action,
                                   std::vector<XrPath> &sourcePaths) const;

        /**
         * Get a localized name for an input source.
         * @param sourcePath      Input source path.
         * @param whichComponents Which components to include.
         * @return Localized name string
         */
        std::string getInputSourceLocalizedName(XrPath sourcePath,
                                                XrInputSourceLocalizedNameFlags whichComponents) const;

        // Action syncing

        /// Activate a certain action set.
        void activateActionSet(ActionSet *actionSet,
                               Path subactionPath = Path());
        /// Deactivate a certain action set.
        void deactivateActionSet(ActionSet *actionSet,
                                 Path subactionPath = Path());
        /// Sync active action sets.
        bool syncActions();

        /// Get the number of action sync counts that have taken place.
        unsigned int getActionSyncCount() const
        {
            return _actionSyncCount;
        }

        // Accessors

        // Find whether the session is ready to begin
        bool isReady() const
        {
            return _state == XR_SESSION_STATE_READY;
        }

        // Find whether the session is running
        bool isRunning() const
        {
            return _running;
        }

        // Find whether the session is already in the process of exiting
        bool isExiting() const
        {
            return _exiting;
        }

        // Find whether the session has been lost
        bool isLost() const
        {
            return _lost || _instance->lost();
        }

        osgViewer::GraphicsWindow *getWindow() const
        {
            return _window.get();
        }

        // State management

        XrSessionState getState() const
        {
            return _state;
        }

        void setState(XrSessionState state)
        {
            _state = state;
        }


        // Conversions

        const osg::ref_ptr<Instance> getInstance() const
        {
            return _instance;
        }

        const System *getSystem() const
        {
            return _system;
        }

        XrInstance getXrInstance() const
        {
            return _system->getXrInstance();
        }

        XrSystemId getXrSystemId() const
        {
            return _system->getXrSystemId();
        }

        XrSession getXrSession()
        {
            return _session;
        }

        // Queries

        typedef std::vector<int64_t> SwapchainFormats;
        const SwapchainFormats &getSwapchainFormats() const;

        Space *getViewSpace();
        ManagedSpace *getLocalSpace();
        Space *getLocalSpace(XrTime time);
        XrTime getLastDisplayTime() const
        {
            return _lastDisplayTime;
        }

        bool recenterLocalSpace();

        void updateVisibilityMasks(XrViewConfigurationType viewConfigurationType,
                                   uint32_t viewIndex);
        osg::ref_ptr<osg::Geometry> getVisibilityMask(uint32_t viewIndex,
                                                      XrVisibilityMaskTypeKHR visibilityMaskType,
                                                      bool force = false);

        // Operations

        /**
         * Check whether the session's GLX context is current.
         * This is intended for internal use only by the functions below.
         * @return whether the session's GLX context is current.
         */
        bool checkCurrent() const;

        /**
         * Make the GLX context current.
         * This makes the GLX context current to workaround broken XR runtimes.
         * It should be used to ensure the context is current before XR calls
         * affected by QUIRK_GL_CONTEXT_IGNORED if shouldSwitchContext()
         * (because the XR runtime will fail to do so), and to restore the
         * context after XR calls affected by QUIRK_GL_CONTEXT_CLEARED if
         * shouldRestoreContext() or getRestoreAction() == CONTEXT_RESTORE
         * (because the XR runtime will have explicitly released the context).
         */
        void makeCurrent() const;
        /**
         * Release the GLX context so that none is current.
         * This makes no GLX context current to workaround broken XR runtimes.
         * It should be used release the switched context after XR calls
         * affected by QUIRK_GL_CONTEXT_IGNORED if shouldSwitchContext()
         * (because we had to call makeCurrent() before the call), and after XR
         * calls affected by QUIRK_GL_CONTEXT_CHANGED if getRestoreAction() ==
         * CONTEXT_RELEASE (because the XR runtime failed to do so).
         */
        void releaseContext() const;

        /// Action to perform before or after XR call due to quirks.
        typedef enum ContextAction {
            /// No action is necessary.
            CONTEXT_IGNORE = 0,
            /// The GLX context should be restored.
            CONTEXT_RESTORE,
            /**
             * The GLX context should be released leaving no context current.
             * It is assumed that no other context will need restoring in its
             * place.
             */
            CONTEXT_RELEASE
        } ContextAction;

        /**
         * Find whether for an XR call affected by QUIRK_GL_CONTEXT_IGNORED it
         * is necessary to switch the context prior to the XR call with
         * makeCurrent(), and also release it afterwards with releaseContext.
         * @return true if the context should be switched and released,
         *         false otherwise.
         */
        bool shouldSwitchContext() const
        {
            return _instance->getQuirk(QUIRK_GL_CONTEXT_IGNORED) &&
                   !checkCurrent();
        }
        /**
         * Find whether for an XR call affected by QUIRK_GL_CONTEXT_CLEARED it
         * is necessary to restore the context after the XR call.
         * @return true if the context should be restored after,
         *         false otherwise.
         */
        bool shouldRestoreContext() const
        {
            return _instance->getQuirk(QUIRK_GL_CONTEXT_CLEARED) &&
                   checkCurrent();
        }
        /**
         * Find what GL context action to perform after an XR call affected by
         * QUIRK_GL_CONTEXT_CLEARED or QUIRK_GL_CONTEXT_CHANGED.
         * @return CONTEXT_RESTORE if the runtime may clear the context and it
         *                         will need restoring.
         *         CONTEXT_RELEASE if the runtime may change the context and
         *                         fail to release it.
         *         CONTEXT_IGNORE  otherwise.
         */
        ContextAction getRestoreAction() const
        {
            bool cleared = _instance->getQuirk(QUIRK_GL_CONTEXT_CLEARED);
            bool changed = _instance->getQuirk(QUIRK_GL_CONTEXT_CHANGED);
            if (cleared || changed)
            {
                bool current = checkCurrent();
                if (cleared && current)
                    return CONTEXT_RESTORE;
                if (changed && !current)
                    return CONTEXT_RELEASE;
            }
            return CONTEXT_IGNORE;
        }


        bool begin(const System::ViewConfiguration &viewConfiguration);
        void end();
        void requestExit();

        const System::ViewConfiguration *getViewConfiguration() const
        {
            return _viewConfiguration;
        }

        class Frame : public osg::Referenced
        {
            public:

                Frame(osg::ref_ptr<Session> session, XrFrameState *frameState);

                virtual ~Frame();

                // Error checking

                bool check(XrResult result, const char *actionMsg) const
                {
                    return _session->check(result, actionMsg);
                }

                // Session related accessors

                Space *getLocalSpace()
                {
                    return _session->getLocalSpace(_time);
                }

                // Accessors

                Session *getSession()
                {
                    return _session;
                }

                bool shouldRender() const
                {
                    return _shouldRender;
                }

                bool hasBegun() const
                {
                    return _begun;
                }

                XrTime getTime() const
                {
                    return _time;
                }

                void locateViews();

                void checkLocateViews()
                {
                    OpenThreads::ScopedLock<OpenThreads::Mutex> lock(_locateViewsMutex);
                    if (!_locatedViews)
                        locateViews();
                }

                bool isOrientationValid()
                {
                    checkLocateViews();
                    return _viewState.viewStateFlags & XR_VIEW_STATE_ORIENTATION_VALID_BIT;
                }
                bool isPositionValid()
                {
                    checkLocateViews();
                    return _viewState.viewStateFlags & XR_VIEW_STATE_POSITION_VALID_BIT;
                }
                bool isOrientationTracked()
                {
                    checkLocateViews();
                    return _viewState.viewStateFlags & XR_VIEW_STATE_ORIENTATION_TRACKED_BIT;
                }
                bool isPositionTracked()
                {
                    checkLocateViews();
                    return _viewState.viewStateFlags & XR_VIEW_STATE_POSITION_TRACKED_BIT;
                }
                uint32_t getNumViews()
                {
                    checkLocateViews();
                    return _views.size();
                }
                const XrFovf &getViewFov(uint32_t index)
                {
                    checkLocateViews();
                    return _views[index].fov;
                }
                const XrPosef &getViewPose(uint32_t index)
                {
                    checkLocateViews();
                    return _views[index].pose;
                }

                // Modifiers

                void setEnvBlendMode(XrEnvironmentBlendMode envBlendMode)
                {
                    _envBlendMode = envBlendMode;
                }
                XrEnvironmentBlendMode getEnvBlendMode() const
                {
                    return _envBlendMode;
                }

                void setOsgFrameNumber(unsigned int osgFrameNumber)
                {
                    _osgFrameNumber = osgFrameNumber;
                }
                unsigned int getOsgFrameNumber() const
                {
                    return _osgFrameNumber;
                }

                void addLayer(osg::ref_ptr<CompositionLayer> layer);

                // Operations

                bool begin();
                bool end();

            protected:

                // Frame info
                osg::ref_ptr<Session> _session;
                XrTime _time;
                XrDuration _period;
                bool _shouldRender;

                // OpenSceneGraph frame
                unsigned int _osgFrameNumber;

                // For access to _locatedViews etc
                OpenThreads::Mutex _locateViewsMutex;

                // View locations (protected by _locateViewsMutex)
                bool _locatedViews;
                XrViewState _viewState;
                std::vector<XrView> _views;

                // Frame end info
                bool _begun;
                XrEnvironmentBlendMode _envBlendMode;
                std::vector<osg::ref_ptr<CompositionLayer> > _layers;
        };

        osg::ref_ptr<Frame> waitFrame();
        // Notify of end of frame
        void onEndFrame(Frame *frame);
        // Notify of reference space change
        void onReferenceSpaceChangePending(const XrEventDataReferenceSpaceChangePending *event);

        // OpenXR extension wrappers
        XrResult xrGetVisibilityMask(const System::ViewConfiguration &viewConfiguration,
                                     uint32_t viewIndex,
                                     XrVisibilityMaskTypeKHR visibilityMaskType,
                                     XrVisibilityMaskKHR *visibilityMask) const
        {
            return _instance->xrGetVisibilityMask(_session,
                                                  viewConfiguration.getType(),
                                                  viewIndex, visibilityMaskType,
                                                  visibilityMask);
        }

    protected:

        // Init data
        osg::observer_ptr<osgViewer::GraphicsWindow> _window;

        // Session data
        osg::ref_ptr<Instance> _instance;
        const System *_system = nullptr;
        XrSession _session = XR_NULL_HANDLE;
        const System::ViewConfiguration *_viewConfiguration = nullptr;

        // Action sets
        std::set<osg::ref_ptr<ActionSet>> _actionSets;
        typedef std::pair<ActionSet *, XrPath> ActionSetSubactionPair;
        std::set<ActionSetSubactionPair> _activeActionSets;
        unsigned int _actionSyncCount = 0;

        // Session state
        XrSessionState _state = XR_SESSION_STATE_UNKNOWN;
        bool _running = false;
        bool _exiting = false;
        mutable bool _lost = false;

        // Swapchain formats
        mutable bool _readSwapchainFormats = false;
        mutable SwapchainFormats _swapchainFormats;

        // Reference spaces
        osg::ref_ptr<Space> _viewSpace;
        std::unique_ptr<ManagedSpace> _localSpace;
        XrTime _lastDisplayTime = 0;

        /*
         * Visibility mask geometry cache.
         * We keep visibility mask geometries cached to avoid duplication and so
         * we can update them after a VisibilityMaskChangedKHR event.
         */
        typedef osg::ref_ptr<osg::Geometry> VisMaskGeometry;
        typedef std::vector<VisMaskGeometry> VisMaskGeometryView;
        typedef std::vector<VisMaskGeometryView> VisMaskGeometries;
        VisMaskGeometries _visMaskCache;
};

} // osgXR::OpenXR

} // osgXR

#endif
