/* -*-c++-*- */
/* osgEarth - Geospatial SDK for OpenSceneGraph
* Copyright 2008-2014 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_DRIVERS_REX_TERRAIN_ENGINE_GEOMETRY_POOL
#define OSGEARTH_DRIVERS_REX_TERRAIN_ENGINE_GEOMETRY_POOL 1

#include "Common"
#include "MeshEditor"
#include "TileRenderModel"
#include <osgEarth/TerrainOptions>
#include <osgEarth/TileKey>
#include <osgEarth/Threading>
#include <osgEarth/PatchLayer>
#include <osgEarth/Metrics>
#include <osgEarth/Math>
#include <osg/Geometry>

//#if OSG_MIN_VERSION_REQUIRED(3,5,9)
//#define SUPPORTS_VAO 1
//#endif

#define VERTEX_VISIBLE       1 // draw it
#define VERTEX_BOUNDARY      2 // vertex lies on a skirt boundary
#define VERTEX_HAS_ELEVATION 4 // not subject to elevation texture
#define VERTEX_SKIRT         8 // it's a skirt vertex (bitmask)
#define VERTEX_CONSTRAINT   16 // part of a non-morphable constraint

namespace osgEarth {
    namespace REX
    {
        using namespace osgEarth;

        using DrawElementsBase = osg::DrawElementsUShort;

        // A DrawElements with a UID.
        class /* internal */ SharedDrawElements : public DrawElementsBase
        {
        public:
            SharedDrawElements(GLenum type) :
                DrawElementsBase(type)
            {
                //nop
            }

            ~SharedDrawElements()
            {
                releaseGLObjects(nullptr);
            }

            void releaseGLObjects(osg::State* state) const override
            {
                DrawElementsBase::releaseGLObjects(state);
                if (state)
                {
                    unsigned cid = GLUtils::getUniqueContextID(*state);
                    _gc[cid]._ebo = nullptr;
                }

                // Do nothing if state is nullptr!
                // Let nature take its course and let the GLObjectPool release it
            }

            void resizeGLObjectBuffers(unsigned size) override
            {
                DrawElementsBase::resizeGLObjectBuffers(size);
                _gc.resize(size);
            }

            struct GCState
            {
                GLBuffer::Ptr _ebo;
            };
            mutable osg::buffered_object<GCState> _gc;
        };

        // Adapted from osgTerrain shared geometry class.
        class /*internal*/ SharedGeometry : public osg::Drawable
        {
        public:
            SharedGeometry();

            SharedGeometry(const SharedGeometry&, const osg::CopyOp& copyop = osg::CopyOp::SHALLOW_COPY);

            META_Node(osgEarthRex, SharedGeometry);

            void setVertexArray(osg::Vec3Array* array) { _vertexArray = array; }
            osg::Vec3Array* getVertexArray() { return _vertexArray.get(); }
            const osg::Vec3Array* getVertexArray() const { return _vertexArray.get(); }

            void setNormalArray(osg::Vec3Array* array) { _normalArray = array; }
            osg::Vec3Array* getNormalArray() { return _normalArray.get(); }
            const osg::Vec3Array* getNormalArray() const { return _normalArray.get(); }

            void setTexCoordArray(osg::Vec3Array* array) { _texcoordArray = array; }
            osg::Vec3Array* getTexCoordArray() { return _texcoordArray.get(); }
            const osg::Vec3Array* getTexCoordArray() const { return _texcoordArray.get(); }

            void setNeighborArray(osg::Vec3Array* array) { _neighborArray = array; }
            osg::Vec3Array* getNeighborArray() { return _neighborArray.get(); }
            const osg::Vec3Array* getNeighborArray() const { return _neighborArray.get(); }

            void setNeighborNormalArray(osg::Vec3Array* array) { _neighborNormalArray = array; }
            osg::Vec3Array* getNeighborNormalArray() { return _neighborNormalArray.get(); }
            const osg::Vec3Array* getNeighborNormalArray() const { return _neighborNormalArray.get(); }

            void setDrawElements(SharedDrawElements* array) { _drawElements = array; }
            SharedDrawElements* getDrawElements() { return _drawElements.get(); }
            const SharedDrawElements* getDrawElements() const { return _drawElements.get(); }

            //! Does this geometry include custom constraints?
            void setHasConstraints(bool value) { _hasConstraints = value; }
            bool hasConstraints() const { return _hasConstraints; }

            // convert to a "real" geometry object
            osg::Geometry* makeOsgGeometry();

            // whether this geometry contains anything
            bool empty() const;

        public: // osg::Drawable

            osg::VertexArrayState* createVertexArrayStateImplementation(osg::RenderInfo& renderInfo) const override;

            void drawVertexArraysImplementation(osg::RenderInfo& renderInfo) const;

            void drawPrimitivesImplementation(osg::RenderInfo& renderInfo) const;

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

            void resizeGLObjectBuffers(unsigned int maxSize) override;

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

            bool supports(const osg::Drawable::AttributeFunctor&) const { return true; }
            void accept(osg::Drawable::AttributeFunctor&);

            bool supports(const osg::Drawable::ConstAttributeFunctor&) const { return true; }
            void accept(osg::Drawable::ConstAttributeFunctor&) const;

            bool supports(const osg::PrimitiveFunctor&) const { return true; }
            void accept(osg::PrimitiveFunctor&) const;

            bool supports(const osg::PrimitiveIndexFunctor&) const { return true; }
            void accept(osg::PrimitiveIndexFunctor&) const;

            const osg::BoundingBox& getBoundingBox() const { return osg::Drawable::getBoundingBox(); }

            const DrawElementsIndirectBindlessCommandNV& getOrCreateNVGLCommand(
                osg::State& state);

        protected:

            virtual ~SharedGeometry();

            osg::ref_ptr<osg::Vec3Array> _vertexArray;
            osg::ref_ptr<osg::Vec3Array> _normalArray;
            osg::ref_ptr<osg::Vec4Array> _colorArray;
            osg::ref_ptr<osg::Vec3Array> _texcoordArray;
            osg::ref_ptr<osg::Vec3Array> _neighborArray;
            osg::ref_ptr<osg::Vec3Array> _neighborNormalArray;
            osg::ref_ptr<SharedDrawElements> _drawElements;
            bool _hasConstraints;


            std::vector<GL4Vertex> _verts;

            struct GCState
            {
                DrawElementsIndirectBindlessCommandNV _command;
                GLBuffer::Ptr _vbo;
            };
            mutable osg::buffered_object<GCState> _gc;

        private:

            friend struct DrawTileCommand;
            friend class GeometryPool;
            mutable osg::buffered_object<GLenum> _ptype;
        };
    }
}


