/* osgEarth
 * Copyright 2025 Pelican Mapping
 * MIT License
 */

#ifndef OSGEARTH_IMAGEUTILS_H
#define OSGEARTH_IMAGEUTILS_H

#include <osgEarth/Common>
#include <osgEarth/Bounds>
#include <osgEarth/optional>
#include <osg/Image>
#include <osg/Texture>
#include <osg/Texture2DArray>
#include <osg/GL>
#include <osg/NodeVisitor>
#include <osgDB/ReaderWriter>
#include <vector>
#include <functional>

//These formats were not added to OSG until after 2.8.3 so we need to define them to use them.
#ifndef GL_EXT_texture_compression_rgtc
  #define GL_COMPRESSED_RED_RGTC1_EXT                0x8DBB
  #define GL_COMPRESSED_SIGNED_RED_RGTC1_EXT         0x8DBC
  #define GL_COMPRESSED_RED_GREEN_RGTC2_EXT          0x8DBD
  #define GL_COMPRESSED_SIGNED_RED_GREEN_RGTC2_EXT   0x8DBE
#endif

#ifndef GL_IMG_texture_compression_pvrtc
    #define GL_COMPRESSED_RGB_PVRTC_4BPPV1_IMG      0x8C00
    #define GL_COMPRESSED_RGB_PVRTC_2BPPV1_IMG      0x8C01
    #define GL_COMPRESSED_RGBA_PVRTC_4BPPV1_IMG     0x8C02
    #define GL_COMPRESSED_RGBA_PVRTC_2BPPV1_IMG     0x8C03
#endif

namespace osgEarth { namespace Util
{
    class OSGEARTH_EXPORT ImageUtils
    {
    public:
        /**
         * Clones an image.
         *
         * Use this instead of the osg::Image copy construtor, which keeps the referenced to
         * its underlying BufferObject around. Calling dirty() on the new clone appears to
         * help, but just call this method instead to be sure.
         */
        static osg::Image* cloneImage( const osg::Image* image );

        /**
         * Copys a portion of one image into another.
         */
        static bool copyAsSubImage(
            const osg::Image* src,
            osg::Image*       dst,
            int dst_start_col, int dst_start_row);

        /**
         * Resizes an image. Returns a new image, leaving the input image unaltered.
         *
         * Note. If the output parameter is NULL, this method will allocate a new image and
         * resize into that new image. If the output parameter is non-NULL, this method will
         * assume that the output image is already allocated to the proper size, and will
         * do a resize+copy into that image. In the latter case, it is your responsibility
         * to make sure the output image is allocated to the proper size.
         *
         * If the output parameter is non-NULL, then the mipmapLevel is also considered.
         * This lets you resize directly into a particular mipmap level of the output image.
         */
        static bool resizeImage(
            const osg::Image* input,
            unsigned int new_s, unsigned int new_t,
            osg::ref_ptr<osg::Image>& output,
            unsigned int mipmapLevel =0, bool bilinear=true );

        /**
         * Crops the input image to the dimensions provided and returns a
         * new image. Returns a new image, leaving the input image unaltered.
         * Note:  The input destination bounds are modified to reflect the bounds of the
         *        actual output image.  Due to the fact that you cannot crop in the middle of a pixel
         *        The specified destination extents and the output extents may vary slightly.
         *@param src_minx
         *       The minimum x coordinate of the input image.
         *@param src_miny
         *       The minimum y coordinate of the input image.
         *@param src_maxx
         *       The maximum x coordinate of the input image.
         *@param src_maxy
         *       The maximum y coordinate of the input image.
         *@param dst_minx
         *       The desired minimum x coordinate of the cropped image.
         *@param dst_miny
         *       The desired minimum y coordinate of the cropped image.
         *@param dst_maxx
         *       The desired maximum x coordinate of the cropped image.
         *@param dst_maxy
         *       The desired maximum y coordinate of the cropped image.
         */
        static osg::Image* cropImage(
            const osg::Image* image,
            double src_minx, double src_miny, double src_maxx, double src_maxy,
            double &dst_minx, double &dst_miny, double &dst_maxx, double &dst_maxy);

