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

#include <osgEarth/Common>
#include <osgEarth/Config>
#include <osgEarth/SceneGraphCallback>
#include <osgEarth/URI>
#include <osgEarth/JsonUtils>
#include <osgEarth/GeoData>
#include <osgEarth/VirtualProgram>
#include <osg/Group>
#include <osg/MatrixTransform>
#include <osgDB/Options>
#include <osgUtil/CullVisitor>
#include <osgEarth/LoadableNode>

namespace osgUtil {
    class IncrementalCompileOperation;
}

/**
 * 3D Tiles
 * https://github.com/AnalyticalGraphicsInc/3d-tiles
 * EXPERIMENTAL
 */
namespace osgEarth { namespace Contrib { namespace ThreeDTiles
{
    using namespace osgEarth;

    struct LoadContext
    {
        URIContext _uc;
    };

    class OSGEARTH_EXPORT Asset
    {
        OE_OPTION(std::string, version);
        OE_OPTION(std::string, tilesetVersion);
        OE_OPTION(std::string, gltfUpAxis);

        Asset() { }
        Asset(const Json::Value& value) { fromJSON(value); }
        void fromJSON(const Json::Value&);
        Json::Value getJSON() const;
    };

    class OSGEARTH_EXPORT BoundingVolume
    {
        OE_OPTION(osg::BoundingBoxd, box);
        OE_OPTION(osg::BoundingBoxd, region);
        OE_OPTION(osg::BoundingSphere, sphere);

        BoundingVolume() { }
        BoundingVolume(const Json::Value& value) { fromJSON(value); }
        void fromJSON(const Json::Value&);
        Json::Value getJSON() const;

        osg::BoundingSphere asBoundingSphere() const;
    };

    class OSGEARTH_EXPORT TileContent
    {
        OE_OPTION(BoundingVolume, boundingVolume);
        OE_OPTION(URI, uri);

        TileContent() { }
        TileContent(const Json::Value& value, LoadContext& uc) { fromJSON(value, uc); }
        void fromJSON(const Json::Value&, LoadContext&);
        Json::Value getJSON() const;
    };

    class OSGEARTH_EXPORT Tile : public osg::Referenced
    {
    public:
        OE_OPTION(BoundingVolume, boundingVolume);
        OE_OPTION(BoundingVolume, viewerRequestVolume);
        OE_OPTION(double, geometricError);
        OE_OPTION(RefinePolicy, refine);
        OE_OPTION(osg::Matrix, transform);
        OE_OPTION(TileContent, content);
        OE_OPTION_VECTOR(osg::ref_ptr<Tile>, children);

        Tile() : _refine(REFINE_ADD) { }
        Tile(const Json::Value& value, LoadContext& uc) { fromJSON(value, uc); }
        void fromJSON(const Json::Value&, LoadContext& uc);
        Json::Value getJSON() const;

        osg::BoundingSphere getBoundingSphere();
    };

    class OSGEARTH_EXPORT Tileset : public osg::Referenced
    {
    public:
        OE_OPTION(Asset, asset);
        OE_OPTION(BoundingVolume, boundingVolume);
        OE_OPTION(double, geometricError);
        OE_OPTION_REFPTR(Tile, root);

        Tileset() { }
        Tileset(const Json::Value& value, LoadContext& uc) { fromJSON(value, uc); }
        void fromJSON(const Json::Value&, LoadContext& uc);
        Json::Value getJSON() const;

        static Tileset* create(const std::string& tilesetJSON, const URIContext& uc);
    };

    class ThreeDTilesetNode;
    class ThreeDTileNode;

    /**
     * Node that renders a 3D-Tiles content record
     */
    class OSGEARTH_EXPORT ThreeDTilesetContentNode : public osg::Group
    {
    public:
        ThreeDTilesetContentNode(ThreeDTilesetNode* tilesetNode, Tileset* tileset, osgDB::Options* options);

        ThreeDTileNode* getTileNode();

    private:
        ThreeDTileNode* _tileNode;
        ThreeDTilesetNode* _tilesetNode;
        osg::ref_ptr< Tileset > _tileset;
        osg::ref_ptr< osgDB::Options > _options;
    };

    /**
     * Node that renders a 3D-Tiles Tile
     */
    class OSGEARTH_EXPORT ThreeDTileNode : public osg::MatrixTransform, public LoadableNode
    {
    public:
        ThreeDTileNode(ThreeDTilesetNode* tileset, Tile* tile, bool immediateLoad, osgDB::Options* options);
        osg::BoundingSphere computeBound() const;

        bool hasContent();

        osg::Node* getContent();

        bool isContentReady();

        void resolveContent();

        void updateTracking(osgUtil::CullVisitor* cv);

        void requestContent(osgUtil::IncrementalCompileOperation* ico);

        double getDistanceToTile(osgUtil::CullVisitor* cv);

        double computeScreenSpaceError(osgUtil::CullVisitor* cv);

        void traverse(osg::NodeVisitor& nv);

        bool unloadContent();

        const Tile* getTile() const { return _tile.get(); }

        RefinePolicy getRefinePolicy() const { return _refine; }

        unsigned int getLastCulledFrameNumber() const;
        float getLastCulledFrameTime() const;

        virtual void resizeGLObjectBuffers(unsigned int maxSize);

        virtual void releaseGLObjects(osg::State* state) const;

        typedef std::list< osg::ref_ptr < ThreeDTileNode > > TileTracker;

        TileTracker::iterator _trackerItr;
        bool _trackerItrValid;

        void setParentTile(ThreeDTileNode* parentTile);

    public: // LoadableNode

