/* osgEarth
* Copyright 2026 Pelican Mapping
* MIT License
*/
#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>

namespace osgEarth { namespace Util
{
    using namespace osgEarth;

    class PagingManager;

    /**
    * Interface used by anything that creates and controls PagedNodes.
    * Its use is optional.
    */
    class PagedNodeManager
    {
    public:
        osg::NodeCallback* visibilityCallback() {
            return _visibilityCallback.get();
        }
        void traverse(osg::NodeVisitor& nv) {
            _visible = true;
            nv.getUserValue<bool>("oe_visible", _visible);
        }
    protected:
        PagedNodeManager() {
            _visibilityCallback = new LambdaCullCallback([this](osg::Node* node, osg::NodeVisitor* nv) {
                if (this->_visible)
                    nv->traverse(*node);
                });
        }
        bool _visible = true;
        osg::ref_ptr<osg::NodeCallback> _visibilityCallback;
    };


    /**
     * 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();

        //! Sets an optional owner that the paged node can query for traversal information.
        //! The owner MUST be an ancestor node in the scene graph.
        void setOwner(PagedNodeManager* owner) {
            _owner = owner;
        }

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

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

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

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

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

        //! The minimum time after last access that this node should be
        //! protected from unloading.
        void setTimeoutSeconds(double value) {
            _timeoutSeconds = value;
        }
        double getTimeoutSeconds() const {
            return _timeoutSeconds;
        }

        //! 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) {
            _lodMethod = value;
        }

        LODMethod getLODMethod() const {
            return _lodMethod;
        }

        //! 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;
        osg::observer_ptr<PagingManager> _pagingManager_weak;
        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 = 256.0f;
        float _maxPixels = FLT_MAX;
        LODMethod _lodMethod = LODMethod::CAMERA_DISTANCE;
        float _priorityScale = 1.0f;
        jobs::context _job;
        bool _preCompile = true;
        std::atomic_int _revision = { 0 };
        bool _autoUnload = true;
        float _lastRange = FLT_MAX;
        float _lastPixelSize = 0.0f;
        double _lastTime = 0.0;
        mutable float _priority = 0.0f;
        RefinePolicy _refinePolicy = REFINE_REPLACE;
        std::string _jobpoolName;
        PagedNodeManager* _owner = nullptr;
        double _timeoutSeconds = 0.0;

        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:
        //! Construct
        PagingManager(const std::string& jobpoolname);

        //! Name of the job pool this pager will use
        const std::string& getJobPoolName() const {
            return _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(osg::NodeVisitor* nv = nullptr);

        float sse() const {
            return _sse;
        }

        struct Stats {
            std::string name;
            float maxRange = -1.0f;
            float lastRange = -1.0f;
        };

        std::vector<Stats> dumpStats();

    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;
        float _sse = 0.0f;

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

        inline void merge(PagedNode2* host);

        friend class PagedNode2;
    };

} }