        /**
         * Crops the input image using the input dimension
         *  Returns a new image, leaving the input image unaltered.
         *@param x
         *       The cropping x coordinate of the input image.
         *@param y
         *       The cropping y coordinate of the input image.
         *@param width
         *       The width in pixels of the crop.
         *@param height
         *       The height in pixels of the crop.
         */
        static osg::Image* cropImage(osg::Image* image, unsigned int x, unsigned int y, unsigned int width, unsigned int height);

        /**
         * Blends the "src" image into the "dest" image, based on the "a" value.
         * The two images must be the same.
         */
        static bool mix( osg::Image* dest, const osg::Image* src, float a );

        /**
         * Creates and returns a copy of the input image after applying a
         * sharpening filter. Returns a new image, leaving the input image unaltered.
         */
        static osg::Image* createSharpenedImage( const osg::Image* image );

        /**
         * For each "layer" in the input image (each bitmap in the "r" dimension),
         * create a new, separate image with r=1.
         * Returns true upon sucess, false upon failure or if r < 2
         */
        static bool flattenImage(
            const osg::Image* image,
            std::vector<osg::ref_ptr<osg::Image> >& output);

        /**
         * Gets whether the input image's dimensions are powers of 2.
         */
        static bool isPowerOfTwo(const osg::Image* image);

        /**
         * Gets a transparent, single pixel image used for a placeholder
         */
        static osg::Image* createEmptyImage();

        /**
         * Gets a transparent image used for a placeholder with the specified dimensions
         */
        static osg::Image* createEmptyImage(unsigned int s, unsigned int t, unsigned int r = 1);

        /**
         * Creates a one-pixel image.
         */
        static osg::Image* createOnePixelImage(const osg::Vec4& color);

        /**
         * Tests an image to see whether it's "empty", i.e. completely transparent,
         * within an alpha threshold.
         */
        static bool isEmptyImage(const osg::Image* image, float alphaThreshold =0.01);

        /**
         * Tests an image to see whether it's "single color", i.e. completely filled with a single color,
         * within an threshold (threshold is tested on each channel).
         */
        static bool isSingleColorImage(const osg::Image* image, float threshold =0.01);

        /**
         * Returns true if it is possible to convert the image to the specified
         * format/datatype specification.
         */
        static bool canConvert(const osg::Image* image, GLenum pixelFormat, GLenum dataType);

        /**
         * Converts an image to the specified format.
         */
        static osg::Image* convert(const osg::Image* image, GLenum pixelFormat, GLenum dataType);

        /**
         *Converts the given image to RGB8
         */
        static osg::Image* convertToRGB8(const osg::Image* image);

        /**
         *Converts the given image to RGBA8
         */
        static osg::Image* convertToRGBA8(const osg::Image* image);

        /**
         * True if the two images are of the same format (pixel format, data type, etc.)
         * though not necessarily the same size, depth, etc.
         */
        static bool sameFormat(const osg::Image* lhs, const osg::Image* rhs);

        /**
         * True if the two images have the same format AND size, and can therefore
         * be used together in a texture array.
         */
        static bool textureArrayCompatible(const osg::Image* lhs, const osg::Image* rhs);

        /**
         *Compares the image data of two images and determines if they are equivalent
         */
        static bool areEquivalent(const osg::Image *lhs, const osg::Image *rhs);

        /**
         * Whether two colors are roughly equivalent.
         */
        static bool areRGBEquivalent( const osg::Vec4& lhs, const osg::Vec4& rhs, float epsilon =0.01f ) {
            return
                fabs(lhs.r() - rhs.r()) < epsilon &&
                fabs(lhs.g() - rhs.g()) < epsilon &&
                fabs(lhs.b() - rhs.b()) < epsilon;
        }

        /**
         * Checks whether the image has an alpha component
         */
        static bool hasAlphaChannel( const osg::Image* image );

        /**
         * Checks whether an image has transparency; i.e. whether
         * there are any pixels with an alpha component whole value
         * falls below the specified threshold.
         */
        static bool hasTransparency(const osg::Image* image, float alphaThreshold =1.0f);

