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

#include <osgEarth/Common>
#include <osgEarth/Config>
#include <osgEarth/ColorFilter>
#include <osgEarth/TileLayer>
#include <osgEarth/URI>
#include <osgEarth/Threading>
#include <osgEarth/Callbacks>
#include <osg/Texture2D>

namespace osgEarth
{
    class Profile;
}

namespace osgEarth
{
    /**
     * Texture paired with a scale/bias matrix defining a sub-window.
     */
    class OSGEARTH_EXPORT TextureWindow
    {
    public:
        //! Empty/invalid texture window
        TextureWindow() : _texture(0L) { }
        //! Construct a texture window
        TextureWindow(osg::Texture* tex, const osg::Matrix& scalebias) : _texture(tex), _matrix(scalebias) { }
        //! Texture
        osg::Texture* getTexture() const { return _texture.get(); }
        //! Scale bias matrix defining subwindow
        const osg::Matrixf& getMatrix() const { return _matrix; }
        //! Is it valid?
        bool valid() const { return _texture.valid(); }

    protected:
        osg::ref_ptr<osg::Texture> _texture;
        osg::Matrixf _matrix;
    };

    /**
     * A map terrain layer containing bitmap image data.
     */
    class OSGEARTH_EXPORT ImageLayer : public TileLayer
    {
    public: // Serialization
        class OSGEARTH_EXPORT Options : public TileLayer::Options {
        public:
            META_LayerOptions(osgEarth, Options, TileLayer::Options);
            OE_OPTION(URI, noDataImageFilename);
            OE_OPTION(osg::Vec4ub, transparentColor, osg::Vec4ub(0, 0, 0, 0));
            OE_OPTION(ColorFilterChain, colorFilters);
            OE_OPTION(osg::Texture::FilterMode, minFilter, osg::Texture::LINEAR_MIPMAP_LINEAR);
            OE_OPTION(osg::Texture::FilterMode, magFilter, osg::Texture::LINEAR);
            OE_OPTION(std::string, textureCompression);
            OE_OPTION(double, edgeBufferRatio, 0.0);
            OE_OPTION(unsigned, reprojectedTileSize, 256u);
            OE_OPTION(Distance, altitude);
            OE_OPTION(bool, coverage, false);
            OE_OPTION(bool, acceptDraping, false);
            OE_OPTION(bool, async, false);
            OE_OPTION(bool, shared, false);
            OE_OPTION(std::string, shareTexUniformName);
            OE_OPTION(std::string, shareTexMatUniformName);
            OE_OPTION(bool, morphImagery, true);
            virtual Config getConfig() const;
        private:
            void fromConfig(const Config& conf);
        };

    public:
        META_Layer_Abstract(osgEarth, ImageLayer, Options, TileLayer);

        //! Convenience function to create an ImageLayer from a ConfigOptions.
        static ImageLayer* create(const ConfigOptions& conf);

        //! Adds a color filter to the filter chain.
        OE_DEPRECATED("Use GLSL")
        void addColorFilter(ColorFilter* filter);

        //! Removes a color filter from the filter chain
        OE_DEPRECATED("Use GLSL")
        void removeColorFilter(ColorFilter* filter);

        //! Accesses the color filter chain
        const ColorFilterChain& getColorFilters() const;

        //! Sets the altitude
        void setAltitude(const Distance& value);
        const Distance& getAltitude() const;

        //! Sets whether this layer should allow draped overlays
        //! to render on it. This is most applicable to layers with a
        //! non-zero altitude (setAltitude). Default is true.
        void setAcceptDraping(bool value);
        bool getAcceptDraping() const;

        //! Marks this layer for asynchronous loading.
        //! Usually all layers participating in a tile must load before the
        //! tile is displayed. This flag defers the current layer so it can 
        //! load asynchronously and display when it is available. This can
        //! help keep a slow-loading layer from blocking the rest of the tile
        //! from displaying. The trade-off is possible visual artifacts
        //! (flashing, no mipmaps/compression) when the new data appears.
        void setAsyncLoading(bool value);
        bool getAsyncLoading() const;

        //! Whether this layer is marked for render sharing.
        //! Only set this before opening the layer or adding it to a map.
        void setShared(bool value);
        bool getShared() const;
        bool isShared() const { return getShared(); }

        //! Whether this layer represents coverage data that should not be subject
        //! to color-space filtering, interpolation, or compression.
        //! Only set this before opening the layer or adding it to a map.
        void setCoverage(bool value);
        bool getCoverage() const;
        bool isCoverage() const { return getCoverage(); }

        //! When isShared() == true, this will return the name of the uniform holding the
        //! image's texture.
        void setSharedTextureUniformName(const std::string& value);
        const std::string& getSharedTextureUniformName() const;

        //! When isShared() == true, this will return the name of the uniform holding the
        //! image's texture matrix.
        void setSharedTextureMatrixUniformName(const std::string& value);
        const std::string& getSharedTextureMatrixUniformName() const;

        //! When isShared() == true, the engine will call this function to bind the
        //! shared layer to a texture image unit.
        optional<int>& sharedImageUnit() { return _shareImageUnit; }
        const optional<int>& sharedImageUnit() const { return _shareImageUnit; }

        osg::Image* getEmptyImage() const { return _emptyImage.get(); }

        //! When the terrain's imagery morphing is globally enabled, whether to 
        //! imagery-morph THIS layer.
        void setMorphImagery(bool value);
        bool getMorphImagery() const;

    public: // methods

