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

#include <osgEarth/Common>
#include <osgEarth/Threading>
#include <osg/Array>
#include <osg/Geometry>

namespace osgEarth
{
    /**
     * Drawable that renders lines using the GPU. It will fall back on rendering
     * OpenGL lines when shader-based rendering is unavailable.
     *
     * Note: If you use this class you must have the oe_Camera
     * uniform set. MapNode sets it automatically so any LineDrawable under
     * a MapNode is fine. Otherwise, use the osgEarth::InstallCameraUniform
     * callback on your scene graph.
     */
    class OSGEARTH_EXPORT LineDrawable : public osg::Drawable
    {
    public:

        //! Create a new LineDrawable.
        //! @param[in ] mode GL line mode: GL_LINE_STRIP or GL_LINE_LOOP
        LineDrawable(GLenum mode =GL_LINE_STRIP);

        //! Copy constructor
        LineDrawable(const LineDrawable& rhs, const osg::CopyOp& copy);

        //! Width in pixels of the line
        void setLineWidth(float width_pixels);
        float getLineWidth() const { return _width; }

        //! Stippling pattern for the line (default is 0xFFFF)
        void setStipplePattern(GLushort pattern);
        GLushort getStipplePattern() const { return _pattern; }

        //! Stippling factor for the line (default is 1)
        void setStippleFactor(GLint factor);
        GLint getStippleFactor() const { return _factor; }

        //! Stippling quantization factor (default is 8.0)
        void setStippleQuantize(GLfloat factor);
        GLfloat getStippleQuantize() const { return _quantize; }

        //! Line smoothing (antialiasing)
        void setLineSmooth(bool value);
        bool getLineSmooth() const { return _smooth; }

        //! Sets the overall color of the line
        void setColor(const osg::Vec4& color);
        const osg::Vec4& getColor() const { return _color; }
        
        //! Append a vertex to the line
        void pushVertex(const osg::Vec3& vert);

        //! Set the value of a vertex at index i
        void setVertex(unsigned i, const osg::Vec3& vert);

        //! Gets the vertex at index i
        const osg::Vec3& getVertex(unsigned i) const;

        //! Sets the color of a vertex at index i
        void setColor(unsigned i, const osg::Vec4& color);

        //! Sets whether to use a GPU shader to draw lines. Default=true.
        //! Set this to false to invoke legacy OpenGL line drawing, which
        //! may be a bit faster but will not support GL CORE profile or GLES.
        //! NOTE: Calling this will remove any data you've added to the LineDrawable!
        void setUseGPU(bool value);
        bool getUseGPU() const { return _useGPU; }

        //! Copy a vertex array into the drawable
        void importVertexArray(const osg::Vec3Array* verts);
        
        //! Copy a vertex attribute array into the drawable
        template<typename T>
        void importVertexAttribArray(unsigned location, const T* data);
        
        //! Allocate space for vertices
        void allocate(unsigned numVerts);

        //! Clears all data
        void clear();

        //! Whether the size is zero.
        bool empty() const { return size() == 0; }

        //! Number of vertices in the drawable
        unsigned getNumVerts() const;

        //! Number of vertices in the drawable
        unsigned size() const { return getNumVerts(); }

        //! Appends a vertex to an attribute array. Use this instead of adding
        //! to the array directly!
        template<typename T>
        void pushVertexAttrib(T* vaa, const typename T::ElementDataType& value);

        //! Gets the value of the vertex attribute from an array at index i
        template<typename T>
        const typename T::ElementDataType& getVertexAttrib(const T* vaa, unsigned i) const;

        //! Pre-allocate space for vertices
        void reserve(unsigned size);

        //! Index of the first vertex to draw (default = 0)
        void setFirst(unsigned index);
        unsigned getFirst() const;

        //! Number of vertices to draw (default = 0, which means draw to the
        //! end of the line
        void setCount(unsigned count);
        unsigned getCount() const;

        //! Rebuild the primitive sets for this drawable. You MUST call this
        //! after adding new data to the drawable!
        void dirty();
        void finish() { dirty(); }

        //! Sets a vertex attribute array at the designated index.
        //! The array must be EMPTY.
        void setVertexAttribArray(unsigned i, osg::Array* a);

        //! Sets a line width on a custom stateset that will apply to
        //! all LineDrawables used with that state set.
        static void setLineWidth(osg::StateSet* stateSet, float value, int overrideFlags=osg::StateAttribute::ON);

    public:

        //! Binding location for "previous" vertex attribute (default = 9)
        static int PreviousVertexAttrLocation;

        //! Binding location for "next" vertex attribute (default = 10)
        static int NextVertexAttrLocation;

    public: // osg::Node

        //! Replace methods from META_Node so we can override accept
        osg::Object* cloneType() const override { return new LineDrawable(); }
        osg::Object* clone(const osg::CopyOp& copyop) const override { return new LineDrawable(*this,copyop); }
        bool isSameKindAs(const osg::Object* obj) const override { return dynamic_cast<const LineDrawable*>(obj)!=nullptr; }
        const char* className() const override { return "LineDrawable"; }
        const char* libraryName() const override { return "osgEarth"; }

