/* -*-c++-*- */
/* osgEarth - Geospatial SDK for OpenSceneGraph
 * Copyright 2020 Pelican Mapping
 * http://osgearth.org
 *
 * osgEarth is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
 */
#pragma once

#include <osgEarth/Common>
#include <osgEarth/ImageLayer>
#include <osgEarth/ElevationLayer>
#include <osgEarth/URI>
#include <osgEarth/Containers>

/**
 * TMS (TileMapService)
 * https://wiki.osgeo.org/wiki/Tile_Map_Service_Specification
 */

//! TMS namespace contains TMS support classes used to the Layers
namespace osgEarth { namespace TMS
{
    class OSGEARTH_EXPORT TileFormat
    {
    public:
        TileFormat();

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

        /**
        *Gets the width of a tile
        */
        unsigned int getWidth() const {return _width;}

        /**
        *Sets the width of a tile
        */
        void setWidth(unsigned int width) {_width = width;}

        /**
        *Gets the height of a tile
        */
        unsigned int getHeight() const {return _height;}

        /**
        *Sets the height of a tile
        */
        void setHeight(unsigned int height) {_height = height;}

        /**
        *Gets the mime type of a tile
        */
        const std::string& getMimeType() const {return _mimeType;}

        /**
        *Sets the mime type of a tile
        */
        void setMimeType(const std::string& mimeType) {_mimeType = mimeType;}

        /**
        *Gets the extension of a tile
        */
        const std::string& getExtension() const {return _extension;}

        /**
        *Sets the extension of a tile
        */
        void setExtension(const std::string& extension) {_extension = extension;}

    protected:
        unsigned int _width;
        unsigned int _height;
        std::string _mimeType;
        std::string _extension;
    };

    /**
    * TMS tile set.  A collection of tiled images at a given zoom level.
    */
    class OSGEARTH_EXPORT TileSet
    {
    public:
        TileSet();

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

        /**
        *Gets the reference link for this TileSet.
        */
        const std::string& getHref() const {return _href;}

        /**
        *Sets the reference link for this TileSet.
        */
        void setHref( const std::string &href) {_href = href;}

        /**
        *Gets the units per pixel for this TileSet.
        */
        double getUnitsPerPixel() const {return _unitsPerPixel;}

        /**
        *Sets the units per pixel for this TileSet.
        */
        void setUnitsPerPixel(double unitsPerPixel) {_unitsPerPixel = unitsPerPixel;}

        /**
        *Gets the zoom level of this TileSet.
        */
        unsigned int getOrder() const {return _order;}

        /**
        *Sets the zoom level of this TileSet.
        */
        void setOrder( int order ) {_order = order;}

    protected:
        std::string _href;
        double _unitsPerPixel;
        unsigned int _order;
    };

    /**
    *A TMS tile map
    */
    class OSGEARTH_EXPORT TileMap : public osg::Referenced
    {
    public:
        enum ProfileType
        {
            TYPE_UNKNOWN,
            TYPE_GEODETIC,
            TYPE_MERCATOR,
            TYPE_LOCAL
        };

        TileMap();

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

        /**
        *Gets the tile map service for this TileMap
        */
        const std::string& getTileMapService() const {return _tileMapService;}

        /**
        *Sets the tile map service for this TileMap
        */
        void setTileMapService(const std::string& tileMapService) {_tileMapService = tileMapService;}

        /**
        *Gets the version of this TileMap
        */
        const std::string& getVersion() const {return _version;}

        /**
        *Sets the version of this TileMap
        */
        void setVersion(const std::string& version) {_version = version;}

        /**
        *Gets the title of this TileMap
        */
        const std::string& getTitle() const {return _title;}

        /**
        *Sets the title of this TileMap
        */
        void setTitle(const std::string& title) {_title = title;}

        /**
        *Gets the abstract of this TileMap
        */
        const std::string& getAbstract() const {return _abstract;}

        /**
        *Sets the abstract of this TileMap
        */
        void setAbstract(const std::string& value) {_abstract = value;}

        /**
        *Gets the spatial reference of this TileMap
        */
        const std::string& getSRS() const {return _srs;}

        /**
        *Sets the spatial reference of this TileMap
        */
        void setSRS(const std::string& srs) {_srs = srs;}

        /**
        *Gets the vertical spatial reference of this TileMap
        */
        const std::string& getVerticalSRS() const { return _vsrs; }

