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

#ifndef OSGEARTH_MAP_H
#define OSGEARTH_MAP_H 1

#include <osgEarth/Common>
#include <osgEarth/GeoData>
#include <osgEarth/Profile>
#include <osgEarth/MapCallback>
#include <osgEarth/Revisioning>
#include <osgEarth/Threading>
#include <osgEarth/ElevationPool>
#include <osgEarth/Cache>
#include <osgDB/Options>
#include <functional>

namespace osgEarth
{
    /**
     * Map is the main data model. A Map hold a collection of
     * Layers, each of which provides data of some kind to the
     * overall Map. Use a MapNode to render the contents of a
     * Map.
     */
    class OSGEARTH_EXPORT Map : public osg::Object
    {
    public:
        META_Object(osgEarth, Map);

        //! Construct a new, empty map.
        Map();

        //! Construct a new, empty map with custom read options.
        Map(const osgDB::Options* readOptions);

        //! This Map's unique ID
        UID getUID() const { return _uid; }

        //! The map's master tiling profile, which defines its SRS and tiling structure
        void setProfile(const Profile* value);
        const Profile* getProfile() const;

        //! Spatial reference system of the map's profile (convenience)
        const SpatialReference* getSRS() const;

        //! SRS of the world (scene)
        const SpatialReference* getWorldSRS() const;

        //! Elevation data interpolation method
        void setElevationInterpolation(const RasterInterpolation& value);
        const RasterInterpolation& getElevationInterpolation() const;

        //! Adds a Layer to the map.
        void addLayer(Layer* layer);

        //! Adds a collection of layers to the map.
        void addLayers(const LayerVector& layers);

        //! Inserts a Layer at a specific index in the Map.
        void insertLayer(Layer* layer, unsigned index);

        //! Removes a layer from the map.
        void removeLayer(Layer* layer);

        //! Moves a layer to another position in the Map.
        void moveLayer(Layer* layer, unsigned index);

        //! Index of the specified layer, or returns getNumLayers() if the layer is not found.
        unsigned getIndexOfLayer(const Layer* layer) const;

        //! Copies references of the map layers into the output list.
        //! This method is thread safe. It returns the map revision that was
        //! in effect when the data was copied.
        Revision getLayers(LayerVector& out_layers) const;

        //! Copies references of the map layers into the output list for which
        //! the predicate function returns true.
        //! This method is thread safe. It returns the map revision that was
        //! in effect when the data was copied.
        inline Revision getLayers(
            LayerVector& out_layers,
            const std::function<bool(const Layer*)>& predicate) const;

        //! Number of layers in the map.
        unsigned getNumLayers() const;

        //! Gets a layer by name.
        Layer* getLayerByName(const std::string& name) const;
        template<typename T> T* getLayerByName(const std::string& name) const;

        //! Gets an image layer by its unique ID.
        Layer* getLayerByUID(UID layerUID) const;
        template<typename T> T* getLayerByUID(UID layerUID) const;

        //! Gets the layer at the specified index.
        Layer* getLayerAt(unsigned index) const;
        template<typename T> T* getLayerAt(unsigned index) const;

        //! Fills the vector with references to all layers of the specified type
        //! and returns the corresponding revision number.
        template<typename T>
        Revision getLayers(std::vector< osg::ref_ptr<T> >& output) const;

        //! Fills the vector with references to all layers of the specified type
        //! and returns the corresponding revision number.
        template<typename T>
        Revision getOpenLayers(std::vector< osg::ref_ptr<T> >& output) const;

        //! Fills the vector with references to all layers satisflying the predicate
        //! and returns the corresponding revision number.
        template<typename T>
        Revision getLayers(
            std::vector< osg::ref_ptr<T> >& output,
            const std::function<bool(const Layer*)>& predicate) const;

        //! Fills the vector with references to all layers of the specified type.
        //! and returns the corresponding revision number.
        template<typename T>
        Revision getLayers(osg::MixinVector< osg::ref_ptr<T> >& output) const;

        //! Fills the vector with references to all layers satisflying the predicate
        //! and returns the corresponding revision number.
        template<typename T>
        Revision getLayers(
            osg::MixinVector<osg::ref_ptr<T>>& output,
            const std::function<bool(const Layer*)>& predicate) const;