        /**
         * Converts an image (in place) to premultiplied-alpha format.
         * Returns False is the conversion fails, e.g., if there is no reader
         * or writer for the image format.
         */
        static bool convertToPremultipliedAlpha(osg::Image* image);

        /**
         * Checks whether the given image is compressed
         */
        static bool isCompressed( const osg::Image* image );

        /**
         * Generated a bump map image for the input image
         */
        static osg::Image* createBumpMap( const osg::Image* input );

        /**
         * Is it a floating-point texture format?
         */
        static bool isFloatingPointInternalFormat( GLint internalFormat );

        /**
         * Returns the string representation of a GL texture internal format enum.
         * For example, given GL_RGBA8 the method will return the string "GL_RGBA8".
         */
        static std::string getInternalFormatString( GLint internalFormat );

        /**
         * Compute a texture compression format suitable for the image.
         */
        static bool computeTextureCompressionMode(
            const osg::Image* image,
            osg::Texture::InternalFormatMode& out_mode);

        /**
         * Replaces "no data" values in the target image with the corresponding
         * value found in the "reference" image. The images much be GL_LUMINANCE
         * data types.
         */
        static bool replaceNoDataValues(
            osg::Image*       target,
            const Bounds&     targetBounds,
            const osg::Image* reference,
            const Bounds&     referenceBounds);

        /**
         * Bicubic upsampling in a quadrant. Target image is already allocated.
         */
        static bool bicubicUpsample(
            const osg::Image* source,
            osg::Image* target,
            unsigned quadrant,
            unsigned stride);

        /**
         * Activates mipmapping for a texture image if the correct filters exist.
         *
         * If OSG has an ImageProcessor service installed, this method will use that
         * to generate mipmaps. If not osgEarth will generate them if possible.

         * Returns true if mipmaps were added
         */
        //static bool generateMipmaps(osg::Texture* texture);

        /**
        * Adds mipmaps to an image.
        * Returns false if the image already has mipmaps, or if we cannot
        * generate mipmaps for some reason.
        */
        //static bool generateMipmaps(osg::Image* image);

        /**
         * Creates a copy of the input image with added mipmaps.
         * @param image Input image to generate mipmaps for
         * @param minLevelSize The smallest mipmap level size to generate
         * @return image with mipmaps. If the input already had mipmaps,
         *   just returns the input pointer (that is why it's const)
         */
        static const osg::Image* mipmapImage(
            const osg::Image* image,
            int minLevelSize = 16);

        /**
        * Adds mipmaps to an existing image if neccessary
        * @param image Input image to generate mipmaps for
        */
        static void mipmapImageInPlace(
            osg::Image* image);

        //! Returns a compressed copy of the input image.
        //! @param image Image to compress
        //! @param method Compression method to use; see ImageLayer::getCompressionMethod
        static const osg::Image* compressImage(
            const osg::Image* image,
            const std::string& method ="cpu");

        //! Compressed an exsting image.
        //! @param image Image to compress
        //! @param method Compression method to use; see ImageLayer::getCompressionMethod
        static void compressImageInPlace(
            osg::Image* image,
            const std::string& method ="auto");

        //! Compresses and mipmaps all textures in the given subgraph
        //! @param node The node to process
        static void compressAndMipmapTextures(osg::Node* node);

        //! Gets an osgDB::ReaderWriter for the given input stream.
        //! Returns NULL if no ReaderWriter can be found.
        static osgDB::ReaderWriter* getReaderWriterForStream(std::istream& stream);

        //! Gets an osgDB::ReaderWriter for the given input string
        //! Returns NULL if no ReaderWriter can be found.
        static osgDB::ReaderWriter* getReaderWriterForString(const std::string& input);

        //! Reads an osg::Image from the given input stream.
        //! Returns NULL if the image could not be read.
        static osg::Image* readStream(std::istream& stream, const osgDB::Options* options);

        //! Make an empty 2D texture array
        static osg::Texture2DArray* makeTexture2DArray(osg::Image* image);

        //! If a max texture size is stipulated in the options, and it
        //! the image supports it, return the size.
        static optional<int> getMaxTextureSize(const osg::Image*, const osgDB::Options*);