        /**
        *Sets the vertical spatial reference of this TileMap
        */
        void setVerticalSRS(const std::string& vsrs) { _vsrs = vsrs; }

        /**
        *Gets the filename that this TileMap was loaded from
        */
        const std::string& getFilename() const {return _filename;}

        /**
        *Sets the filename that this TileMap was loaded from
        */
        void setFilename(const std::string& filename) {_filename = filename;}

        /**
        *Gets the minimum zoom level that this TileMap has valid data for
        */
        unsigned int getMinLevel() const {return _minLevel;}

        /**
        *Gets the maximum level that this TileMap has valid data for
        */
        unsigned int getMaxLevel() const {return _maxLevel;}

        /**
        *Computes the minimum and maximum levels of valid data for this TileMap
        */
        void computeMinMaxLevel();

        /**
        *Computes the number of tiles at level 0 from the existing TileSets
        */
        void computeNumTiles();

        /**
        *Gets the x coordinate of the origin of this TileMap
        */
        double getOriginX() const {return _originX;}

        /**
        *Sets the x coordinate of the origin of this TileMap
        */
        void setOriginX(double x) {_originX = x;}

        /**
        *Gets the y coordinate of the origin of this TileMap
        */
        double getOriginY() const {return _originY;}

        /**
        *Sets the y coordinate of the origin of this TileMap
        */
        void setOriginY(double y) {_originY = y;}

        /**
        * Sets the timestamp of the TimeMap, i.e. the last modified time
        */
        void setTimeStamp(TimeStamp t) { _timestamp = t; }

        /**
        * Gets the timestap of the TileMap
        */
        TimeStamp getTimeStamp() const { return _timestamp; }

        /**
        *Sets the origin of this TileMap
        *
        *@param x
        *       The origin's x coordinate
        *@param y
        *       The origin's y coordinate
        */
        void setOrigin(double x, double y);

        /**
        *Gets the extents of this TileMap
        *@param minX
        *       The minimum x coordinate of the extents
        *@param minY
        *       The minimum y coordinate of the extents
        *@param maxX
        *       The maximum x coordinate of the extents
        *@param maxY
        *       The maximum y coordinate of the extents
        */
        void getExtents( double &minX, double &minY, double &maxX, double &maxY) const;

        /**
        *Sets the extents of this TileMap
        *@param minX
        *       The minimum x coordinate of the extents
        *@param minY
        *       The minimum y coordinate of the extents
        *@param maxX
        *       The maximum x coordinate of the extents
        *@param maxY
        *       The maximum y coordinate of the extents
        */
        void setExtents( double minX, double minY, double maxX, double maxY);

        /**
        *Gets the number of tiles wide at lod 0
        */
        unsigned int getNumTilesWide() const { return _numTilesWide; }

        /**
        *Sets the number of tiles wide at lod 0
        */
        void setNumTilesWide(unsigned int w) { _numTilesWide = w; }

        /**
        *Gets the number of tiles high at lod 0
        */
        unsigned int getNumTilesHigh() const { return _numTilesHigh; }

        /**
        *Sets the number of tiles high at lod 0
        */
        void setNumTilesHigh(unsigned int h) {_numTilesHigh = h;}

        ProfileType getProfileType() const {return _profile_type;}
        void setProfileType( ProfileType type ) {_profile_type = type;}

        const Profile* createProfile() const;

        /** Gets the TileFormat for this TileMap */
        TileFormat& getFormat() {return _format;}
        
        /** Gets the TileFormat for this TileMap */
        const TileFormat& getFormat() const {return _format;}


        /** A list of TileSets */
        typedef std::vector<TileSet> TileSetList;

        /** Gets the TileSets for this TileMap */
        TileSetList& getTileSets() {return _tileSets;}

        /** Gets the TileSets for this TileMap */
        const TileSetList& getTileSets() const {return _tileSets;}

        DataExtentList& getDataExtents() { return _dataExtents;}
        const DataExtentList& getDataExtents() const { return _dataExtents;}

       
        /**
        *Gets a URL string that can be used to retrieve the image for the given TileKey
        *@param tileKey
        *       The TileKey to get the URL for.
        *@param invertY
        *       If false, treat tile 0,0 as the lower left tile in the the map.  If true, treat 0,0 as the top left tile in the map.
        *@returns
        *       The URL if the data intersects the TileKey.
        */
        std::string getURL(const osgEarth::TileKey& tileKey, bool invertY);