        //! Fills the vector with references to all layers of the specified type.
        //! and returns the corresponding revision number.
        template<typename T>
        Revision getOpenLayers(osg::MixinVector< osg::ref_ptr<T> >& output) const;

        //! The first layer of the specified type. This is useful when you
        //! know there in only one layer of the type you are looking for.
        template<typename T> T* getLayer() const;

        //! Adds a map layer callback to this map. This will be notified whenever layers are
        //! added, removed, or re-ordered.
        MapCallback* addMapCallback(MapCallback* callback) const;

        //! Removes a callback previously added with addMapCallback.
        void removeMapCallback(MapCallback* callback) const;

        //! Begins a batch-update operation. Call this if you intend to add multiple
        //! layers at once; then call endUpdate() to complete the operation.
        //! Between calls to beginUpdate() and endUpdate(), Map will not invoke
        //! any callbacks you added wtih addMapCallback.
        void beginUpdate();

        //! Completes a batch update operation that started with a call to
        //! beginUpdate(). Fires all callbacks for operations that occurred
        //! since the call to beginUpdate().
        void endUpdate();

        //! Removes all layers from this map.
        void clear();

        //! Sets the readable name of this map.
        void setMapName( const std::string& name );
        const std::string& getMapName() const { return _name; }

        //! Cache for this Map. Set to NULL for no cache.
        //! Only call this during intiailization.
        void setCache( Cache* cache );
        Cache* getCache() const;

        //! Caching policy (now the map should use the cache)
        void setCachePolicy(const CachePolicy& value);
        const CachePolicy& getCachePolicy() const;

        //! Gets the revision # of the map. The revision # changes every time
        //! you add, remove, or move layers. You can use this to track changes
        //! in the map model (as a alternative to installing a MapCallback).
        Revision getDataModelRevision() const;

        //! Gets the database options associated with this map.
        const osgDB::Options* getReadOptions() const { return _readOptions.get(); }

        //! Gets a version of the map profile without any vertical datum
        const Profile* getProfileNoVDatum() const { return _profileNoVDatum.get(); }

        //! Access to an elevation sampling service tied to this map
        ElevationPool* getElevationPool() const;

        //! Returns true if data for the given tile key and layers is likely
        //! to generate data quickly. This is used for pager thread pool shunting.
        //! Really just a best guess based on available information.
        bool isFast(const TileKey& key, const LayerVector& layers) const;

        //! List of attribution strings to be displayed by the application
        void getAttributions(StringSet& attributions) const;

        //! Number of layers marked as terrain pathes
        int getNumTerrainPatchLayers() const;

    protected:

        virtual ~Map();

        // disables copy construction
        Map(const Map& rhs, const osg::CopyOp& copy) { }

    public: // serialization data
        class OSGEARTH_EXPORT Options : public ConfigOptions {
        public:
            META_ConfigOptions(osgEarth, Options, ConfigOptions);
            OE_OPTION(std::string, name);
            OE_OPTION(ProfileOptions, profile);
            OE_OPTION(CacheOptions, cache);
            OE_OPTION(CachePolicy, cachePolicy);
            OE_OPTION(RasterInterpolation, elevationInterpolation);
            OE_OPTION(std::string, profileLayer);
            virtual Config getConfig() const;
        private:
            void fromConfig(const Config&);
        };

        //! Internal - create from serialized options
        Map(
            const Map::Options& options,
            const osgDB::Options* readOptions);

        //! Ready-only access to the serialization options for this map
        const Options& options() const { return _optionsConcrete; }

    private:
        UID _uid;
        std::string _name;
        LayerVector _layers;
        mutable MapCallbackList _mapCallbacks;
        mutable Threading::ReadWriteMutex _mapDataMutex;
        osg::ref_ptr<const Profile> _profile;
        osg::ref_ptr<const Profile> _profileNoVDatum;
        Revision _dataModelRevision;
        osg::ref_ptr<osgDB::Options> _readOptions;
        osg::ref_ptr<ElevationPool> _elevationPool;
        osg::ref_ptr<CacheSettings> _cacheSettings;
        int _numTerrainPatchLayers;