        //! Convert u,v [0..1] into s,t [pixels] with nearest neighbor sampling.
        template<typename NDCTYPE, typename INDEXTYPE>
        static void nnUVtoST(NDCTYPE u, NDCTYPE v, INDEXTYPE& s, INDEXTYPE& t, unsigned width, unsigned height) {
            const NDCTYPE umin = 1.0 / (2.0 * width);
            const NDCTYPE vmin = 1.0 / (2.0 * height);
            s = u<umin ? 0 : u>(1.0 - umin) ? width - 1 : (int)floor(u*(double)width);
            t = v<vmin ? 0 : v>(1.0 - vmin) ? height - 1 : (int)floor(v*(double)height);
        }

        /**
         * Convenience object to iterate over an image with a lamdba function
         */
        struct ImageIterator
        {
        public:
            ImageIterator(const osg::Image* image) :
                _image(image) { }

            //ImageIterator(PixelReader& reader) :
            //    _image(reader._image) { }

            //ImageIterator(PixelWriter& writer) :
            //    _image(writer._image) { }

            inline int r() const { return _r; }
            inline int s() const { return _s; }
            inline int t() const { return _t; }
            inline int m() const { return 0; }
            inline double u() const { return _u; }
            inline double v() const { return _v; }
            inline void quit() { _break = true; }

            template<typename CALLABLE>
            inline void forEachPixel(CALLABLE&& func)
            {
                _break = false;
                int tt = _image->t(), ss = _image->s(), rr = _image->r();
                for (_t = 0; _t < tt && !_break; ++_t)
                {
                    _v = (double)_t / (double)(_image->t() - 1);

                    for (_s = 0; _s < ss && !_break; ++_s)
                    {
                        _u = (double)_s / (double)(_image->s() - 1);

                        for (_r = 0; _r < rr && !_break; ++_r)
                        {
                            func(*this);
                        }
                    }
                }
            }

        private:
            const osg::Image* _image;
            int    _r, _s, _t;
            double _u, _v;
            bool _break = false;
        };

        /**
         * Reads color data out of an image, regardles of its internal pixel format.
         */
        class OSGEARTH_EXPORT PixelReader
        {
        public:
            //! Empty pixel reader. Must call setImage before use.
            PixelReader();

            /**
             * Constructs a pixel reader. "Normalized" means that the values in the source
             * image have been scaled to [0..1] and should be denormalized upon reading.
             */
            PixelReader(const osg::Image* image);

            /** Sets an image to read. */
            void setImage(const osg::Image* image);

            //! Sets a texture whose first image to read (along with texture params)
            void setTexture(const osg::Texture* tex);

            //! Whether to denormalize data upon reading or to leave it as-is
            void setDenormalize(bool value) { _normalized = value; }

            /** Whether to use bilinear interpolation when reading with u,v coords (default=true) */
            void setBilinear(bool value) { _bilinear = value; }

            //! Whether to sample the image like a texture (GLSL)
            void setSampleAsTexture(bool value) { _sampleAsTexture = value; }

            //! Whether to sample with texture-wrapping
            void setSampleAsRepeatingTexture(bool value) { _sampleAsRepeatingTexture = value; }

            /** Whether PixelReader supports a given format/datatype combiniation. */
            static bool supports( GLenum pixelFormat, GLenum dataType );

            /** Whether PixelReader can read from the specified image. */
            static bool supports( const osg::Image* image ) {
                return image && supports(image->getPixelFormat(), image->getDataType() );
            }
            
            //! Whether the image is valid and usable
            bool valid() const {
                return _image != nullptr && _read != nullptr;
            }

            inline int s() const { return _image->s(); }
            inline int t() const { return _image->t(); }
            inline int r() const { return _image->r(); }

            //! Returns an iterator into this image
            ImageIterator iterator() const {
                return ImageIterator(_image);
            }

            //! Iterator over this image with the user function CALLABLE
            //! with the signature void CALLABLE(ImageIterator&)
            template<typename CALLABLE>
            void forEachPixel(CALLABLE&& func) const {
                ImageIterator(_image).forEachPixel(func);
            }

