/* --*-c++-*-- */
/* osgEarth
 * Copyright 2025 Pelican Mapping
 * MIT License
 */
#pragma once

#include <osgEarth/Common>
#include <osgEarth/FeatureModelSource>
#include <osgEarth/Style>
#include <osgEarth/NodeUtils>
#include <osgEarth/Threading>
#include <osgEarth/SceneGraphCallback>
#include <osgEarth/TextureArena>
#include <osgDB/Callbacks>
#include <osg/Node>
#include <set>

namespace osgEarth { namespace Util
{
    class Session;

    /**
     * A scene graph that renders feature data.
     * This class will handle all the internals of selecting features, gridding feature
     * data if required, and sorting features based on style. Then for each cell and each
     * style, it will invoke the FeatureNodeFactory to create the actual data for each set.
     */
    class OSGEARTH_EXPORT FeatureModelGraph : public osg::Group, public PagedNodeManager
    {
    public:
        /**
         * Constructs a new model graph.
         *
         * @param session
         *      Session under which to create elements in this graph
         * @param options
         *      Model source options
         * @param factory
         *      Node factory that will be invoked to compile feature data into nodes
         */
        FeatureModelGraph(const FeatureModelOptions& options);

        //! Set a scene graph callback host for this FMG
        void setSceneGraphCallbacks(SceneGraphCallbacks* callbacks);

        //! Sets the object that creates geometry nodes.
        void setNodeFactory(FeatureNodeFactory* factory);
        FeatureNodeFactory* getNodeFactory() const { return _factory.get(); }

        //! Sets the style sheet to use to render features 
        void setStyleSheet(StyleSheet* styles);

        //! Session associated with this feature graph.
        void setSession(Session* session);
        const Session* getSession() const;

        //! Min visible range
        void setMinRange(float value);

        //! Max visible range
        void setMaxRange(float value);

        //! Whether to attempt to use NVGL extensions
        void setUseNVGL(bool value);

        //! Sets a feature query buffer width as a percentage of the current tile.
        void setFeatureQueryBufferWidthAsPercentage(double value);

        //! Initialized the graph after setting all parameters.
        Status open();

        //! Shut down the service, notifying any active loading
        //! threads to stop working
        void shutdown();
        void setDone() { shutdown(); }

        //! Returns true is open() was called, false if shutdown was called
        bool isActive() const { return _isActive; }

        /**
         * Loads and returns a subnode. Used internally for paging.
         */
        osg::ref_ptr<osg::Group> load(
            unsigned lod, unsigned tileX, unsigned tileY,
            const std::string& uri,
            const osgDB::Options* readOptions);

        /**
         * Access to the features levels
         */
        const std::vector<const FeatureLevel*>& getLevels() const { return _lodmap; };

        //! Set the name of the object that owns this graph (for debugging)
        void setOwnerName(const std::string& name);
        const std::string& getOwnerName() const { return _ownerName; }

        std::shared_ptr<std::atomic_int> loadedTiles;

    public: // osg::Node

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

    protected:

        virtual ~FeatureModelGraph();

        osg::ref_ptr<osg::Node> setupPaging();

        osg::ref_ptr<osg::Group> buildTile( 
            const FeatureLevel&   level, 
            const GeoExtent&      extent, 
            const TileKey*        key,
            const osgDB::Options* readOptions);

        osg::Group* build( 
            const Style&          baseStyle, 
            const Query&          baseQuery, 
            const GeoExtent&      extent, 
            FeatureIndexBuilder*  index,
            const osgDB::Options* readOptions,
            ProgressCallback*     progress);


    private:

        //void ctor();
        
        osg::Group* createStyleGroup(
            const Style&          style, 
            const Query&          query, 
            FeatureIndexBuilder*  index,
            const osgDB::Options* readOptions,
            ProgressCallback*     progress);

        osg::Group* createStyleGroup(
            const Style&          style, 
            FeatureList&          workingSet, 
            const FilterContext&  contextPrototype,
            const osgDB::Options* readOptions,
            const Query&          query);

        void buildStyleGroups(
            const StyleSelector*  selector,
            const Query&          baseQuery,
            FeatureIndexBuilder*  index,
            osg::Group*           parent,
            const osgDB::Options* readOptions,
            ProgressCallback*     progress);

        void queryAndSortIntoStyleGroups(
            const Query&            query,
            const StringExpression& styleExpr,
            FeatureIndexBuilder*    index,
            osg::Group*             parent,
            const osgDB::Options*   readOptions,
            ProgressCallback*       progress);

        osg::Group* getOrCreateStyleGroupFromFactory(
            const Style& style);
       
        osg::BoundingSphered getBoundInWorldCoords( 
            const GeoExtent& extent,
            const Profile* tilingProfile =0L) const;

        void buildSubTilePagedLODs(
            unsigned lod, unsigned tileX, unsigned tileY,
            osg::Group* parent,
            const osgDB::Options* readOptions);
        
        osg::Group* readTileFromCache(
            const std::string&    cacheKey,
            const osgDB::Options* readOptions);

        bool writeTileToCache(
            const std::string&    cacheKey,
            osg::Group*           tile,
            const osgDB::Options* readOptions);

        void redraw();

    private:
        std::string _ownerName;
        bool _isActive;
        FeatureModelOptions              _options;
        osg::ref_ptr<FeatureNodeFactory> _factory;
        osg::ref_ptr<Session>            _session;
        osg::ref_ptr<StyleSheet>         _styleSheet;
        std::set<std::string>            _blacklist;
        Threading::ReadWriteMutex        _blacklistMutex;
        GeoExtent                        _usableFeatureExtent;
        bool                             _featureExtentClamped;
        GeoExtent                        _usableMapExtent;
        osg::BoundingSphered             _fullWorldBound;
        bool                             _useTiledSource;
        std::vector<const FeatureLevel*> _lodmap;
        RecursiveMutex                   _redrawMutex;
        optional<float> _minRange;
        optional<float> _maxRange;
        osg::ref_ptr<TextureArena> _textures;
        mutable std::vector<Texture::WeakPtr> _texturesCache;
        mutable std::mutex _texturesCacheMutex;

        std::atomic_int _cacheReads;
        std::atomic_int _cacheHits;

        osg::ref_ptr<osgDB::FileLocationCallback> _defaultFileLocationCallback;

        osg::observer_ptr<ModelSource> _modelSource;

        osg::ref_ptr<FeatureSourceIndex> _featureIndex;

        osg::ref_ptr<SceneGraphCallbacks> _sgCallbacks;

        osg::ref_ptr<osgDB::ObjectCache> _nodeCachingImageCache;

        optional<double> _featureQueryBufferWidthAsPercentage;

        void runPreMergeOperations(osg::Node* node);
        void runPostMergeOperations(osg::Node* node);
        void applyRenderSymbology(const Style& style, osg::Node* node);
        bool createOrUpdateNode(FeatureCursor*, const Style&, FilterContext&, const osgDB::Options*, osg::ref_ptr<osg::Node>& output, const Query& query);

        //osg::ref_ptr<FeatureCursor> createCursor(FeatureSource* fs, FilterContext& cx, const Query& query, ProgressCallback* progress) const;
        FeatureFilterChain _filterChain;
    };
} }
