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

#include <osgEarth/Common>
#include <osgEarth/GLUtils>
#include <osgEarth/URI>
#include <osg/Object>
#include <osg/StateAttribute>
#include <osg/buffered_value>
#include <osg/Image>
#include <queue>
#include <unordered_set>

namespace osgEarth
{
    /**
     * A texture that can be GPU-resident or not. Managed by a TextureArena.
     *
     * Stages of residency:
     * 1. Not resident - only the URI is set
     * 2. CPU resident - osg::Image is loaded into memory
     * 3. GPU resident - GPU handle is in GPU table; GPU memory might not be allocated
     * 4. GPU commited - GPU handle active, GPU texture fully available
     */
    class OSGEARTH_EXPORT Texture
    {
    public:
        using Ptr = std::shared_ptr<Texture>;
        using WeakPtr = std::weak_ptr<Texture>;

        //! Create for a given texture target
        static Ptr create(
            osg::Image* image = nullptr,
            GLenum target = GL_TEXTURE_2D);

        //! Create from an existing OSG texture instance
        static Ptr create(
            osg::Texture* tex);

        //! Make this texture resident as a bindless texture
        void makeResident(const osg::State&, bool toggle) const;

        //! True if the texture is resident in state's context
        bool isResident(const osg::State&) const;

        //! True if the texture has been uploaded to the GPU 
        //! in the state's context
        bool isCompiled(const osg::State&) const;

        //! Whether we need to compile (or recompile) this texture
        bool needsCompile(const osg::State&) const;

        //! Wether the underlying image is dynamic
        //! and needs updates.
        bool needsUpdates() const;

        //! Update the underlying image if necessary
        void update(osg::NodeVisitor& nv);

        //! GL memory functions
        //! Returns true if the object needed compiling and was compiled.
        bool compileGLObjects(osg::State&) const;
        void resizeGLObjectBuffers(unsigned);
        void releaseGLObjects(osg::State*, bool force = false) const;

        OE_PROPERTY(std::string, name, {});
        OE_PROPERTY(std::string, category, {});
        OE_PROPERTY(bool, compress, true);
        OE_PROPERTY(bool, mipmap, true);
        OE_PROPERTY(bool, clamp_s, false);
        OE_PROPERTY(bool, clamp_t, false);
        OE_PROPERTY(bool, clamp_r, false);
        OE_PROPERTY(bool, keepImage, true);
        OE_PROPERTY(unsigned, maxDim, 65536);

        OE_OPTION(URI, uri);
        OE_OPTION(GLenum, internalFormat);
        OE_OPTION(float, maxAnisotropy);

        OE_OPTION(float, minValue);
        OE_OPTION(float, maxValue);

        OE_PROPERTY(osg::ref_ptr<osg::Texture>, osgTexture, {});
        OE_PROPERTY_CONST(GLenum, target, GL_TEXTURE_2D);

        GLenum getPixelFormat() const;

        //! Whether any actual image data has been loaded into this object,
        //! or whether we need to load it from the URI
        bool hasImage() const;

        //! Whether this is a texture used solely for RTT
        bool isFBO() const;

        //! GLObjects are shareable across GCs because they are static and bindless.
        struct GLObjects : public BindlessShareableGLObjects
        {
            GLTexture::Ptr _gltexture;
            unsigned _imageModCount = 0u;
        };
        mutable osg::buffered_object<GLObjects> _globjects;

        // for gpu paging support:
        OE_PROPERTY(int, ownerRevision, -1);
        OE_PROPERTY(bool, dormant, false);

        ~Texture();

        GLTexture::Ptr getGLObject(osg::State&) const;

    protected:
        Texture(GLenum target);
        Texture(osg::Texture*);
        void* _host;
        friend class TextureArena;
    };

    typedef std::vector<Texture::Ptr> TextureVector;

    /**
     * TextureArena is a dynamic bindless texture atlas. Add as many texture
     * as you want and control memory management by activating and deactivating
     * them.
     *
     * Call add() to have the arena start managing a Texture.
     * After that, you can call activate() or deactivate() on individual textures
     * to control their availability on the GPU.
     */
#define OE_TEXTURE_ARENA_SA_TYPE_ID (osg::StateAttribute::Type)(osg::StateAttribute::CAPABILITY+8675309)

