/* -*-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_DEPTH_ADJUSTMENT_H
#define OSGEARTH_DEPTH_ADJUSTMENT_H 1

#include <osgEarth/Common>
#include <osgEarth/Config>
#include <osgEarth/Units>
#include <osg/Group>
#include <osg/Program>
#include <osg/Uniform>
#include <osg/observer_ptr>

/**
* Depth Offsetting.
* 
* Geometry that coincides with the terrain can result in z-fighting artifacts.
* Depth offsetting mitigates this by biasing the depth value of the geometry.
* The idea is similar to polygon offsetting, but is dynamic and applies to all
* geometry (not just polygons).
*
* Depth offsetting works by pretending the vertex is closer to the camera 
* than it actually is, and writing a depth value based on that simulated
* location. The distance we shift the vertex towards the camera is the "bias".
*
* The "range" is the distance from camera to vertex at which a given
* bias is applied. The minimum bias is applied to geometry at or below the
* minimum range; the maximum bias is applied to geometry at or above the 
* maximum range; and the bias is interpolated for ranges in between.
*
* The tessellation granularity of the geometry affects how well depth offsetting
* works at a given camera distance. As a rule of thumb, the closer the camera is
* to the geometry, the more it needs to be tessellated in order for depth
* offsetting to work properly.
*/
namespace osgEarth
{
    /**
     * Depth Offsetting options.
     */
    class OSGEARTH_EXPORT DepthOffsetOptions
    {
    public:
        DepthOffsetOptions(const Config& conf =Config());

    public:
        //! whether to enable depth offsetting (when applicable)
        OE_OPTION(bool, enabled, true);

        //! a static depth offset distance (in meters) to apply to all geometry.
        OE_OPTION(Distance, range, Distance(0.1, Units::METERS));

        //! depth bias (in meters) applied at the minimum camera range.
        OE_OPTION(Distance, minBias, Distance(100, Units::METERS));

        //! depth bias (in meters) applied at the maximum camera range.
        OE_OPTION(Distance, maxBias, Distance(10000, Units::METERS));

        //! camera range (in meters) at which to apply the minimum depth bias.
        OE_OPTION(Distance, minRange, Distance(1000, Units::METERS));

        //! camera range (in meters) at which to apply the maximum depth bias.
        OE_OPTION(Distance, maxRange, Distance(10000000, Units::METERS));

        //! automatic calculation of the minRange based on geometry analysis
        OE_OPTION(bool, automatic, true);

    public:
        Config getConfig() const;
    };
}

OSGEARTH_SPECIALIZE_CONFIG(osgEarth::DepthOffsetOptions);

namespace osgEarth
{
    /**
     * Interface for adding depth offset methods to anothe class without
     * disturbing the class hierarchy. Use this in conjuction with the
     * Adapter.
     */
    class DepthOffsetInterface
    {
    public:
        virtual void setDepthOffsetOptions( const DepthOffsetOptions& options ) =0;
        virtual const DepthOffsetOptions& getDepthOffsetOptions() const =0;

        virtual ~DepthOffsetInterface() { }
    };

    namespace Util
    {
        /**
         * Adapter that affects depth offset functionality on a graph.
         */
        class OSGEARTH_EXPORT DepthOffsetAdapter : public DepthOffsetInterface
        {
        public:
            DepthOffsetAdapter();
            DepthOffsetAdapter(osg::Node* graph);
        
            virtual ~DepthOffsetAdapter() { }

            void setGraph(osg::Node* graph );
            void recalculate();

            bool isDirty() const { return _dirty; }

            /** whether depth offsetting is supported. */
            bool supported() const { return _supported; }

        public: // DepthOffsetInterface

            void setDepthOffsetOptions( const DepthOffsetOptions& options );
            const DepthOffsetOptions& getDepthOffsetOptions() const { return _options; }

        private:
            void init();
            void updateUniforms();

            bool                         _supported;
            bool                         _dirty;
            osg::observer_ptr<osg::Node> _graph;
            osg::ref_ptr<osg::Uniform>   _paramsUniform;
            DepthOffsetOptions           _options;
        };
    }


    /**
     * Group that applies the depth offset technique to its children.
     */
    class OSGEARTH_EXPORT DepthOffsetGroup : public osg::Group, public DepthOffsetInterface
    {
    public:
        /**
         * Constructs a new depth offset group
         */
        DepthOffsetGroup();


    public: // DepthOffsetInterface

        void setDepthOffsetOptions( const DepthOffsetOptions& options );

        const DepthOffsetOptions& getDepthOffsetOptions() const;


    public: // osg::Node

        virtual osg::BoundingSphere computeBound() const;

        virtual void traverse(osg::NodeVisitor& );

    protected:
        void setUniforms();
        void dirty();
        void scheduleUpdate();

        Util::DepthOffsetAdapter _adapter;
        bool               _updatePending;

        virtual ~DepthOffsetGroup() { }
    };

} // namespace osgEarth

#endif // OSGEARTH_DEPTH_ADJUSTMENT_H
