/* -*-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>

 /**
  * GDAL (Geospatial Data Abstraction Library) Layers
  */
class GDALDataset;
class GDALRasterBand;


namespace osgEarth
{
    namespace GDAL
    {
        /**
         * Encapsulates a user-supplied GDALDataset
         */
        class OSGEARTH_EXPORT ExternalDataset : public osg::Referenced // NO EXPORT; header only
        {
        public:
            //! Default constructor
            ExternalDataset() = default;

            //! Init constructor
            ExternalDataset(GDALDataset* dataset, bool ownsDataset) : 
                 _dataset(dataset), _ownsDataset(ownsDataset) {}

        public:
            GDALDataset* dataset() const { return _dataset; };
            void setDataset(GDALDataset* dataset) { _dataset = dataset; };

            bool ownsDataset() const { return _ownsDataset; };
            void setOwnsDataset(bool ownsDataset) { _ownsDataset = ownsDataset; };

        private:
            GDALDataset* _dataset = nullptr;
            bool _ownsDataset = true;
        };

        // GDAL-specific serialization data to be incorpoated by the LayerOptions below
        class OSGEARTH_EXPORT Options
        {
        public:
            Options() { }
            Options(const ConfigOptions& input);
            OE_OPTION(URI, url);
            OE_OPTION(std::string, connection);
            OE_OPTION(unsigned, subDataSet, 0u);
            OE_OPTION(RasterInterpolation, interpolation, INTERP_AVERAGE);
            OE_OPTION(ProfileOptions, warpProfile);
            OE_OPTION(bool, useVRT, false);
            OE_OPTION(bool, coverageUsesPaletteIndex, true);
            OE_OPTION(bool, singleThreaded, false);
            OE_OPTION(ProfileOptions, fallbackProfile);

            void readFrom(const Config& conf);
            void writeTo(Config& conf) const;
        };

        /**
         * Driver for reading raster data using GDAL.
         * It is rarely necessary to use this object directly; use a
         * GDALImageLayer or GDALElevationLayer instead.
         */
        class OSGEARTH_EXPORT Driver
        {
        public:
            using Ptr = std::shared_ptr<Driver>;

            //! Constructs a new driver

            virtual ~Driver();

            //! Value to interpet as "no data"
            void setNoDataValue(float value) { _noDataValue = value; }

            //! Minimum valid data value (anything less is "no data")
            void setMinValidValue(float value) { _minValidValue = value; }

            //! Maximum valid data value (anything more is "no data")
            void setMaxValidValue(float value) { _maxValidValue = value; }

            //! Maximum LOD at which to return real data
            void setMaxDataLevel(unsigned value) { _maxDataLevel = value; }

            //! Assign an external GDAL dataset to use.
            void setExternalDataset(ExternalDataset* value);

            //! Opens and initializes the connection to the dataset
            Status open(
                const std::string& name,
                const GDAL::Options& options,
                unsigned tileSize,
                const Profile* fallback_profile,
                DataExtentList* out_dataExtents,
                const osgDB::Options* readOptions,
                bool verbose);

            //! Creates an image if possible
            osg::Image* createImage(
                const TileKey& key,
                unsigned tileSize,
                bool isCoverage,
                ProgressCallback* progress);

            //! Creates a heightfield if possible
            osg::HeightField* createHeightField(
                const TileKey& key,
                unsigned tileSize,
                ProgressCallback* progress);

            //! Creates a heightfield if possible using a faster path that creates a temporary warped VRT.
            osg::HeightField* createHeightFieldWithVRT(
                const TileKey& key,
                unsigned tileSize,
                ProgressCallback* progress);

            //! Profile of the underlying data source
            const Profile* getProfile() { return _profile.get(); }

        private:
            void pixelToGeo(double, double, double&, double&);
            void geoToPixel(double, double, double&, double&);

            bool isValidValue(float, GDALRasterBand*) const;
            bool isValidValue(float value, float noDataValue) const;
            bool intersects(const TileKey&);
            float getInterpolatedDEMValue(GDALRasterBand* band, double x, double y, bool applyOffset = true);
            float getInterpolatedDEMValueWorkspace(GDALRasterBand* band, double u, double v, float* data, int width, int height);

            optional<float> _noDataValue, _minValidValue, _maxValidValue;
            optional<unsigned> _maxDataLevel = 30;
            GDALDataset* _srcDS = nullptr;
            GDALDataset* _warpedDS = nullptr;
            bool _pixelIsArea = true; // AREA_OR_POINT metadata if available
            double _linearUnits = 1.0;
            double _geotransform[6];
            double _invtransform[6];
            GeoExtent _extents;
            Bounds _bounds;
            osg::ref_ptr<const Profile> _profile;
            GDAL::Options _gdalOptions;
            const GDAL::Options& gdalOptions() const { return _gdalOptions; }
            osg::ref_ptr<GDAL::ExternalDataset> _externalDataset;
            std::string _name;