        struct LayerCB : public LayerCallback {
            LayerCB(Map*);
            osg::observer_ptr<Map> _map;
            void onOpen(Layer* layer);
            void onClose(Layer* layer);
        };
        osg::ref_ptr<LayerCallback> _layerCB;
        friend struct LayerCB;
        void notifyOnLayerOpenOrClose(Layer*);

        void installLayerCallbacks(Layer*);
        void uninstallLayerCallbacks(Layer*);

        void init();
        friend class MapInfo;
        Options _optionsConcrete;
        Options& options() { return _optionsConcrete; }
    };




    // Templated inline methods

    template<typename T> T* Map::getLayerByName(const std::string& name) const {
        return dynamic_cast<T*>(getLayerByName(name));
    }

    template<typename T> T* Map::getLayerByUID(UID layerUID) const {
        return dynamic_cast<T*>(getLayerByUID(layerUID));
    }

    template<typename T> T* Map::getLayerAt(unsigned index) const {
        return dynamic_cast<T*>(getLayerAt(index));
    }

    template<typename T>
    Revision Map::getLayers(std::vector<osg::ref_ptr<T>>& output) const {
        Threading::ScopedReadLock lock(_mapDataMutex);
        for (auto& i : _layers) {
            T* obj = dynamic_cast<T*>(i.get());
            if (obj) output.push_back(obj);
        }
        return _dataModelRevision;
    }

    template<typename T>
    Revision Map::getLayers(std::vector<osg::ref_ptr<T>>& output, const std::function<bool(const Layer*)>& predicate) const {
        Threading::ScopedReadLock lock(_mapDataMutex);
        for (auto& i : _layers) {
            T* obj = dynamic_cast<T*>(i.get());
            if (obj != nullptr && predicate(obj))
                output.push_back(obj);
        }
        return _dataModelRevision;
    }

    template<typename T>
    Revision Map::getOpenLayers(std::vector<osg::ref_ptr<T>>& output) const {
        Threading::ScopedReadLock lock(_mapDataMutex);
        for (auto& i : _layers) {
            if (i->isOpen()) {
                T* obj = dynamic_cast<T*>(i.get());
                if (obj) output.push_back(obj);
            }
        }
        return _dataModelRevision;
    }

    template<typename T>
    Revision Map::getLayers(osg::MixinVector<osg::ref_ptr<T>>& output) const {
        Threading::ScopedReadLock lock(_mapDataMutex);
        for (auto& i : _layers) {
            T* obj = dynamic_cast<T*>(i.get());
            if (obj) output.push_back(obj);
        }
        return _dataModelRevision;
    }

    template<typename T>
    Revision Map::getLayers(osg::MixinVector<osg::ref_ptr<T>>& output, const std::function<bool(const Layer*)>& predicate) const {
        Threading::ScopedReadLock lock(_mapDataMutex);
        for (auto& i : _layers) {
            T* obj = dynamic_cast<T*>(i.get());
            if (obj != nullptr && predicate(obj))
                output.push_back(obj);
        }
        return _dataModelRevision;
    }

    template<typename T>
    Revision Map::getOpenLayers(osg::MixinVector<osg::ref_ptr<T>>& output) const {
        Threading::ScopedReadLock lock(_mapDataMutex);
        for (auto& i : _layers) {
            if (i->isOpen()) {
                T* obj = dynamic_cast<T*>(i.get());
                if (obj) output.push_back(obj);
            }
        }
        return _dataModelRevision;
    }

    template<typename T> T* Map::getLayer() const {
        Threading::ScopedReadLock lock(_mapDataMutex);
        for (auto& i : _layers) {
            T* obj = dynamic_cast<T*>(i.get());
            if (obj) return obj;
        }
        return 0L;
    }

    Revision Map::getLayers(LayerVector& output, const std::function<bool(const Layer*)>& predicate) const {
        Threading::ScopedReadLock lock(_mapDataMutex);
        for (auto& i : _layers) {
            if (predicate(i.get()))
                output.push_back(i.get());
        }
        return _dataModelRevision;
    }
}

#endif // OSGEARTH_MAP_H
