/* osgEarth
 * Copyright 2026 Pelican Mapping
 * MIT License
 */
#pragma once

#include <osgEarth/Common>
#include <osgEarth/optional>
#include <osgEarth/SpatialReference>
#include <osgEarth/GeoTransform>
#include <osgEarth/Math>

#include <osg/ClusterCullingCallback>
#include <osg/Vec3d>
#include <osg/Vec3>
#include <osg/LOD>
#include <osgUtil/CullVisitor>
#include <osgUtil/RenderBin>

namespace osgEarth
{
    //! Method to use when determining when to switch levels of detail.
    enum class LODMethod
    {
        CAMERA_DISTANCE = osg::LOD::DISTANCE_FROM_EYE_POINT,
        SCREEN_SPACE = osg::LOD::PIXEL_SIZE_ON_SCREEN
    };

    namespace Util
    {
        /**
         * A customized CCC that works correctly under an RTT camera and also with
         * an orthographic projection matrix.
         */
        class SuperClusterCullingCallback : public osg::ClusterCullingCallback
        {
        public:
            bool cull(osg::NodeVisitor* nv, osg::Drawable*, osg::State*) const;
        };


        /**
         * Utility functions for creating cluster cullers
         */
        class OSGEARTH_EXPORT ClusterCullingFactory
        {
        public:
            /**
             * Creates a cluster culling callback based on the data in a node graph.
             * NOTE! Never put a CCC somewhere where it will be under a transform. They
             * only work in absolute world space.
             */
            static osg::NodeCallback* create(osg::Node* node, const osg::Vec3d& ecefControlPoint);

            /**
             * Same as above, but uses another method to compute the parameters. There should only
             * be one, but we need to investigate to see which is the better algorithm. Keeping this
             * for now since it works with the feature setup..
             */
            static osg::NodeCallback* create2(osg::Node* node, const osg::Vec3d& ecefControlPoint);

            /**
             * Creates a cluster culling callback and installs it on the node. If the node is
             * a transform, it will create a group above the transform and install the callback
             * on that group instead.
             */
            static osg::Node* createAndInstall(osg::Node* node, const osg::Vec3d& ecefControlPoint);

            /**
             * Creates a cluster culling callback with the standard parameters.
             */
            static osg::NodeCallback* create(const osg::Vec3& controlPoint, const osg::Vec3& normal, float deviation, float radius);

            /**
             * Creates a cluster culling callback based on an extent (for geocentric tiles).
             */
            static osg::NodeCallback* create(const class GeoExtent& extent);
        };

        /**
         * Simple occlusion culling callback that does a ray interseciton between the eyepoint
         * and a A GeoTransform node.
         */
        struct OSGEARTH_EXPORT OcclusionCullingCallback : public osg::NodeCallback
        {
            OcclusionCullingCallback(GeoTransform* xform);

            /** Maximum eye altitude at which to perform the occlusion culling test */
            double getMaxAltitude() const;
            void setMaxAltitude(double value);

            /**
            * Gets the maximum number of ms that the OcclusionCullingCallback can run on each frame.
            */
            static double getMaxFrameTime();

            /**
            * Sets the maximum number of ms that the OcclusionCullingCallback can run on each frame.
            * @param ms
            *     The maximum number of milliseconds to run the OcclusionCullingCallback on each frame.
            */
            static void setMaxFrameTime(double ms);

        public: //NodeCallback

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

        private:

            osg::observer_ptr<GeoTransform> _xform;
            osg::Vec3d _prevEye;
            bool _visible;
            double _maxAltitude;
            static double _maxFrameTime;
        };

        /**
         * Culling utilities.
         */
        struct OSGEARTH_EXPORT Culling
        {
            static osgUtil::CullVisitor* asCullVisitor(osg::NodeVisitor* nv);
            static osgUtil::CullVisitor* asCullVisitor(osg::NodeVisitor& nv) { return asCullVisitor(&nv); }
        };

        /**
         * Node Visitor that proxies the CullVisitor but uses a separate
         * frustum for bounds-culling.
         */
        class ProxyCullVisitor : public osg::NodeVisitor, public osg::CullStack
        {
        private:
            osgUtil::CullVisitor* _cv;
            osg::Polytope         _proxyFrustum;
            osg::Polytope         _proxyProjFrustum;
            osg::Matrix           _proxyModelViewMatrix;
            osg::Matrix           _proxyProjMatrix;

        public:
            ProxyCullVisitor(osgUtil::CullVisitor* cv, const osg::Matrix& proj, const osg::Matrix& view);

            // access to the underlying cull visitor.
            osgUtil::CullVisitor* getCullVisitor() { return _cv; }

            bool isCulledByProxyFrustum(osg::Node& node);
            bool isCulledByProxyFrustum(const osg::BoundingBox& bbox);

        public: // proxy functions:
            osg::Vec3 getEyePoint() const;
            osg::Vec3 getViewPoint() const;
            float getDistanceToEyePoint(const osg::Vec3& pos, bool useLODScale) const;
            float getDistanceFromEyePoint(const osg::Vec3& pos, bool useLODScale) const;
            float getDistanceToViewPoint(const osg::Vec3& pos, bool useLODScale) const;