        /**
        *Determines whether or not the given TileKey intersects the TileMap
        */
        bool intersectsKey(const osgEarth::TileKey& tileKey);

        /**
        *Automatically generates a number of TileSets
        */
        void generateTileSets(unsigned int numLevels = 20);

        /**
        * Creates a TileMap corresponding to one of osgEarth's built-in profile types
        */
        static TileMap* create(
            const std::string& url,
            const Profile* profile,
            const DataExtentList& dataExtents,
            const std::string& format,
            int tile_width,
            int tile_height );

    protected:
        std::string _tileMapService;
        std::string _version;
        std::string _title;
        std::string _abstract;
        std::string _srs;
        std::string _vsrs;


        double _originX, _originY;

        double _minX, _minY, _maxX, _maxY;

        TileSetList _tileSets;

        TileFormat _format;

        std::string _filename;

        unsigned int _minLevel;
        unsigned int _maxLevel;

        unsigned int _numTilesWide;
        unsigned int _numTilesHigh;

        ProfileType _profile_type;

        TimeStamp _timestamp;

        DataExtentList _dataExtents;
    };

    class OSGEARTH_EXPORT TileMapReaderWriter
    {
    public:
        static TileMap* read( const URI& location, const osgDB::ReaderWriter::Options* options );
        static TileMap* read( std::istream &in );
        static TileMap* read( const Config& conf );

        static void write(const TileMap* tileMap, const std::string& location);
        static void write(const TileMap* tileMap, std::ostream& output);

    private:
        TileMapReaderWriter();
        TileMapReaderWriter(const TileMapReaderWriter &tmr);

        /** dtor */
        virtual ~TileMapReaderWriter() { }
    };


    /**
     * An entry in a TileMapService's list of TileMaps.
     */
    struct OSGEARTH_EXPORT TileMapEntry
    {
        TileMapEntry( const std::string& _title, const std::string& _href, const std::string& _srs, const std::string& _profile);

        std::string title;
        std::string href;
        std::string srs;
        std::string profile;
    };

    typedef std::list< TileMapEntry > TileMapEntryList;

    /**
     * Reads a list of TileMapEntry's from a server.  Useful for displaying a list of layers to the user
     */
    class OSGEARTH_EXPORT TileMapServiceReader
    {
    public:
        /**
         * Reads a list of TileMapEntry's from a server
         * @param location
         *    A URL to the TMS service URL like http://server.com/tiles/1.0.0/
         */
        static bool read( const std::string &location, const osgDB::ReaderWriter::Options* options, TileMapEntryList& tileMaps );    
        static bool read( const Config& conf, TileMapEntryList& tileMaps);

    private:
        TileMapServiceReader();
        TileMapServiceReader( const TileMapServiceReader& rhs );
    };

    /**
     * Underlying TMS driver that does the actual TMS I/O
     */
    class OSGEARTH_EXPORT Driver
    {
    public:
        Status open(
            const URI& uri,
            osg::ref_ptr<const Profile>& profile,
            const std::string& format,
            bool isCoverage,
            DataExtentList& out_dataExtents,
            const osgDB::Options* readOptions);

        void close();

        ReadResult read(
            const URI& uri,
            const TileKey& key, 
            bool invertY,
            ProgressCallback* progress,
            const osgDB::Options* readOptions) const;

        bool write(
            const URI& uri,
            const TileKey& key,
            const osg::Image* image,
            bool invertY,
            ProgressCallback* progress,
            const osgDB::Options* writeOptions) const;

    private:
        osg::ref_ptr<TMS::TileMap> _tileMap;
        osg::ref_ptr<osgDB::ReaderWriter> _writer;
        bool _forceRGBWrites;
        bool _isCoverage;

        bool resolveWriter(const std::string& format);
    };

    /**
     * Serializable TMS options
     */
    class OSGEARTH_EXPORT Options
    {
    public:
        OE_OPTION(URI, url);
        OE_OPTION(std::string, tmsType);
        OE_OPTION(std::string, format);
        static Config getMetadata();
        void readFrom(const Config& conf);
        void writeTo(Config&) const;
    };
} }


namespace osgEarth
{
    /**
     * Image layer connected to a TMS (Tile Map Service) facility
     */
    class OSGEARTH_EXPORT TMSImageLayer : public ImageLayer
    {
    public: // serialization
        class OSGEARTH_EXPORT Options : public ImageLayer::Options, public TMS::Options {
        public:
            META_LayerOptions(osgEarth, Options, ImageLayer::Options);
            Config getConfig() const override;
        private:
            void fromConfig(const Config& conf);
        };