        //! Override Node::accept to include the singleton GPU statset
        void accept(osg::NodeVisitor& nv) override;

    public: // osg::Object

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

    public:
            
        //! GL mode (for serializer only; do not use)
        void setMode(GLenum mode);
        GLenum getMode() const { return _mode; }

        osg::StateSet* getGPUStateSet() const { return _gpuStateSet.get(); }

    protected:

        //! destructor
        virtual ~LineDrawable();

    private:
        GLenum _mode;
        bool _useGPU;
        osg::Vec4 _color;
        GLint _factor;
        GLushort _pattern;
        GLfloat _quantize;
        float _width;
        bool _smooth;
        unsigned _first;
        unsigned _count;
        osg::Vec3Array* _current;
        osg::Vec3Array* _previous;
        osg::Vec3Array* _next;
        osg::Vec4Array* _colors;

        void initialize();
        void setupShaders();

        friend class LineGroup;
        friend class ObjectIndex;

        unsigned actualVertsPerVirtualVert(unsigned) const;
        unsigned numVirtualVerts(const osg::Array*) const;
        unsigned getRealIndex(unsigned) const;
        void updateFirstCount();

        osg::ref_ptr<osg::StateSet> _gpuStateSet;
        mutable std::mutex _mutex;
        osg::ref_ptr<osg::Geometry> _geom;

    public: // osg::Drawable pass thrus

        void drawImplementation(osg::RenderInfo& ri) const override;

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

        osg::BoundingSphere computeBound() const override {
            return _geom->computeBound();
        }
        osg::BoundingBox computeBoundingBox() const override {
            return _geom->computeBoundingBox();
        }
        bool supports(const AttributeFunctor& f) const override {
            return _geom->supports(f);
        }
        void accept(AttributeFunctor& f) override {
            return _geom->accept(f);
        }
        bool supports(const ConstAttributeFunctor& f) const override {
            return _geom->supports(f);
        }
        void accept(ConstAttributeFunctor& f) const override {
            return _geom->accept(f);
        }
        bool supports(const osg::PrimitiveFunctor& f) const override {
            return _geom->supports(f);
        }
        void accept(osg::PrimitiveFunctor& f) const override {
            return _geom->accept(f);
        }
        bool supports(const osg::PrimitiveIndexFunctor& f) const override {
            return _geom->supports(f);
        }
        void accept(osg::PrimitiveIndexFunctor& f) const override {
            return _geom->accept(f);
        }
        void dirtyGLObjects() override {
            osg::Drawable::dirtyGLObjects();
            if (_geom.valid())
                _geom->dirtyGLObjects();
        }

    };


    /**
     * Group for importing LineDrawables from osg::Geometry or for
     * optimizing multiple LineDrawables for performance.
     */
    class OSGEARTH_EXPORT LineGroup : public osg::Group
    {
    public:
        META_Node(osgEarth, LineGroup);

        //! Construct a new line group
        LineGroup();

        //! Copy constructor
        LineGroup(const LineGroup& rhs, const osg::CopyOp& copy);

        //! Imports any GL line drawables from a node graph, converts them
        //! to LineDrawables, and adds them to this LineGroup. If will detect
        //! and set any LineWidth or LineStipple attributes it finds, but will not
        //! find any attributes that occur in the graph above the imported node.
        //!
        //! If you set removePrimitiveSets to true, it will remove all line-based
        //! primitive sets from the node after import.
        //!
        //! This method is for quickly importing legacy scene graphs; if you are
        //! writing new code, use the LineDrawable API directly instead!
        void import(osg::Node* node, bool removePrimitiveSets =false);
        
        //! Optimize the LineDrawables under this group for performance.
        //! Only call this after you finish adding drawables to your group.
        //! It will attempt to combine drawables and state sets, but it will also
        //! render the graph henceforth immutable.
        void optimize();

        //! Get child i as a LineDrawable
        LineDrawable* getLineDrawable(unsigned i);

    protected:
        //! destructor
        virtual ~LineGroup();
    };

    
    // Template implementations ..........................................

    template<typename T>
    void LineDrawable::pushVertexAttrib(T* vaa, const typename T::ElementDataType& value)
    {
        unsigned nvv = numVirtualVerts(vaa);
        unsigned num = actualVertsPerVirtualVert(nvv);
        for (unsigned i = 0; i<num; ++i)
            vaa->push_back(value);
    }

    template<typename T>
    const typename T::ElementDataType& LineDrawable::getVertexAttrib(const T* vaa, unsigned i) const
    {
        return (*vaa)[getRealIndex(i)];
    }

    template<typename T>
    void LineDrawable::importVertexAttribArray(unsigned location, const T* data)
    {
        T* vaa = osg::cloneType(data);
        _geom->setVertexAttribArray(location, vaa);
        for (unsigned i=0; i < data->getNumElements(); ++i)
            pushVertexAttrib(vaa, (*data)[i]);
    }

} // namespace osgEarth