        protected:

            osgUtil::CullVisitor::value_type distance(const osg::Vec3& coord, const osg::Matrix& matrix);

            void handle_cull_callbacks_and_traverse(osg::Node& node);

            void apply(osg::Node& node);
            void apply(osg::Transform& xform);
            void apply(osg::Drawable& drawable);
            void apply(osg::LOD& lod);
        };


        /**
         * Horizon culling in a shader program.
         */
        class OSGEARTH_EXPORT HorizonCullingProgram
        {
        public:
            static void install(osg::StateSet* stateset);
            static void remove(osg::StateSet* stateset);
        };


        /**
         * Group that lets you adjust the LOD scale on its children.
         */
        class OSGEARTH_EXPORT LODScaleGroup : public osg::Group
        {
        public:
            LODScaleGroup();

            /**
             * Factor by which to multiply the camera's LOD scale.
             */
            void setLODScaleFactor(float value) { _scaleFactor = value; }
            float getLODScaleFactor() const { return _scaleFactor; }

        public: // osg::Group
            virtual void traverse(osg::NodeVisitor& nv);

        protected:
            virtual ~LODScaleGroup() { }

        private:
            float _scaleFactor;
        };


        struct OSGEARTH_EXPORT CullDebugger
        {
            Config dumpRenderBin(osgUtil::RenderBin* bin) const;
        };


        /**
         * Cull callback for cameras that sets the oe_Camera uniform.
         */
        class OSGEARTH_EXPORT InstallCameraUniform : public osg::NodeCallback
        {
        public:
            META_Object(osgEarth, InstallCameraUniform);

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

            InstallCameraUniform() { }

        protected:
            InstallCameraUniform(const InstallCameraUniform& rhs, const osg::CopyOp& copy) :
                osg::NodeCallback(rhs, copy) { }
        };

        /**
         * Cull callback that limits visibility to an altitude range (HAE)
         */
        class OSGEARTH_EXPORT AltitudeCullCallback : public osg::NodeCallback
        {
        public:
            optional<float>& maxAltitude() { return _maxAltitude; }
            osg::ref_ptr<const SpatialReference>& srs() { return _srs; }

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

        private:
            optional<float> _maxAltitude;
            osg::ref_ptr<const SpatialReference> _srs;
        };

        /**
         * Cull callback that will permit culling but will supress rendering.
         * We use this to toggle node visibility to that paged scene graphs
         * will continue to page in/out even when the layer is not visible.
         */
        class OSGEARTH_EXPORT ToggleVisibleCullCallback : public osg::NodeCallback
        {
        public:
            void setVisible(bool value)
            {
                _visible = value;
            }

            bool getVisible() const
            {
                return _visible;
            }

        private:
            struct EmptyRenderBin : public osgUtil::RenderBin
            {
                EmptyRenderBin(osgUtil::RenderStage* stage) :
                    osgUtil::RenderBin()
                {
                    setName("OE_EMPTY_RENDER_BIN");
                    _stage = stage;
                }
            };
 
            bool _visible = true;

        public:
            void operator()(osg::Node* node, osg::NodeVisitor* nv) override;
        };

        /**
        * A cull callback that only continues if the ancestor node
        * is visible (i.e. if the "oe_visible" user value is set to true).
        */
        class OSGEARTH_EXPORT CullIfVisibleCallback : public osg::NodeCallback
        {
        public:
            void operator()(osg::Node* node, osg::NodeVisitor* nv) override;
        };

        /**
        * A cull callback that invokes a user-supplied lambda function.
        */
        class OSGEARTH_EXPORT LambdaCullCallback : public osg::NodeCallback
        {
        public:
            LambdaCullCallback(std::function<void(osg::Node*, osg::NodeVisitor*)> func)
                : _func(func) {
            }
            void operator()(osg::Node* node, osg::NodeVisitor* nv) {
                if (_func)
                    _func(node, nv);
                else
                    traverse(node, nv);
            }
        private:
            std::function<void(osg::Node*, osg::NodeVisitor*)> _func;
        };

        //! Compute the extent (in on-screen pixels) of an object as pos with radius radius.
        inline float getPixelSize(osg::CullStack* cull, const osg::Vec3d& pos, double radius)
        {
            auto& proj = *cull->getProjectionMatrix();
            bool ortho = ProjectionMatrix::isOrtho(proj);

            auto vpHeight = cull->getViewport()->height();
            double ppu = 0.0; // pixels per world unit

            if (ortho)
            {
                ppu = std::abs(proj(1, 1)) * vpHeight * 0.5;
            }
            else // perspective
            {
                auto eye_vs = pos * (*cull->getModelViewMatrix());
                double dist = std::max(-eye_vs.z() - radius, 1.0);
                ppu = std::abs(proj(1, 1)) * vpHeight * 0.5 / dist;
            }

            return 2.0f * std::max(radius, 0.5) * ppu;
        }
    }
}