    public:
        META_Layer(osgEarth, TMSImageLayer, Options, ImageLayer, TMSImage);

    public:
        //! Base URL for TMS requests
        void setURL(const URI& value);
        const URI& getURL() const;

        //! Options TMS "type"; only possible setting is "google" which will
        //! invert the Y axis for tile indices
        void setTMSType(const std::string& value);
        const std::string& getTMSType() const;

        //! Data format to request from the service
        void setFormat(const std::string& value);
        const std::string& getFormat() const;

    public: // Layer
        
        //! Establishes a connection to the TMS repository
        Status openImplementation() override;
        Status closeImplementation() override;

        //! Creates a raster image for the given tile key
        GeoImage createImageImplementation(const TileKey& key, ProgressCallback* progress) const override;

        //! Writes a raster image for he given tile key (if open for writing)
        Status writeImageImplementation(const TileKey& key, const osg::Image* image, ProgressCallback* progress) const override;

    protected: // Layer

        //! Called by constructors
        void init() override;

        bool isWritingSupported() const override { return true; }

    protected:

        //! Destructor
        virtual ~TMSImageLayer() { }

    private:
        TMS::Driver _driver;
        mutable ReadWriteMutex _mutex;
    };


    /**
     * Elevation layer connected to a TMS (Tile Map Service) facility
     */
    class OSGEARTH_EXPORT TMSElevationLayer : public ElevationLayer
    {
    public: // serialization
        class OSGEARTH_EXPORT Options : public ElevationLayer::Options, public TMS::Options {
        public:
            META_LayerOptions(osgEarth, Options, ElevationLayer::Options);
            OE_OPTION(std::string, elevationEncoding, {});
            OE_OPTION(bool, stitchEdges, false);
            OE_OPTION(RasterInterpolation, interpolation, INTERP_BILINEAR);
            Config getConfig() const override;
        private:
            void fromConfig(const Config& conf);
        };

    public:
        META_Layer(osgEarth, TMSElevationLayer, Options, ElevationLayer, TMSElevation);

        //! Base URL for TMS requests
        void setURL(const URI& value);
        const URI& getURL() const;

        //! Options TMS "type"; only possible setting is "google" which will
        //! invert the Y axis for tile indices
        void setTMSType(const std::string& value);
        const std::string& getTMSType() const;

        //! Data format to request from the service
        void setFormat(const std::string& value);
        const std::string& getFormat() const;

        //! Encoding encoding type
        //! @param value Encoding type, one of "" (default), "mapbox", "terrarium"
        void setElevationEncoding(const std::string& value);
        const std::string& getElevationEncoding() const;

        //! Whether to stitch the edges of abutting tiles together to prevent gaps.
        //! Some elevation data is image based, and the side of one tile does not match
        //! the value on the correspond side of the abutting tile. This option will
        //! resample the data to make the edges match, for a small performance hit.
        //! @param value True to stitch edges (default is false)
        void setStitchEdges(const bool& value);
        const bool& getStitchEdges() const;

        //! Interpolation method to use when stitching edges = true.
        //! Default is INTERP_BILINEAR.
        void setInterpolation(const RasterInterpolation& value);
        const RasterInterpolation& getInterpolation() const;

    public: // Layer
        
        //! Establishes a connection to the TMS repository
        Status openImplementation() override;
        Status closeImplementation() override;

        //! Creates a heightfield for the given tile key
        GeoHeightField createHeightFieldImplementation(const TileKey& key, ProgressCallback* progress) const override;

        //! Writes a raster image for he given tile key (if open for writing)
        Status writeHeightFieldImplementation(const TileKey& key, const osg::HeightField* hf, ProgressCallback* progress) const override;

    protected: // Layer

        //! Called by constructors
        void init() override;

        bool isWritingSupported() const override { return true; }

    protected:

        //! Destructor
        virtual ~TMSElevationLayer() { }

    private:
        osg::ref_ptr<TMSImageLayer> _imageLayer;
        mutable Util::LRUCache<TileKey, GeoImage> _stitchingCache{ true, 64 };
    };

} // namespace osgEarth

OSGEARTH_SPECIALIZE_CONFIG(osgEarth::TMSImageLayer::Options);
OSGEARTH_SPECIALIZE_CONFIG(osgEarth::TMSElevationLayer::Options);
