/* -*-c++-*- */
/* osgEarth - Geospatial SDK for OpenSceneGraph
* Copyright 2020 Pelican Mapping
* http://osgearth.org
*
* osgEarth 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 2 of the License, or
* (at your option) any later version.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program.  If not, see <http://www.gnu.org/licenses/>
*/
#pragma once

#include <osgEarth/Common>
#include <osgEarth/optional>
#include <osgEarth/Threading>
#include <osgEarth/SceneGraphCallback>
#include <osgEarth/Utils>
#include <osgEarth/LoadableNode>
#include <osgEarth/CullingUtils>
#include <queue>
#include <list>
#include <memory>

namespace osgEarth { namespace Util
{
    using namespace osgEarth;

    /**
     * Internal node type to handle on-demand loading and unloading
     * of content.
     * A PagedNode2 must have a PagingManager as a scene graph ancestor.
     */
    class OSGEARTH_EXPORT PagedNode2 : 
        public osg::Group, 
        public osgEarth::LoadableNode
    {
    public:
        //! Type of function used to load content.
        using Loader = std::function<osg::ref_ptr<osg::Node>(Cancelable*)>;

    public:
        //! Construct an empty paged node
        PagedNode2();

        //! Function to run to load the asychronous child. When null, there is no child data.
        void setLoadFunction(const Loader& value);
        const Loader& getLoadFunction() const {
            return _load_function;
        }

        //! Set the center of this node, which is necessary since we
        //! cannot compute the bounding sphere before the asynchronous load
        void setCenter(const osg::Vec3& value) {
            _userBS.mutable_value().center() = value;
        }
        const osg::Vec3& getCenter() const {
            return _userBS->center();
        }

        //! Set the radius of this node's bounding sphere, which is necessary
        //! since we cannot compute the bounding sphere before the asynchronous load
        void setRadius(float value) {
            _userBS.mutable_value().radius() = value;
        }
        float getRadius() const {
            return _userBS->radius();
        }

        //! Sets the minimum distance from camera at which to load child data
        //! and activates range-based loading.
        void setMinRange(float value) {
            _minRange = value, _useRange = true;
        }

        //! Minimum distance from camera at which to load child data
        //! when using range-based loading.
        float getMinRange() const {
            return _minRange;
        }

        //! Sets the maximum distance from camera at which to load child data
        //! and activates range-based loading.
        void setMaxRange(float value) {
            _maxRange = value, _useRange = true;
        }

        //! Maximum distance from camera at which to load child data
        //! when using range-based loading.
        float getMaxRange() const {
            return _maxRange;
        }

        //! Sets the minimum pixel extent at which to load child data
        //! and activates screen-space-based loading.
        void setMinPixels(float value) {
            _minPixels = value, _useRange = false;
        }

        //! Minimum pixel extent at which to load child data
        //! when using screen-size-based loading.
        float getMinPixels() const {
            return _minPixels;
        }

        //! Sets the maximum pixel extent at which to load child data
        //! and activates screen-size-based loading.
        void setMaxPixels(float value) {
            _maxPixels = value, _useRange = false;
        }

        //! Maximum pixel extent at which to load child data
        //! when using screen-size-based loading.
        float getMaxPixels() const {
            return _maxPixels;
        }

        //! Multiply the load job's priority by this number
        void setPriorityScale(float value) {
            _priorityScale = value;
        }

        //! Multiply the load job's priority by this number
        float getPriorityScale() const {
            return _priorityScale;
        }

        //! Pre- and post-merge callbacks for the async data
        void setSceneGraphCallbacks(SceneGraphCallbacks* value) {
            _callbacks = value;
        }

        //! Pre- and post-merge callbacks for the async data
        SceneGraphCallbacks* getSceneGraphCallbacks() const {
            return _callbacks.get();
        }

        //! Whether to pre-compile GL objects before merging
        void setPreCompileGLObjects(bool value) {
            _preCompile = value;
        }

        //! Whether to pre-compile GL objects before merging
        bool getPreCompileGLObjects() const {
            return _preCompile;
        }

        //! Whether to continue rendering the normal children after
        //! the asynchronous node becomes visible
        //! Default value = REFINE_REPLACE
        void setRefinePolicy(RefinePolicy value) {
            _refinePolicy = value;
        }

        //! Priority to use if loading manually via the load() function.
        //! If this node is culled in the scene graph, this value will be
        //! overwritten.
        void setPriority(float value) {
            _priority = value;
        }

        float getPriority() const {
            return _priority;
        }

        //! The LOD refinement mode (range versus SSE). Make sure you
        //! have set the appropriate min/max range or min/max pixels.
        void setLODMethod(const LODMethod& value) {
            _useRange = (value == LODMethod::CAMERA_DISTANCE);
        }

        LODMethod getLODMethod() const {
            return _useRange ? LODMethod::CAMERA_DISTANCE : LODMethod::SCREEN_SPACE;
        }

        //! Mark the content as "in use" so that it will not
        //! be removed if setAutoUnload is true.
        void touch();

    public: // LoadableNode API

        void load() override;
        void unload() override;
        bool isLoadComplete() const override;
        bool isHighestResolution() const override;

        RefinePolicy getRefinePolicy() const override {
            return _refinePolicy;
        }
        bool getAutoUnload() const override {
            return _autoUnload;
        }
        void setAutoUnload(bool value) override {
            _autoUnload = value;
        }

    public: // osg::Node overrides

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

        osg::BoundingSphere computeBound() const override;

    protected:

        virtual ~PagedNode2();

        //! Starts the content loading for this node
        void startLoad(const osg::Object* host);

    private:
        friend class PagingManager;

        Loader _load_function;

        void* _token = nullptr;
        class PagingManager* _pagingManager = nullptr;
        osg::ref_ptr<SceneGraphCallbacks> _callbacks;
        
        std::atomic_bool _loadGate = { false };

        jobs::future<osg::ref_ptr<osg::Node>> _loaded;
        jobs::future<bool> _merged;

        Mutex _mutex;
        optional<osg::BoundingSphere> _userBS;
        float _minRange = 0.0f;
        float _maxRange = FLT_MAX;
        float _minPixels = 0.0f;
        float _maxPixels = FLT_MAX;
        bool _useRange = true;
        float _priorityScale = 1.0f;
        jobs::context _job;
        bool _preCompile = true;
        std::atomic_int _revision = { 0 };
        bool _autoUnload = true;
        float _lastRange = FLT_MAX;
        mutable float _priority = 0.0f;
        RefinePolicy _refinePolicy = REFINE_REPLACE;
        std::string _jobpoolName;

        bool merge(int revision);
        void traverseChildren(osg::NodeVisitor& nv);
    };

