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

#include <osgEarth/Common>
#include <osgEarth/TextureArena>
#include <osgEarth/Containers>
#include <osgEarth/NodeUtils>
#include <osg/Matrix>
#include <vector>

namespace osgEarth
{
    namespace Util {
        class ShaderPackage;
    }

    /**
    * Parameters controling the placement and appearance of a decal
    */
    struct DecalParameters
    {
        //! Optional matrix that places and orients the decal
        osg::Matrix matrix;

        //! Rendered size of the decal in world units (meters)
        osg::Vec3f size = osg::Vec3f{ 1.0f, 1.0f, 10.0f };

        //! Texture dimensions in world units (meters). If not set, defaults to 'size'.
        std::optional<osg::Vec2f> textureSize;

        //! Opacity (alpha)
        float opacity = 1.0f;
    };

    /**
    * A single decal.
    * If you change the parameters after adding to a DecalNode, be sure to call
    * dirtyBound() on the DecalNode.
    */
    struct Decal : public DecalParameters
    {
        //! Texture to project on a surface
        Texture::Ptr texture;
    };

    /**
    * DecalNode
    * A node that holds one or more Decals
    */
    class OSGEARTH_EXPORT DecalNode : public osg::Node
    {
    public:
        DecalNode() = default;

        //! Decals living in this node
        std::vector<Decal>& getDecals() { return _decals; }
        const std::vector<Decal>& getDecals() const { return _decals; }

        OE_OPTION(float, minPixels, 0.0f);

        //! If true, render decal bounding boxes
        void debug(bool);

    public: // osg::Node overrides

        void traverse(osg::NodeVisitor& nv) override;
        osg::BoundingSphere computeBound() const override;

    private:
        std::vector<Decal> _decals;
        osg::ref_ptr<osg::Group> _debug;
    };


    /**
     * DecalRTTNode
     * Node that uses an RTT camera to create a single decal from its subgraph.
     */
    class OSGEARTH_EXPORT DecalRTTNode : public osg::Group
    {
    public:
        DecalRTTNode() = default;

        //! Access the decal parameters.
        DecalParameters& getDecal() {
            return _decal;
        }

        //! Dimensions of the RTT texture to create
        void setRTTSize(unsigned width, unsigned height) {
            _texWidth = width;
            _texHeight = height;
        }

        //! Dimensions of the RTT texture to create
        std::pair<unsigned, unsigned> getRTTSize() const {
            return { _texWidth, _texHeight };
        }

        //! If true, the RTT texture will be updated every frame.
        void setDynamic(bool value) {
            _dynamic = value;
        }

        //! If true, the RTT texture will be updated every frame.
        inline bool getDynamic() const {
            return _dynamic;
        }

        //! Whether to automatically set the decal size to the bounding box of the children.
        void setAutoDecalSize(bool value) {
            _autoSize = true;
        }

        //! Whether to automatically set the decal size to the bounding box of the children.
        inline bool getAutoDecalSize() const {
            return _autoSize;
        }

        //! If dynamic is true and you change the children of this node,
        //! call dirty to update the RTT camera.
        void dirty();

    public: // osg::Node overrides

        void traverse(osg::NodeVisitor& nv) override;
        osg::BoundingSphere computeBound() const override;

    protected:

        Decal _decal;
        osg::ref_ptr<osg::Texture2D> _tex;
        osg::ref_ptr<osg::Camera> _rtt;
        mutable bool _needsRTT = true;
        unsigned _texWidth = 1024;
        unsigned _texHeight = 1024;
        bool _dynamic = false;
        bool _autoSize = false;
        std::mutex _rttMutex;
    };

    namespace detail
    {
        struct DecalDrawList
        {
            struct PerCamera
            {
                std::vector<Decal> leaves;
            };
            PerObjectFastMap<osg::State*, PerCamera> _perCamera;
        };
    }

    /**
    * DecalDecorator renders decals collected during the cull traversal
    * by applying them to the "target" graph you specify when creating
    * the decorator.
    */
    class OSGEARTH_EXPORT DecalDecorator : public osg::Camera
    {
    public:
        //! Creates a new decorator that will apply decals to the
        //! target group, and adds it as a child of the target group.
        static DecalDecorator* getOrCreate(osg::Group* target);

        //! Removes an existing DecalDecorator from the target group.
        static void remove(osg::Group* target);


    public: // debugging

        void debugTiles(bool value) {
            _debugTiles = value;
            dirtyUniforms();
        }

        void setPixelsPerTile(int value) {
            _pixelsPerTile = std::max(value, 16);
            dirtyUniforms();
        }

        int getPixelsPerTile() const {
            return _pixelsPerTile;
        }

    protected:

        struct GPUFrustum
        {
            osg::Vec4f planes[4];
        };
        static_assert(sizeof(GPUFrustum) % 16 == 0);

        struct GPUTile
        {
            std::uint32_t count;
            std::uint32_t indices[16];
            std::uint32_t padding[3];
        };
        static_assert(sizeof(GPUTile) % 16 == 0);