        //! Creates an image for the given tile key.
        //! @param key TileKey for which to create an image
        //! @param progress Optional progress/cancelation callback
        GeoImage createImage(const TileKey& key);

        //! Creates an image for the given tile key.
        //! @param key TileKey for which to create an image
        //! @param progress Optional progress/cancelation callback
        GeoImage createImage(const TileKey& key, ProgressCallback* progress);

        //! Stores an image in this layer (if writing is enabled).
        //! Returns a status value indicating whether the store succeeded.
        Status writeImage(const TileKey& key, const osg::Image* image, ProgressCallback* progress = {});

        //! Stores a pre-encoded image in this layer (if encoded writing is supported).
        //! Returns s tatus value indicating whether the store succeeded.
        Result<osg::ref_ptr<osg::Image>> encodeImage(const TileKey& key, const osg::Image* image, ProgressCallback* progress = {});

        //! Returns the compression method prefered by this layer
        //! that you can pass to ImageUtils::compressImage.
        const std::string getCompressionMethod() const;

        //! Fired after a new image is created, but before it is cached.
        Callback<void(const TileKey&, GeoImage&)> onCreate;

        //! Add a post-processing layer
        void addPostLayer(Layer* layer);


    public: // Texture support

        //! Whether to use createTexture to create data for this layer
        //! instead of createImage and a TileSource driver
        bool useCreateTexture() const { return _useCreateTexture; }

        //! Override this method to create the texture when useCreateTexture is true
        virtual TextureWindow createTexture(const TileKey& key, ProgressCallback* progress) const
        {
            return TextureWindow();
        }

        //! Subclass overrides this to generate image data for the key.
        //! The key will always be in the same profile as the layer.
        virtual GeoImage createImageImplementation(const TileKey&, ProgressCallback* progress) const
        {
            return GeoImage::INVALID;
        }

    protected:

        //! Subclass can override this to write data for a tile key.
        virtual Status writeImageImplementation(const TileKey&, const osg::Image*, ProgressCallback*) const;

        virtual Result<osg::ref_ptr<osg::Image>> encodeImageImplementation(const TileKey&, const osg::Image*, ProgressCallback*) const;

        //! Modify the bbox if an altitude is set (for culling)
        virtual void modifyTileBoundingBox(const TileKey& key, osg::BoundingBox& box) const;

        //! Post processing image creation entry points
        GeoImage createImage(
            const GeoImage& canvas,
            const TileKey& key,
            ProgressCallback* progress);

        //! Override to write an image over top of an existing image
        virtual GeoImage createImageImplementation(
            const GeoImage& canvas,
            const TileKey& key,
            ProgressCallback* progress) const {
            return canvas;
        }

        //! Override to do something to an image before returning
        //! it from createImage (including a GeoImage read from the cache)
        virtual void postCreateImageImplementation(
            GeoImage& createdImage,
            const TileKey& key,
            ProgressCallback* progress) const { }

    protected: // Layer

        virtual void init() override;

        //! Open the layer for reading.
        virtual Status openImplementation() override;

        /** dtor */
        virtual ~ImageLayer() { }

        //! Configure the layer to create textures via createTexture instead of
        //! using a createImage driver
        void setUseCreateTexture();

        //! Apply a post-processing layers to the image
        virtual GeoImage applyPostLayer(
            const GeoImage& image,
            const TileKey& key,
            Layer* postLayer,
            ProgressCallback* progress) const;

        osg::ref_ptr<osg::Image> _emptyImage;

    private:

        // Creates an image that's in the same profile as the provided key.
        GeoImage createImageInKeyProfile(
            const TileKey& key,
            ProgressCallback* progress);

        // Fetches multiple images from the TileSource; mosaics/reprojects/crops as necessary, and
        // returns a single tile. This is called by createImageFromTileSource() if the key profile
        // doesn't match the layer profile.
        GeoImage assembleImage(
            const TileKey& key,
            ProgressCallback* progress);

        // Creates an image that enhances the previous LOD's image
        // using a fractal algorithm.
        GeoImage createFractalUpsampledImage(
            const TileKey& key,
            ProgressCallback* p);

        optional<int> _shareImageUnit;
        bool _useCreateTexture;

        Mutexed<std::vector<osg::ref_ptr<Layer>>> _postLayers;

        Gate<TileKey> _sentry;

        osg::ref_ptr<osg::Image> _nodataImage;
    };

    typedef std::vector< osg::ref_ptr<ImageLayer> > ImageLayerVector;

    /**
     * Texture that loads its data asynchronously
     */
    class OSGEARTH_EXPORT FutureTexture
    {
    public:
        virtual bool doneLoading() { update(); return _resolved; }
        virtual bool succeeded() { update(); return _resolved && !_failed; }
        virtual bool failed() { update(); return _resolved && _failed; }

    protected:
        FutureTexture() : _resolved(false), _failed(false) { }
        bool _resolved, _failed;
        virtual void update() = 0;
    };

    class OSGEARTH_EXPORT FutureTexture2D :
        public osg::Texture2D,
        public FutureTexture
    {
    public:
        FutureTexture2D(
            ImageLayer* layer,
            const TileKey& key);

    protected:
        virtual ~FutureTexture2D() { }

        void update() override;

    private:
        TileKey _key;
        osg::ref_ptr<ImageLayer> _layer;
        mutable Future<GeoImage> _result;

        void dispatch() const;
    };

} // namespace osgEarth

OSGEARTH_SPECIALIZE_CONFIG(osgEarth::ImageLayer::Options);