namespace osgEarth {
    namespace REX
    {

        /**
                 * Hashtable key for unique (and therefore shareable) geometries.
                 */
        struct GeometryKey
        {
            GeometryKey() :
                lod(-1),
                tileY(0),
                patch(false),
                size(0u)
            {
                //nop
            }

            GeometryKey(const GeometryKey& rhs) :
                lod(rhs.lod),
                tileY(rhs.tileY),
                patch(rhs.patch),
                size(rhs.size)
            {
                //nop
            }

            bool operator < (const GeometryKey& rhs) const
            {
                if (lod < rhs.lod) return true;
                if (lod > rhs.lod) return false;
                if (tileY < rhs.tileY) return true;
                if (tileY > rhs.tileY) return false;
                if (size < rhs.size) return true;
                if (size > rhs.size) return false;
                if (patch == false && rhs.patch == true) return true;
                return false;
            }

            bool operator == (const GeometryKey& rhs) const
            {
                return
                    lod == rhs.lod &&
                    tileY == rhs.tileY &&
                    size == rhs.size &&
                    patch == rhs.patch;
            }

            bool operator != (const GeometryKey& rhs) const
            {
                return
                    lod != rhs.lod ||
                    tileY != rhs.tileY ||
                    size != rhs.size ||
                    patch != rhs.patch;
            }

