/* osgEarth
 * Copyright 2008-2016 Pelican Mapping
 * MIT License
 */
#pragma once

#include <osgEarth/Common>
#include <osgEarth/Elevation>
#include <osgEarth/ElevationLayer>
#include <osgEarth/GeoData>
#include <osgEarth/TileKey>
#include <osgEarth/Threading>
#include <osgEarth/Containers>
#include <unordered_map>

namespace osgEarth
{
    /**
     * Stores pointers to elevation data wherever it might exist
     * so we can quickly access it for queries.
     */
    class OSGEARTH_EXPORT ElevationPool : public osg::Referenced
    {
    public:
        using WeakPointer = osg::observer_ptr<ElevationTile>;
        using Pointer = osg::ref_ptr<ElevationTile>;
        using WeakLUT = std::unordered_map<Internal::RevElevationKey, WeakPointer>;

    public:

        //! User data that a client can use to speed up queries in
        //! a local geographic area or sample a custom set of layers.
        class OSGEARTH_EXPORT WorkingSet
        {
        public:
            //! Construct a new local working set cache.
            //! @param size Cache size
            WorkingSet(unsigned size =32u);

            //! Assign a specific set of elevation layer to use
            //! for sampling. Usually this is unnecessary as the Pool
            //! will synchronize with the map set by setMap, but you
            //! may want to use a custom Pool with a specific subset
            //! of query layers.
            //! @param layers Set of elevation layers to use
            void setElevationLayers(const ElevationLayerVector& layers) {
                _elevationLayers = layers;
            }

            //! Invalidate the cache.
            void clear();

        private:
            LRUCache<Internal::RevElevationKey, Pointer> _lru;
            ElevationLayerVector _elevationLayers;
            friend class ElevationPool;
        };

    private:
        struct MapData {
            osg::observer_ptr<const Map> map;
            ElevationLayerVector layers;
            size_t hash;
            int mapRevision;
            osg::ref_ptr<const Profile> mapProfile;
            osg::ref_ptr<const Profile> mapProfileNoVDatum;
            RasterInterpolation interpolation;
            std::map<const ElevationLayer*, void*> index;
        };

        MapData snapshotMapData(WorkingSet* ws);

    public:
        /**
         * Object for doing lots of queries in a localized area at a specific LOD.
         * Call prepareEnvelope to initialize one.
         */
        class OSGEARTH_EXPORT Envelope
        {
        public:

            //! For each point in an array of points, sample the elevation and store
            //! the result in the Z coordinate. Input points must be in the map's SRS.
            //! @param begin Iterator pointing to beginning of point array
            //! @param end Iterator pointing to end of point array
            //! @param progress Optional progress callback (can be nullptr)
            //! @param failValue Value to store in Z if the sampling fails
            //! @return Number of valid elevations sampled, or -1 if there was an error
            int sampleMapCoords(
                std::vector<osg::Vec3d>::iterator begin,
                std::vector<osg::Vec3d>::iterator end,
                ProgressCallback* progress,
                float failValue = NO_DATA_VALUE);

        public:
            // internal
            using QuickCache = vector_map<
                Internal::RevElevationKey,
                osg::ref_ptr<ElevationTile>>;

            //! Calculated tile LOD based on resolution
            int getLOD() const {
                return _lod;
            }

        private:
            MapData _mapDataSnapshot;
            Internal::RevElevationKey _key;
            QuickCache _cache;
            double _pw, _ph, _pxmin, _pymin;
            osg::ref_ptr<ElevationTile> _raster;
            int _lod;
            unsigned _tw, _th;
            WorkingSet* _ws;
            WorkingSet _default_ws;
            osg::ref_ptr<const Map> _map;
            osg::ref_ptr<const Profile> _profile;
            ElevationPool* _pool;

            friend class ElevationPool;
        };

    public:
        //! Construct the elevation pool
        ElevationPool();

        //! Assign map to the pool. Required.
        void setMap(const Map* map);

        //! Sample the map's elevation at a point.
        //! @param p  Point at which to sample
        //! @param ws Optional working set (can be NULL)
        //! @param progress Optional progress callback
        ElevationSample getSample(
            const GeoPoint& p,
            WorkingSet* ws,
            ProgressCallback* progress =nullptr);

        //! Sample the map's elevation at a point.
        //! @param p Point at which to sample
        //! @param resolution Resolution at which to attempt to sample (in point srs)
        //! @param ws Optional working set (can be NULL)
        //! @param progress Optional progress callback
        ElevationSample getSample(
            const GeoPoint& p,
            const Distance& resolution,
            WorkingSet* ws,
            ProgressCallback* progress =nullptr);

        //! Extract a complete tile of elevation data
        //! @param key TileKey or data to extact
        //! @param acceptLowerRes Return a lower resolution tile if the requested one isn't available
        //! @param out_elev Output elevation texture tile
        //! @param ws Optional working set (can be nullptr)
        //! @param progress Optional progress callback (can be nullptr)
        //! @return true upon success, false upon failure
        bool getTile(
            const TileKey& key,
            bool acceptLowerRes,
            osg::ref_ptr<ElevationTile>& out_elev,
            WorkingSet* ws,
            ProgressCallback* progress);