    /**
     * Group node class that performs memory management
     * functions for a graph of PagedNodes. This object
     * should be an ancestor of any PagedNode objects that
     * it is going to manage.
     */
    class OSGEARTH_EXPORT PagingManager : public osg::Group
    {
    public:
        PagingManager(const std::string& jobpoolname = {});

        //! Maximum number of nodes to merge into the scene graph per update pass
        void setMaxMergesPerFrame(unsigned value) {
            _mergesPerFrame = value;
        }
        unsigned getMaxMergesPerFrame() const {
            return _mergesPerFrame;
        }

        //! Number of nodes under management
        unsigned getNumTrackedNodes() const {
            return _tracker.size();
        }

        //! Subordinates call this to inform the paging manager they are still alive.
        void* use(PagedNode2* node, void* token)
        {
            scoped_lock_if lock(_trackerMutex, _threadsafe);
            return _tracker.use(node, token);
        }

        //! Manually call an update on the PagingManager.  This should only be used if you are loading data outside of a traditional frameloop and want to merge data.
        void update();

    protected:
        virtual ~PagingManager();

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

    private:
        bool _threadsafe = true;
        Mutex _trackerMutex;
        SentryTracker<osg::ref_ptr<PagedNode2>> _tracker;
        using UpdateFunc = std::function<void(Cancelable*)>;
        UpdateFunc _updateFunc;
        jobs::jobpool::metrics_t* _metrics = nullptr;
        std::string _jobpoolName;

        mutable Mutex _mergeMutex;
        struct ToMerge {
            osg::observer_ptr<PagedNode2> _node;
            int _revision;
        };
        std::queue<ToMerge> _mergeQueue;
        unsigned _mergesPerFrame = ~0u;
        std::atomic_bool _newFrame = { false };

        inline void merge(PagedNode2* host);

        friend class PagedNode2;
    };

} }