            // hash function for unordered_map
            std::size_t operator()(const GeometryKey& key) const
            {
                return osgEarth::hash_value_unsigned(
                    (unsigned)key.lod,
                    (unsigned)key.tileY,
                    key.size,
                    key.patch ? 1u : 0u);
            }

            int      lod;
            int      tileY;
            bool     patch;
            unsigned size;
        };
    }
}

namespace std {
    // std::hash specialization for GeometryKey
    template<> struct hash<osgEarth::REX::GeometryKey> {
        inline size_t operator()(const osgEarth::REX::GeometryKey& key) const {
            return osgEarth::hash_value_unsigned(
                (unsigned)key.lod,
                (unsigned)key.tileY,
                key.size, key.patch ? 1u : 0u);
        }
    };
}


namespace osgEarth {
    namespace REX
    {
    /**
     * Pool of terrain tile geometries.
     *
     * In a geocentric map, every tile at a particular LOD and a particular latitudinal
     * (north-south) extent shares exactly the same geometry; each tile is just shifted
     * and rotated differently. Therefore we can use the same Geometry for all tiles that
     * share the same LOD and same min/max latitude in a geocentric map. In a projected
     * map, all tiles at a given LOD share the same geometry regardless of extent, so eve
     * more sharing is possible.
     *
     * This object creates and returns geometries based on TileKeys, sharing instances
     * whenever possible. Concept adapted from OSG's osgTerrain::GeometryPool.
     */
    class GeometryPool : public osg::Group
    {
    public:
        /** Construct the geometry pool */
        GeometryPool();

    public:
        

        typedef std::unordered_map<GeometryKey, osg::ref_ptr<SharedGeometry>> GeometryMap;

        /**
         * Gets the Geometry associated with a tile key, creating a new one if
         * necessary and storing it in the pool.
         */
        void getPooledGeometry(
            const TileKey& tileKey,
            unsigned tileSize,
            const Map* map,
            const TerrainOptions& options,
            osg::ref_ptr<SharedGeometry>& out,
            Cancelable* state);

        /**
         * The number of elements (incides) in the terrain skirt, if applicable
         */
        int getNumSkirtElements(
            unsigned tileSize,
            float skirtRatio) const;

        /**
         * Are we doing pooling?
         */
        bool isEnabled() const { return _enabled; }

        /**
         * Clear and reset the pool.
         */
        void clear();

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


    public: // osg::Node

        /** Perform an update traversal to check for unused resources. */
        void traverse(osg::NodeVisitor& nv);

    protected:
        virtual ~GeometryPool() { }

        mutable Threading::Gate<GeometryKey> _keygate;
        mutable Threading::Mutex _geometryMapMutex;
        GeometryMap _geometryMap;
        osg::ref_ptr<SharedDrawElements> _defaultPrimSet;

        void createKeyForTileKey(
            const TileKey& tileKey,
            unsigned       size,
            GeometryKey&   out) const;

        SharedGeometry* createGeometry(
            const TileKey& tileKey,
            unsigned tileSize,
            float skirtRatio,
            bool gpuTessellation,
            bool morphTerrain,
            MeshEditor& meshEditor,
            Cancelable* state) const;

        // builds a primitive set to use for any tile without a mask
        SharedDrawElements* createPrimitiveSet(
            unsigned tileSize,
            float skirtRatio,
            bool UseGpuTessellation) const;

        void tessellateSurface(
            unsigned tileSize,
            osg::DrawElements* primSet) const;

        bool _enabled;
        bool _debug;
    };

} } // namespace osgEarth::REX

#endif // OSGEARTH_DRIVERS_REX_TERRAIN_ENGINE_GEOMETRY_POOL