        void load() override
        {
            // Load the content for this tile and attempt to resolve it.
            requestContent(nullptr);
            resolveContent();

            // If this tile has children we also need to load their content so this node is ready to subdivide
            if (_children.valid())
            {
                for (unsigned int i = 0; i < _children->getNumChildren(); i++)
                {
                    osg::ref_ptr< ThreeDTileNode > childTile = dynamic_cast<ThreeDTileNode*>(_children->getChild(i));
                    if (childTile.valid())
                    {
                        if (childTile->hasContent() && !childTile->isContentReady())
                        {
                            childTile->requestContent(nullptr);   
                            childTile->resolveContent();
                        }
                    }
                }
            }
        }

        void unload() override
        {
            unloadContent();
        }

        bool isHighestResolution() const override
        {
            return getNumChildren() == 0;
        }

        bool isLoadComplete() const override
        {
            auto t = const_cast<ThreeDTileNode*>(this);

            // Check to see if the content of this tile is loaded.
            bool isContentReady = false;
            if (t->hasContent())
            {
                isContentReady = t->isContentReady();
            }

            // If this tile has children, check to make sure it's content is loaded as well.  This will allow this tile to subdivide property.
            bool areChildrenReady = true;
            if (_children.valid())
            {
                for (unsigned int i = 0; i < _children->getNumChildren(); i++)
                {
                    osg::ref_ptr< ThreeDTileNode > childTile = dynamic_cast<ThreeDTileNode*>(_children->getChild(i));
                    if (childTile.valid())
                    {
                        if (childTile->hasContent() && !childTile->isContentReady())
                        {
                            areChildrenReady = false;
                        }
                    }
                }
            }

            return areChildrenReady && isContentReady;
        }

        bool getAutoUnload() const override
        {
            return _autoUnload;
        }

        void setAutoUnload(bool value) override
        {
            _autoUnload = value;
        }


    private:

        void createDebugBounds();

        void computeBoundingVolume();

        osg::ref_ptr< Tile > _tile;

        osg::ref_ptr< osg::Node > _content;
        osg::ref_ptr< osg::Group > _children;

        osg::ref_ptr< osg::Node > _boundsDebug;
        ThreeDTilesetNode* _tileset;

        Threading::Future< osg::ref_ptr<osg::Node> > _contentFuture;
        bool _requestedContent;

        bool _immediateLoad;

        bool _firstVisit;

        osg::BoundingBoxd _boundingBox;
        osg::Matrixd _boundingBoxLocalToWorld;

        osg::BoundingSphere _localBoundingSphere;

        osg::ref_ptr< osgDB::Options > _options;

        osg::Vec4 _debugColor;

        unsigned int _lastCulledFrameNumber;
        float _lastCulledFrameTime;

        bool _autoUnload = true;

        RefinePolicy _refine;

        osg::observer_ptr< ThreeDTileNode > _parentTile;
    };

    /**
    * Node representing a 3D-Tiles Tileset.
    */
    class OSGEARTH_EXPORT ThreeDTilesetNode : public osg::Group
    {
    public:
        ThreeDTilesetNode(Tileset* tileset, const std::string& authorizationHeader, SceneGraphCallbacks* sceneGraphCallbacks, osgDB::Options* options);

        float getMaximumScreenSpaceError() const;
        void setMaximumScreenSpaceError(float maximumScreenSpaceError);

        double getSSEDenominator() const;

        void touchTile(ThreeDTileNode* node);

        void traverse(osg::NodeVisitor& nv);

        const Tileset* getTileset() const { return _tileset.get(); }

        const osgDB::Options* getOptions() const { return _options.get(); }

        const std::string& getAuthorizationHeader() const { return _authorizationHeader; }
        void setAuthorizationHeader(const std::string& authorizationHeader) { _authorizationHeader = authorizationHeader; }

        //! Get the scene graph callback host
        SceneGraphCallbacks* getSceneGraphCallbacks(SceneGraphCallbacks* callbacks);

        //! Set the scene graph callback host
        void setSceneGraphCallbacks(SceneGraphCallbacks* callbacks);

        void runPreMergeOperations(osg::Node* node);
        void runPostMergeOperations(osg::Node* node);

        /**
         * Gets/sets the maximum number of tiles to keep in memory before expiring them.
         */
        unsigned int getMaxTiles() const;
        void setMaxTiles(unsigned int maxTiles);

        /**
         * Gets/sets the max age of tiles before they are considered for expiration.
         */
        float getMaxAge() const;
        void setMaxAge(float maxAge);

        /**
         * Turns on/off bounding volume visualization.
         */
        bool getShowBoundingVolumes() const;
        void setShowBoundingVolumes(bool showBoundingVolumes);

        /**
         * Turns on/off rendering a debug color per tile.
         */
        bool getColorPerTile() const;
        void setColorPerTile(bool colorPerTile);

        const std::string& getOwnerName() const;
        void setOwnerName(const std::string& name);

    private:
        void expireTiles(const osg::NodeVisitor& nv);

        osg::ref_ptr<Tileset> _tileset;
        osg::ref_ptr<osgDB::Options> _options;
        float _maximumScreenSpaceError;

        mutable std::mutex _mutex;
        ThreeDTileNode::TileTracker _tracker;
        ThreeDTileNode::TileTracker::iterator _sentryItr;

        unsigned int _maxTiles;
        float _maxAge;

        bool _showBoundingVolumes;
        bool _showColorPerTile;

        osg::ref_ptr< VirtualProgram> _debugVP;

        unsigned int _lastExpiredFrame;

        double _sseDenominator;

        std::string _authorizationHeader;

        osg::ref_ptr<SceneGraphCallbacks> _sgCallbacks;

        std::string _ownerName;
    };

} } }