    class OSGEARTH_EXPORT TextureArena : public osg::StateAttribute
    {
    public:
        META_StateAttribute(osgEarth, TextureArena, OE_TEXTURE_ARENA_SA_TYPE_ID);

        //! Construct an empty arena.
        TextureArena();

        //! Whether to automatically release textures when they are no longer
        //! referenced anywhere outside of this arena. Default is false.
        //! You can enable this if you want to populate the arena with
        //! textures that will be released when their owners desctruct (for example).
        void setAutoRelease(bool value);
        bool getAutoRelease() const { return _autoRelease; }

        //! Whether to automatically free the GPU memory associates with dormant
        //! textures. A dormat texture exists in the arena, but has not been accessed
        //! by a recent call to add(). Auto Paging can greatly reduce GPU memory usage
        //! at the expense of potential frame drops. Default is false.
        //! When true, the developer must update the arena's revision (by calling
        //! setRevision) before calling add() to use textures.
        void setAutoPaging(bool value);
        bool getAutoPaging() const { return _autoPaging; }

        //! When auto paging is enabled, sets the current revision that textures must
        //! match to avoid being paged out of the GPU.
        void setRevision(int value) { _ownerRevision = value; }

        //! Sets the GLSL binding point for the arena. Default is 1.
        void setBindingPoint(unsigned value);

        //! Sets the maximum texture size (in one dimension, in pixels)
        //! to allocate on the GPU.
        void setMaxTextureSize(unsigned pixels);

        //! Adds a texture to the arena. On next apply() GL handles will allocate.
        //! You only need to call this once for each texture.
        //! Returns the index of the texture handle
        int add(
            Texture::Ptr tex,
            const osgDB::Options* readOptions = nullptr);

        //! Find and return the index of this texture in the arena,
        //! or return -1 if not found.
        int find(Texture::Ptr tex) const;

        //! Find the texture at index i.
        Texture::Ptr find(unsigned index) const;

        //! Number of textures registered
        size_t size() const { return _textures.size(); }

        //! Textures installed in this arena
        const TextureVector& getTextures() const { return _textures; }

        //! destruct
        virtual ~TextureArena();

        //! Update traversal (called automatically if this TexturArena
        //! is installed in a StateSet in the traversed scene graph)
        void update(osg::NodeVisitor& nv);

        //! Flush any unused textures immediately from the arena
        void flush();

    public: // StateAttribute
        void apply(osg::State&) const override;
        void compileGLObjects(osg::State&) const override;
        void resizeGLObjectBuffers(unsigned) override;
        void releaseGLObjects(osg::State*) const override;
        int compare(const osg::StateAttribute& rhs) const override { return -1; }

    private:
        //! disable copy
        TextureArena(const TextureArena&, const osg::CopyOp&) { }

        void releaseGLObjects(osg::State*, bool force) const;

        // GL objects that can be shared across contexts
        struct GLObjects : public BindlessShareableGLObjects
        {
            bool _inUse = false;
            bool _handleBufferDirty = true;
            int _lastAppliedFrame = -1;

            std::queue<int> _toCompile;
            TextureVector _toRemove;
            GLBuffer::Ptr _handleBuffer;
            std::vector<GLuint64> _handles;
        };
        mutable osg::buffered_object<GLObjects> _globjects;

        mutable TextureVector _textures;
        mutable std::unordered_map<Texture*, unsigned> _textureIndices;
        mutable std::unordered_set<unsigned> _dynamicTextures;

        bool _autoRelease = false;
        unsigned _bindingPoint = 1u;
        bool _useUBO = false;
        mutable int _releasePtr = 0;
        unsigned _maxDim = 65536u;
        bool _autoPaging = false;
        int _ownerRevision = 0;

        mutable Mutex _m;

        int find_no_lock(Texture::Ptr tex) const;
        void purgeTextureIfOrphaned_no_lock(unsigned index);

        friend class Texture;
        void notifyOfTextureRelease(osg::State*) const;
    };
}
