/* osgEarth
* Copyright 2025 Pelican Mapping
* MIT License
*/
#ifndef OSGEARTHUTIL_LINEAR_LINE_OF_SIGHT
#define OSGEARTHUTIL_LINEAR_LINE_OF_SIGHT

#include <osgEarth/LineOfSight>
#include <osgEarth/MapNode>
#include <osgEarth/MapNodeObserver>
#include <osgEarth/Terrain>
#include <osgEarth/GeoData>
#include <osgEarth/Draggers>

namespace osgEarth { namespace Contrib
{
    /**
     * A Node that can be used to display point to point line of sight calculations
     */
    class OSGEARTH_EXPORT LinearLineOfSightNode: public LineOfSightNode, public MapNodeObserver
    {
    public:
        /**
         *Constructs and new LinearLineOfSightNode
         *@param mapNode
         *       The MapNode that this LinearLineOfSightNode will be operating on
         */
        LinearLineOfSightNode( osgEarth::MapNode* mapNode );

        virtual ~LinearLineOfSightNode();

        /**
         *Constructs and new LinearLineOfSightNode
         *@param mapNode
         *       The MapNode that this LinearLineOfSightNode will be operating on
         *@param start
         *       The start point
         *@param end
         *       The end point
         */
        LinearLineOfSightNode( 
            osgEarth::MapNode* mapNode, 
            const GeoPoint&    start,
            const GeoPoint&    end );

        /**
         * Get the start point
         */
        //const osg::Vec3d& getStart() const;
        const GeoPoint& getStart() const;

        /**
         * Gets the start point in world coordinates
         */
        const osg::Vec3d& getStartWorld() const;

        /**
         * Set the start point.  The point should be in the Map's coordinate system.  So if you're dealing with a geocentric map
         * the location should be in the form lon, lat, elevation
         */
        void setStart(const GeoPoint& start);

        /**
         * Get the end point
         */
        const GeoPoint& getEnd() const;

        /**
         * Gets the end point in world coordinates
         */
        const osg::Vec3d& getEndWorld() const;

        /**
         * Set the end point.  The point should be in the Map's coordinate system.  So if you're dealing with a geocentric map
         * the location should be in the form lon, lat, elevation
         */
        void setEnd(const GeoPoint& end);

        /**
         * Gets the hit point.  Only valid is getHasLOS is false.
         */
        const GeoPoint& getHit() const;

        /**
         * Gets the hit point in world coordinates
         */
        const osg::Vec3d& getHitWorld() const;

        /**
         * Gets whether not this calculation has line of sight.
         */
        bool getHasLOS() const;

        /**
         * Set the good color
         */
        void setGoodColor( const osg::Vec4f &color );

        /**
         * Gets the good color
         */
        const osg::Vec4f& getGoodColor() const;

        /**
         * Sets the bad color
         */
        void setBadColor( const osg::Vec4f &color );

        /**
         * Gets the bad color
         */
        const osg::Vec4f& getBadColor() const;

        /**
         * Gets the display mode
         */
        LineOfSight::DisplayMode getDisplayMode() const;

        /**
         * Sets the display mode
         */
        void setDisplayMode( LineOfSight::DisplayMode displayMode );

        /**
         * Called when the underlying terrain has changed.
         */
        void terrainChanged( const osgEarth::TileKey& tileKey, osg::Node* terrain );

        void addChangedCallback( LOSChangedCallback* callback );
        void removeChangedCallback( LOSChangedCallback* callback );        

        bool getTerrainOnly() const;

        void setTerrainOnly( bool terrainOnly );

    public: // MapNodeObserver
        
        /**
         * Gets the MapNode that this LineOfSightNode is operating on.
         */
        virtual osgEarth::MapNode* getMapNode() { return _mapNode.get(); }

        virtual void setMapNode( osgEarth::MapNode* mapNode );


    private:
        osg::Node* getNode();
        void compute(osg::Node* node, bool backgroundThread = false);
        void draw(bool backgroundThread = false);
        void subscribeToTerrain();
        osg::observer_ptr< osgEarth::MapNode > _mapNode;
        bool _hasLOS;

        LineOfSight::DisplayMode _displayMode;    
        osg::Vec4 _goodColor;
        osg::Vec4 _badColor;

        GeoPoint _hit;
        GeoPoint _start;
        GeoPoint _end;

        osg::Vec3d _startWorld;
        osg::Vec3d _endWorld;
        osg::Vec3d _hitWorld;

        LOSChangedCallbackList _changedCallbacks;

        osg::ref_ptr < osgEarth::TerrainCallback > _terrainChangedCallback;
        
        bool _clearNeeded;
        bool _terrainOnly;
    };


    /**********************************************************************/


    /**
     * An update callback that allows you to attach a LineOfSightNode to two moving nodes.
     * The update callback will update the start and end points of the LineOfSight calcuation to
     * follow the nodes.
     *
     * Example:
     * LineOfSightNode* los = new LineOfSightNode(myMapNode);
     * los->setUpdateCallback( new LineOfSightTether( startNode, endNode ) );
     */
    class OSGEARTH_EXPORT LineOfSightTether : public osg::NodeCallback
    {
    public:
        LineOfSightTether(osg::Node* startNode, osg::Node* endNode);

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

        virtual void operator()(osg::Node* node, osg::NodeVisitor* nv);  

        osg::Node* startNode() { return _startNode.get(); }
        osg::Node* endNode() { return _endNode.get(); }

    private:
        osg::ref_ptr< osg::Node > _startNode;
        osg::ref_ptr< osg::Node > _endNode;
    };

    /**********************************************************************/

    /**
     * An editor node that allows you to move the start and end points
     * of the LineOfSightNode
     */
    class OSGEARTH_EXPORT LinearLineOfSightEditor : public osg::Group
    {
    public:
        /**
         * Create a new LineOfSightEditor
         * @param los
         *        The LineOfSightNode to edit
         */
        LinearLineOfSightEditor(LinearLineOfSightNode* los);    
        
        virtual ~LinearLineOfSightEditor();    

        /**
         *Updates the position of the draggers to represent the actual location of the LineOfSightNode.
         *This should be called if the los is changed outside of the editor and would probably benefit
         *from the LineOfSightNode having a callback that notifies listeners that the start/end points have changed.
         */
        void updateDraggers();
    private:
        osg::ref_ptr< LinearLineOfSightNode > _los;
        osgEarth::Dragger* _startDragger;
        osgEarth::Dragger* _endDragger;
        osg::ref_ptr< LOSChangedCallback > _callback;
    };

} }

#endif // OSGEARTHUTIL_LINEAR_LINE_OF_SIGHT