        //! For each point in an array of points, sample the elevation and store
        //! the result in the Z coordinate. Input points must be in the map's SRS,
        //! and the sampling resolution is taken from the W coordinate.
        //! @param begin Iterator pointing to beginning of point array
        //! @param end Iterator pointing to end of point array
        //! @param ws Optional working set (local cache, can be nullptr)
        //! @param progress Optional progress callback (can be nullptr)
        //! @param failValue Value to store in Z if the sampling fails
        //! @return Number of valid elevations sampled, or -1 if there was an error
        int sampleMapCoords(
            std::vector<osg::Vec4d>::iterator begin,
            std::vector<osg::Vec4d>::iterator end,
            WorkingSet* ws,
            ProgressCallback* progress,
            float failValue =NO_DATA_VALUE);
        
        //! For each point in an array of points, sample the elevation and store
        //! the result in the Z coordinate. Input points must be in the map's SRS.
        //! @param begin Iterator pointing to beginning of point array
        //! @param end Iterator pointing to end of point array
        //! @param resolution Resolution at which to sample the points
        //! @param ws Optional working set (local cache, can be nullptr)
        //! @param progress Optional progress callback (can be nullptr)
        //! @param failValue Value to store in Z if the sampling fails
        //! @return Number of valid elevations sampled, or -1 if there was an error
        int sampleMapCoords(
            std::vector<osg::Vec3d>::iterator begin,
            std::vector<osg::Vec3d>::iterator end,
            const Distance& resolution,
            WorkingSet* ws,
            ProgressCallback* progress,
            float failValue = NO_DATA_VALUE);

        //! Creates an envelope for sampling lots of points in a localized region
        //! @param out Created envelope (output)
        //! @param refPoint Reference point near which you intend to sample points
        //! @param resolution Resolution at which to intend to sample points
        //! @param ws Optional working set (can be nullptr)
        bool prepareEnvelope(
            Envelope& out,
            const GeoPoint& refPoint,
            const Distance& resolution,
            WorkingSet* ws =nullptr);

        //! The SRS of the map; you can get this to pre-transform points
        //! before a call to sampleMapCoords.
        const SpatialReference* getMapSRS() const;

    protected:
        //! Destructor
        virtual ~ElevationPool();

    private:

        // stores weak pointers to elevation textures wherever they may exist
        // elsewhere in the system, including the local L2 LRU.
        WeakLUT _globalLUT;
        Threading::ReadWriteMutex _globalLUTMutex;

        // LRU container that stores the last N strong references to accessed tiles.
        // Not used directly - just used to hold ref_ptrs to things so they stay
        // alive in the global LUT (see above).
        mutable LRUCache<Internal::RevElevationKey, Pointer> _L2;

        // elevation tile size
        unsigned _tileSize;

        // current map data reflection
        MapData _mapData;
        Threading::ReadWriteMutex _mapDataMutex;

        // internal - get a sample at a point
        ElevationSample getSample(
            const GeoPoint& p,
            unsigned maxLOD,
            WorkingSet* ws,
            ProgressCallback* progress);        

        //! Best LOD this a point for the working set, or -1 if no data in index
        int getLOD(double x, double y, WorkingSet*, int maxLOD);

        osg::ref_ptr<ElevationTile> getOrCreateRaster(
            MapData& snapshot,
            const Internal::RevElevationKey& key,
            bool acceptLowerRes,
            ProgressCallback* progress);

        bool findExistingRaster(
            const Internal::RevElevationKey& key,
            osg::ref_ptr<ElevationTile>& result,
            bool* fromGlobalWeakLUT);
    };

    // backwards compat
    using ElevationTexture = ElevationTile;

    /**
    * Utility to run elevation queries in the background.
    */
    class OSGEARTH_EXPORT AsyncElevationSampler
    {
    public:
        //! Construct a new sampler
        //! @param map Map the sampler will use to sample elevation data
        //! @param threads Number of threads the sampler should use
        AsyncElevationSampler(
            const Map* map,
            unsigned threads =0u);

        //! Destructor
        virtual ~AsyncElevationSampler() { }

        //! Sample elevation at a point at highest available resolution
        //! @param p Point at which to sample terrain elevation
        //! @return Future result of the sample
        jobs::future<ElevationSample> getSample(
            const GeoPoint& p);

        //! Sample elevation at a point and a target resolution
        //! @param p Point at which to sample terrain elevation
        //! @param resolution Resolution at which to sample terrain
        //! @return Future result of the sample
        jobs::future<ElevationSample> getSample(
            const GeoPoint& p,
            const Distance& resolution);

    protected:
        osg::observer_ptr<const Map> _map;
        ElevationPool::WorkingSet _ws;
        jobs::jobpool* _arena;
    };
} // namespace