            const std::string& getName() const { return _name; }
        };

        //! Creates an OSG image from an entire GDAL dataset
        extern OSGEARTH_EXPORT osg::Image* reprojectImage(
            const osg::Image* srcImage,
            const std::string srcWKT,
            double srcMinX, double srcMinY, double srcMaxX, double srcMaxY,
            const std::string destWKT,
            double destMinX, double destMinY, double destMaxX, double destMaxY,
            int width = 0,
            int height = 0,
            bool useBilinearInterpolation = true);


        struct LayerBase
        {
        protected:
            mutable Util::PerThread<GDAL::Driver::Ptr> _driverPerThread;
            mutable std::mutex _singleThreadingMutex;
            mutable GDAL::Driver::Ptr _driverSingleThreaded = nullptr;
            mutable Util::ReadWriteMutex _createCloseMutex;
        };
    }
}



namespace osgEarth
{
    /**
     * Image layer connected to a GDAL raster dataset
     */
    class OSGEARTH_EXPORT GDALImageLayer : public ImageLayer, public GDAL::LayerBase
    {
    public: // serialization
        class OSGEARTH_EXPORT Options : public ImageLayer::Options, public GDAL::Options {
        public:
            META_LayerOptions(osgEarth, Options, ImageLayer::Options);
            virtual Config getConfig() const;
        private:
            void fromConfig(const Config&);
        };

    public:
        META_Layer(osgEarth, GDALImageLayer, Options, ImageLayer, GDALImage);

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

        //! Database connection for GDAL database queries (alternative to URL)
        void setConnection(const std::string& value);
        const std::string& getConnection() const;

        //! GDAL sub-dataset index (optional)
        void setSubDataSet(const unsigned& value);
        const unsigned& getSubDataSet() const;

        //! Interpolation method for resampling (default is bilinear)
        void setInterpolation(const RasterInterpolation& value);
        const RasterInterpolation& getInterpolation() const;

        //! Use a single-threaded driver (default is multi-threaded)
        void setSingleThreaded(bool value);
        bool getSingleThreaded() const;

        //! User-supplied external dataset
        void setExternalDataset(GDAL::ExternalDataset* value);

    public: // Layer

        //! Called by the constructor
        void init() override;

        //! Establishes a connection to the TMS repository
        Status openImplementation() override;

        //! Closes down any GDAL connections
        Status closeImplementation() override;

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


    //! Elevation layer connected to a GDAL facility
    class OSGEARTH_EXPORT GDALElevationLayer : public ElevationLayer, public GDAL::LayerBase
    {
    public: // serialization
        class OSGEARTH_EXPORT Options : public ElevationLayer::Options, public GDAL::Options {
        public:
            META_LayerOptions(osgEarth, Options, ElevationLayer::Options);
            virtual Config getConfig() const;
        private:
            void fromConfig(const Config&);
        };

    public:
        META_Layer(osgEarth, GDALElevationLayer, Options, ElevationLayer, GDALElevation);

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

        //! Database connection for GDAL database queries (alternative to URL)
        void setConnection(const std::string& value);
        const std::string& getConnection() const;

        //! GDAL sub-dataset index (optional)
        void setSubDataSet(const unsigned& value);
        const unsigned& getSubDataSet() const;

        //! Forced profile for reprojection (still need this?)
        void setWarpProfile(const ProfileOptions& value);
        const ProfileOptions& getWarpProfile() const;

        //! Interpolation method for resampling (default is bilinear)
        void setInterpolation(const RasterInterpolation& value);
        const RasterInterpolation& getInterpolation() const;

        //! User-supplied external dataset
        void setExternalDataset(GDAL::ExternalDataset* value);
        GDAL::ExternalDataset* getExtenalDataset() const;

        //! Use the new VRT read approach
        void setUseVRT(const bool& value);
        const bool& getUseVRT() const;

        //! Use a single-threaded driver (default is multi-threaded)
        void setSingleThreaded(bool value);
        bool getSingleThreaded() const;

    public: // Layer

        //! Called by the constructor
        void init() override;

        //! Establishes a connection to the repository
        Status openImplementation() override;

        //! Closes down any GDAL connections
        Status closeImplementation() override;

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

} // namespace osgEarth

OSGEARTH_SPECIALIZE_CONFIG(osgEarth::GDALImageLayer::Options);
OSGEARTH_SPECIALIZE_CONFIG(osgEarth::GDALElevationLayer::Options);
