/* -*-c++-*- */
/* osgEarth - Geospatial SDK for OpenSceneGraph
 * Copyright 2019 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_HORIZON_H
#define OSGEARTH_HORIZON_H 1

#include <osgEarth/Common>
#include <osgEarth/SpatialReference>
#include <osg/NodeCallback>
#include <osg/Vec3d>
#include <osg/Shape>
#include <osg/observer_ptr>
#include <osg/Version>
#include <osg/ValueObject>
#include <osg/MatrixTransform>

namespace osgEarth
{
    /**
     * Horizon operations (for a geocentric map).
     */
    class OSGEARTH_EXPORT Horizon : public osg::Object
    {
    public:
        META_Object(osgEarth, Horizon);

        // Retrieve a Horizon object from the NodeVisitor. Returns NULL
        // if this operation is not supported.
        static Horizon* get(osg::NodeVisitor& nv);

        // Stores this object in the NodeVisitor. Returns false if the
        // operation is not supported.
        bool put(osg::NodeVisitor& nv);

    public:
        /** Construct a horizon using a default WGS84 ellipsoid model. */
        Horizon();

        /** Construct a horizon providing the ellipsoid model. */
        Horizon(const osg::EllipsoidModel& ellipsoid);

        /** Construct a horizon from a spatial reference. */
        Horizon(const SpatialReference* srs);

        /** Copy */
        Horizon(const Horizon& rhs, const osg::CopyOp& op = osg::CopyOp::DEEP_COPY_ALL);

        /**
         * Ellipsoid model to use for occlusion testing
         */
        void setEllipsoid(const osg::EllipsoidModel& ellipsoid);

        /**
         * Set the minimum allowable height-above-ellipsoid to consider when doing
         * horizon visibility testing.
         */
        void setMinHAE(double meters);
        double getMinHAE() const { return _minHAE; }

        /**
         * Sets the eye position to use when testing for occlusion.
         * Returns true if the value changed; false if it was the same
         */
        bool setEye(const osg::Vec3d& eyeECEF);
        
        /**
         * Whether a point is visible over the horizon.
         * Specify an optional radius to test a sphere.
         */
        bool isVisible(const osg::Vec3d& point, double radius =0.0) const;

        /**
         * Whether "target" with bounding radius "radius" is visible from "eye".
         * This function does not require you to call setEye.
         */
        bool isVisible(const osg::Vec3d& eye, const osg::Vec3d& target, double radius) const;
                
        /**
         * Whether a bounding sphere is visible over the horizon.
         */
        bool isVisible(const osg::BoundingSphere& bs) const {
            return isVisible(bs.center(), bs.radius());
        }

        /**
         * Whether the horizon occludes a point/sphere.
         */
        bool occludes(const osg::Vec3d& point, double radius =0.0) const {
            return !isVisible(point, radius);
        }

        /**
         * Sets the output variable to the horizon plane plane with its
         * normal pointing at the eye. 
         */
        bool getPlane(osg::Plane& out_plane) const;

        //! Caclulate distance from eye to visible horizon
        double getDistanceToVisibleHorizon() const;

        /**
         * Gets the radius of the ellipsoid under the eye
         */
        double getRadius() const;
        
    protected:

        virtual ~Horizon() { }

        osg::EllipsoidModel _em;

        bool       _valid;
        osg::Vec3d _eye;
        osg::Vec3d _eyeUnit;
        osg::Vec3d _VC;       
        double     _VCmag;    // distance from eye to center (scaled)
        double     _VCmag2;   // distance from eye to center squared (scaled)
        double     _VHmag2;   // distance from eye to horizon squared (scaled)
        double     _coneCos;  // cosine of half-cone
        double     _coneTan;  // tangent of half-cone

        osg::Vec3d _scale;
        osg::Vec3d _scaleInv;
        double     _minVCmag;
        double     _minHAE;
    };


    /**
     * Cull callback that culls a node if it is occluded by the
     * horizon.
     */
    class OSGEARTH_EXPORT HorizonCullCallback : public osg::NodeCallback
    {
    public:
        /** Construct the callback with a default Horizon model. */
        HorizonCullCallback();

        /**
         * Whether to cull by the center point only, or by the bounding sphere.
         */
        void setCullByCenterPointOnly(bool value) { _centerOnly = value; }
        bool getCullByCenterPointOnly() const { return _centerOnly; }

        /**
         * Enable or disable the culler
         */
        void setEnabled(bool value) { _enabled = value; }
        bool getEnabled() const { return _enabled; }

        /**
         * Sets an alternate node whose bounds will be used to perform
         * the culling test. By default, the culling test is performed
         * against the node to which this callback is attached.
         */
        void setProxyNode(osg::Node* node) { _proxy = node; }
        osg::observer_ptr<osg::Node>& getProxyNode() { return _proxy; }
        const osg::observer_ptr<osg::Node>& getProxyNode() const { return _proxy; }

        /**
         * Horizon to cull againts
         */
        void setHorizon(Horizon* horizon) { _horizonProto = horizon; }
        Horizon* getHorizon() { return _horizonProto.get(); }
        const Horizon* getHorizon() const { return _horizonProto.get(); }

    public: // osg::NodeCallback
        void operator()(osg::Node* node, osg::NodeVisitor* nv);

    protected:
        virtual ~HorizonCullCallback() { }

        bool isVisible(osg::Node* node, osg::NodeVisitor* nv);

    private:
        bool _centerOnly;
        bool _enabled;
        osg::ref_ptr<Horizon> _horizonProto;
        osg::observer_ptr<osg::Node> _proxy;
    };


    // Debugging node for visualizing the horizon in stealth mode
    class OSGEARTH_EXPORT HorizonNode : public osg::MatrixTransform
    {
    public:
        HorizonNode();
        void traverse(osg::NodeVisitor& nv);
    };

}

#endif // OSGEARTH_HORIZON_H