            //! Returns a color from an image at pixel (s,t,r,m)
            inline osg::Vec4f operator()(int s, int t, int r=0, int m=0) const {
                return _read(this, s, t, r, m);
            }
            inline void operator()(osg::Vec4f& output, int s, int t, int r=0, int m=0) const {
                output = _read(this, s, t, r, m);
            }

            //! Returns a color from an image at pixel (s,t,r,m)
            inline osg::Vec4f operator()(unsigned s, unsigned t, unsigned r=0, int m=0) const {
                return _read(this, s, t, r, m);
            }            
            inline void operator()(osg::Vec4f& output, unsigned s, unsigned t, int r=0, int m=0) const {
                output = _read(this, s, t, r, m);
            }

            //! composite version of pixel read operator
            template<typename T>
            inline osg::Vec4f operator()(const T& composite) const {
                return (*this)(composite.s(), composite.t(), composite.r());
            }
            template<typename T>
            inline void operator()(osg::Vec4f& output, const T& composite) const {
                output = _read(this, composite.s(), composite.t(), composite.r(), 0);
            }

            /** Reads a color from the image by unit coords [0..1] */
            osg::Vec4f operator()(float u, float v, int r=0, int m=0) const;
            
            inline void operator()(osg::Vec4f& output, float u, float v, int r=0, int m=0) const {
                output = operator()(u, v, r, m);
            }

            osg::Vec4f operator()(double u, double v, int r=0, int m=0) const;

            inline void operator()(osg::Vec4f& output, double u, double v, int t = 0, int m = 0) const {
                output = operator()(u, v, t, m);
            }

            // internals:
            const unsigned char* data(int s=0, int t=0, int r=0, int m=0) const {
                return m == 0 ?
                    _image->data() + s*_colBytes + t*_rowBytes + r*_imageBytes :
                    _image->getMipmapData(m-1) + (s>>m)*_colBytes + (t>>m)*(_rowBytes>>m) + r*(_imageBytes>>m);
            }

            typedef osg::Vec4f (*ReaderFunc)(const PixelReader* ia, int s, int t, int r, int m);

            ReaderFunc _read;
            const osg::Image* _image;
            unsigned _colBytes;
            unsigned _rowBytes;
            unsigned _imageBytes;
            bool     _normalized;
            bool     _bilinear;
            bool     _sampleAsTexture;
            bool     _sampleAsRepeatingTexture;
        };

        /**
         * Writes color data to an image, regardles of its internal pixel format.
         */
        class OSGEARTH_EXPORT PixelWriter
        {
        public:
            /**
             * Constructs a pixel writer. "Normalized" means the values are scaled to [0..1]
             * before writing.
             */
            PixelWriter(osg::Image* image);

            //! Whether data should be normalized to [0..1] or left as-is
            void setNormalize(bool value) { _normalized = value; }

            /** Whether PixelWriter can write to an image with the given format/datatype combo. */
            static bool supports( GLenum pixelFormat, GLenum dataType );

            /** Whether PixelWriter can write to non-const version of an image. */
            static bool supports( const osg::Image* image ) {
                return image && supports(image->getPixelFormat(), image->getDataType() );
            }

            inline int s() const { return _image->s(); }
            inline int t() const { return _image->t(); }
            inline int r() const { return _image->r(); }

            //! Initialize to all one value
            void assign(const osg::Vec4& c);

            //! Initialize to all one value (one layer)
            void assign(const osg::Vec4& c, int r);

            /** Writes a color to a pixel. */
            inline void operator()(const osg::Vec4& c, int s, int t, int r=0, int m=0) {
                (*_writer)(this, c, s, t, r, m );
            }

            inline void f(const osg::Vec4& c, float s, float t, int r=0, int m=0) {
                this->operator()( c,
                    (int)(s * (float)(_image->s()-1)),
                    (int)(t * (float)(_image->t()-1)),
                    r, m);
            }

            //! composite version of pixel write operator
            template<typename T>
            inline void operator()(const osg::Vec4& c, const T& composite) const {
                (*_writer)(this, c, composite.s(), composite.t(), composite.r(), composite.m());
            }

            //! Iterator over this image with the user function CALLABLE
            //! with the signature void CALLABLE(ImageIterator&)
            template<typename CALLABLE>
            void forEachPixel(CALLABLE&& func) const {
                ImageIterator(_image).forEachPixel(func);
            }