        struct GPUParams
        {
            osg::Matrixf invProjMatrix;
            osg::Vec4i viewport = { 0, 0, 0, 0 };
            osg::Vec2i numTiles = { -1, -1 };
            GLuint pixelsPerTile = 16;
            float debugTiles = 0.0f;
        };
        static_assert(sizeof(GPUParams) % 16 == 0);

        struct GPUDecal
        {
            osg::Matrixf mvm;
            osg::Matrixf mvmInverse;
            float halfX, halfY, halfZ; // half extents of bbox (no vec3 in ssbo please)
            union {
                std::int32_t textureIndex = -1;
                std::int32_t count;
            };
            float opacity = 1.0f;
            GLfloat padding[3];
        };
        static_assert(sizeof(GPUDecal) % 16 == 0);

        osg::ref_ptr<TextureArena> _textures;
        std::shared_ptr<detail::DecalDrawList> _drawList;
        mutable std::mutex _mutex;
        bool _debugTiles = false;
        mutable bool _uniformsDirty = false;
        int _pixelsPerTile = 16;

        // shader bindings
        std::uint32_t _paramsBinding = 20;
        std::uint32_t _frustumsBinding = 21;
        std::uint32_t _tilesBinding = 22;
        std::uint32_t _decalsBinding = 23;
        std::uint32_t _texturesBinding = 24;

        Util::ShaderPackage& setBindings(Util::ShaderPackage& package) const;

        struct GLObjects : public PerStateGLObjects
        {
            GLBuffer::Ptr decalsBuffer;
            GLBuffer::Ptr paramsBuffer;
            GLBuffer::Ptr frustumsBuffer;
            GLBuffer::Ptr tilesBuffer;

            std::vector<GPUDecal> decals;
            GPUParams params;
            bool dirty = false;

            osg::Camera* camera = nullptr;
            osg::Matrix mvm;

            osg::GLExtensions* ext() const {
                return paramsBuffer->ext();
            }
        };
        mutable osg::buffered_object<GLObjects> _globjects;

        osg::ref_ptr<osg::Program> _computeFrustumsProgram;
        osg::ref_ptr<osg::Program> _cullProgram;

        void dirtyUniforms();

        void operator()(osg::RenderInfo&) const;

        void releaseGLObjects(osg::State* state) const override;

        void accept(osg::NodeVisitor& nv) override;

    protected:

        // disallow direct construction (use DecalDecorator::getOrCreate instead)
        DecalDecorator();

        // compute a new frustum tile grid if neccessary and return true if it was
        bool computeFrustumGrid(osg::State& state, GLObjects&) const;

        // run the gpu culling
        void cull(osg::State& state, GLObjects&) const;

        // bind results of culling output for rendering
        void applyRenderingState(osg::State& state) const;

        friend class DecalGroup;
        friend class DecalApplier;

        struct CameraCallback : public osg::Camera::DrawCallback {
            CameraCallback(DecalDecorator* d) : decorator(d) {}
            DecalDecorator* decorator = nullptr;
            void operator()(osg::RenderInfo& ri) const override {
                decorator->operator()(ri);
            }
        };
        friend struct CameraCallback;
    };


#define OE_DECAL_APPLIER_ATTR_TYPE (osg::StateAttribute::Type)1001001001

    class OSGEARTH_EXPORT DecalApplier : public osg::StateAttribute
    {
    public:
        META_StateAttribute(osgEarth, DecalApplier, OE_DECAL_APPLIER_ATTR_TYPE);

        void setDecorator(DecalDecorator* dec) {
            _decorator = dec;
        }

    public: // osg::StateAttribute overrides

        void apply(osg::State& state) const override;
        void compileGLObjects(osg::State& state) const override { apply(state); }
        void resizeGLObjectBuffers(unsigned) override {}
        //void releaseGLObjects(osg::State* state) const override;
        int compare(const osg::StateAttribute& rhs) const override { return -1; }

    protected:

        osg::ref_ptr<DecalDecorator> _decorator;

        DecalApplier() = default;
        DecalApplier(const DecalApplier& rhs, const osg::CopyOp& copyop = osg::CopyOp::SHALLOW_COPY)
            : osg::StateAttribute(rhs) {}

        friend class DecalDecorator;
    };

    /**
    * Group that installs a DecalManager to collect decals during the cull traversal.
    * A corresponding DecalDecorator will render those decals on some other subgraph.
    */
    class OSGEARTH_EXPORT DecalGroup : public osg::Group
    {
    public:
        DecalGroup(DecalDecorator* decorator);

        inline void traverse(osg::NodeVisitor& nv) override {
            if (nv.getVisitorType() == nv.CULL_VISITOR) {
                ObjectStorage::set(&nv, _drawList);
            }
            osg::Group::traverse(nv);
        }

    protected:
        std::shared_ptr<detail::DecalDrawList> _drawList;
    };
}
