/* -*-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_TERRAIN_H
#define OSGEARTH_TERRAIN_H 1

#include <osgEarth/Common>
#include <osgEarth/TileKey>
#include <osgEarth/Threading>
#include <osg/OperationThread>
#include <osg/View>

namespace osgEarth
{
    class Terrain;
    class SpatialReference;

    /**
     * This object is passed to terrain callbacks to provide context information
     * and a feedback interface to terrain callback consumers.
     */
    class TerrainCallbackContext
    {
    public:
        const Terrain* getTerrain() const { return _terrain; }

        /** Indicate that the callback should be removed immediately */
        void remove() { _remove = true; }

        /** whether the user called remove(). */
        bool markedForRemoval() const { return _remove; }

        
    public:
        TerrainCallbackContext(Terrain* terrain)
            : _remove(false), _terrain(terrain) { }

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

    protected:
        bool _remove;
        Terrain* _terrain;
        friend class Terrain;
    };


    /**
     * Callback that you can register with the Terrain in order to receive
     * update messaged about the terrain scene graph.
     */
    class TerrainCallback : public osg::Referenced
    {
    public:
        /**
         * A tile was added to the terrain graph.
         * @param key
         *      Tile key of the new tile, including the geographic extents
         * @param graph
         *      Scene graph that can be used to intersect the tile (though it
         *      may include more than just the new tile)
         * @param context
         *      Contextual information about the callback
         */
        virtual void onTileUpdate(
            const TileKey&          key, 
            osg::Node*              graph, 
            TerrainCallbackContext& context)
        {
            // back compat only
            onTileAdded(key, graph, context);
        }

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

    public: // deprecated - exists for backwards compatibilty only

        virtual void onTileAdded(
            const TileKey&          key, 
            osg::Node*              graph, 
            TerrainCallbackContext& context) { }
    };


    /**
     * Convenience adapter for hooking a callback into a class. The
     * class must be derived from osg::Referenced. When the class
     * destructs, the callback will automatically remove itself from
     * the Terrain object.
     */
    template<typename T>
    class TerrainCallbackAdapter : public TerrainCallback
    {
    public:
        TerrainCallbackAdapter(T* t) : _t(t) { }
        void onTileUpdate(const TileKey&          key, 
                         osg::Node*              tile, 
                         TerrainCallbackContext& context)
        {
            osg::ref_ptr<T> t;
            if (_t.lock(t))
                t->onTileUpdate(key, tile, context);
            else
                context.remove();
        }
    protected:
        virtual ~TerrainCallbackAdapter() { }
        osg::observer_ptr<T> _t;
    };


    /**
     * Interface for an object that can resolve the terrain elevation
     * at a given map location.
     */
    class /*interface-only*/ TerrainResolver
    {
    public:
        virtual bool getHeight(
            const SpatialReference* srs,
            double                  x,
            double                  y,
            double*                 out_heightAboveMSL,
            double*                 out_heightAboveEllipsoid =0L) const =0;

        virtual bool getHeight(
            osg::Node*              patch,
            const SpatialReference* srs,
            double                  x,
            double                  y,
            double*                 out_heightAboveMSL,
            double*                 out_heightAboveEllipsoid =0L) const =0;

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

    // backwards compatibility
    typedef TerrainResolver TerrainHeightProvider;


    /**
     * Services for interacting with the live terrain graph. This differs from
     * the Map model; Map represents the parametric data backing the terrain, 
     * while Terrain represents the actual geometry in memory.
     *
     * All returned map coordinate values are in the units conveyed in the
     * spatial reference at getSRS().
     */
    class OSGEARTH_EXPORT Terrain : public osg::Referenced, public TerrainResolver
    {
    public:
        /**
         * Gets the profile of the map with which this terrain is associated.
         */
        const Profile* getProfile() const { return _profile.get(); }

        /**
         * Gets the spatial reference of the map with which this terrain is
         * associated.
         */
        const SpatialReference* getSRS() const { return _profile->getSRS(); }


    public: // TerrainResolver interface

        /**
         * Intersects the terrain at the location x, y and returns the height data.
         *
         * @param srs
         *      Spatial reference system of (x,y) coordinates
         * @param x, y
         *      Coordinates at which to query the height
         * @param out_heightAboveMSL
         *      The resulting relative height goes here. The height is relative to MSL
         *      (mean sea level) as expressed by the map's vertical datum.
         * @param out_heightAboveEllipsoid
         *      The resulting geodetic height goes here. The height is relative to the
         *      geodetic ellipsoid expressed by the map's SRS.
         */
        bool getHeight(
            const SpatialReference* srs,
            double                  x,
            double                  y,
            double*                 out_heightAboveMSL,
            double*                 out_heightAboveEllipsoid =0L) const;

        /**
         * Save as above, but specify a subgraph patch.
         */
        bool getHeight(
            osg::Node*              patch,
            const SpatialReference* srs,
            double                  x,
            double                  y,
            double*                 out_heightAboveMSL,
            double*                 out_heightAboveEllipsoid =0L) const;

    public:

        /**
         * Returns the world coordinates under the mouse.
         * @param view
         *      View in which to do the query
         * @param mx, my
         *      Mouse coordinates
         * @param out_coords 
         *      Stores the world coordinates under the mouse (when returning true)
         */
        bool getWorldCoordsUnderMouse(
            osg::View*  view,
            float       mx,
            float       my,
            osg::Vec3d& out_world ) const;

    public:
        /**
         * Adds a terrain callback.
         *
         * @param callback
         *      Terrain callback to add. This will get called whenever tile data changes in
         *      the active terrain graph
         */
        void addTerrainCallback(TerrainCallback* callback);

        /**
         * Removes a terrain callback.
         */
        void removeTerrainCallback(TerrainCallback* callback );
        

    public:

        /**
         * Accept a node visitor on the terrain's scene graph.
         */
        void accept( osg::NodeVisitor& nv );

        // access the raw terrain graph
        osg::Node* getGraph() const { return _graph.get(); }
        
        // queues the onTileUpdate callback (internal)
        void notifyTileUpdate( const TileKey& key, osg::Node* tile );

        // queues the onTileRemoved callback (internal)
        void notifyTilesRemoved(const std::vector<TileKey>& keys);

        // internal
        void notifyMapElevationChanged();

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

    private:
        //! Construct the Terrain graph interface
        Terrain(osg::Node* graph, const Profile* profile);

        /** update traversal. */
        void update();

        friend class TerrainEngineNode;

        typedef std::list< osg::ref_ptr<TerrainCallback> > CallbackList;

        CallbackList                 _callbacks;
        Threading::ReadWriteMutex    _callbacksMutex;
        std::atomic_int              _callbacksSize; // separate size tracker for MT size check w/o a lock

        osg::ref_ptr<const Profile>  _profile;
        osg::observer_ptr<osg::Node> _graph;

        osg::ref_ptr<osg::OperationQueue> _updateQueue;
        
        void fireMapElevationChanged();
        void fireTileUpdate( const TileKey& key, osg::Node* tile );
        void fireTilesRemoved(const std::vector<TileKey>& keys);

        struct onTileUpdateOperation : public osg::Operation {
            osg::observer_ptr<Terrain> _terrain;
            TileKey _key;
            osg::observer_ptr<osg::Node> _node;
            unsigned _count;
            onTileUpdateOperation(const TileKey& key, osg::Node* node, Terrain* terrain);
            void operator()(osg::Object*);
            int _delay;
        };
    };

}

#endif // OSGEARTH_COMPOSITING_H