            // internals:
            osg::Image* _image;
            unsigned _colBytes;
            unsigned _rowBytes;
            unsigned _imageBytes;
            bool     _normalized;

            unsigned char* data(int s=0, int t=0, int r=0, int m=0) const;

            typedef void (*WriterFunc)(const PixelWriter* iw, const osg::Vec4 c, int s, int t, int r, int m);
            WriterFunc _writer;
        };

        /**
         * Convenience object to iterate over an image, with or without
         * a geospatial extent, with a lambda function
         */
        template<typename EXTENT>
        struct ImageIteratorWithExtent
        {
        public:
            ImageIteratorWithExtent(const osg::Image* image, const EXTENT& extent) :
                _image(image), _extent(extent) { }

            ImageIteratorWithExtent(PixelReader& reader, const EXTENT& extent) :
                _image(reader._image), _extent(extent) { }

            ImageIteratorWithExtent(PixelWriter& writer, const EXTENT& extent) :
                _image(writer._image), _extent(extent) { }

            inline int s() const { return _s; }
            inline int t() const { return _t; }
            inline int r() const { return _r; }
            inline int m() const { return 0; }
            inline double u() const { return _u; }
            inline double v() const { return _v; }
            inline double x() const { return _x; }
            inline double y() const { return _y; }

            template<typename CALLABLE>
            inline void forEachPixel(CALLABLE&& func)
            {
                forEachPixelOnCenter(func);
            }

            template<typename CALLABLE>
            inline void forEachPixelOnCenter(CALLABLE&& func)
            {
                int tt = _image->t(), ss = _image->s(), rr = _image->r();
                double _bu = 0.5 / (double)ss;
                double _bv = 0.5 / (double)tt;

                for (_t = 0; _t < tt; ++_t)
                {
                    _v = _bv + ((double)_t * 2.0 * _bv);
                    _y = _extent.yMin() + _extent.height() * _v;

                    for (_s = 0; _s < ss; ++_s)
                    {
                        _u = _bu + ((double)_s * 2.0 * _bu);
                        _x = _extent.xMin() + _extent.width() * _u;

                        for (_r = 0; _r < rr; ++_r)
                        {
                            func();
                        }
                    }
                }
            }

        private:
            const osg::Image* _image;
            const EXTENT _extent;
            int    _s, _t, _r;
            double _u, _v;
            double _x, _y;
        };

        template<typename EXTENT>
        class PixelReaderWithExtent : public PixelReader
        {
        public:
            PixelReaderWithExtent(const osg::Image* image, const EXTENT& extent) :
                PixelReader(image), _extent(extent) { }

            void readCoord(osg::Vec4f& value, double x, double y, int r = 0, int m = 0) const {
                PixelReader::operator()(
                    value,
                    (double)(x - _extent.xMin()) / _extent.width(),
                    (double)(y - _extent.yMin()) / _extent.height(),
                    r, m);
            }

            bool readCoordWithoutClamping(osg::Vec4f& value, double x, double y, int r = 0, int m = 0) const {
                double u = (x - _extent.xMin()) / _extent.width();
                double v = (y - _extent.yMin()) / _extent.height();
                if (u < 0.0 || u > 1.0 || v < 0.0 || v > 1.0) return false;
                PixelReader::operator()(value, u, v, r, m);
                return true;
            }

        private:
            const EXTENT _extent;
        };
    };

    /** Visitor that finds and operates on textures and images */
    class OSGEARTH_EXPORT TextureAndImageVisitor : public osg::NodeVisitor
    {
    public:
        using osg::NodeVisitor::apply;

        TextureAndImageVisitor();
        virtual ~TextureAndImageVisitor() { }

    public:
        /** Visits a texture and, by default, all its components images */
        virtual void apply(osg::Texture& texture);

        /** Visits an image inside a texture */
        virtual void apply(osg::Image& image) { }

    public: // osg::NodeVisitor
        virtual void apply(osg::Node& node);
        virtual void apply(osg::StateSet& stateSet);
    };
} }

#endif //OSGEARTH_IMAGEUTILS_H
