/* -*-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 "TileRenderModel"
#include <osgEarth/TerrainOptions>
#include <osgEarth/TileKey>
#include <osgEarth/Threading>
#include <osgEarth/PatchLayer>
#include <osgEarth/Metrics>
#include <osgEarth/Math>
#include <osgEarth/TileMesher>
#include <osg/Geometry>

namespace osgEarth
{
    namespace REX
    {
        using namespace osgEarth;

        using DrawElementsBase = osg::DrawElementsUShort;
        //using DrawElementsBase = osg::DrawElementsUInt;

        // 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)
                {
                    GLObjects::get(_globjects, *state)._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);
                if (size > _globjects.size())
                    _globjects.resize(size);
            }

            // Shareable because they are static and bindless
            struct GLObjects : public BindlessShareableGLObjects
            {
                GLBuffer::Ptr _ebo; // bindless buffer
            };
            mutable osg::buffered_object<GLObjects> _globjects;
        };

        // 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;

            // Shareable because they are static and bindless
            struct GLObjects : public BindlessShareableGLObjects
            {
                DrawElementsIndirectBindlessCommandNV _command;
                GLBuffer::Ptr _vbo;
            };
            mutable osg::buffered_object<GLObjects> _globjects;

        //private:

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


        /**
        * 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();

        //! 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 TerrainOptionsAPI& options,
            osg::ref_ptr<SharedGeometry>& out,
            Cancelable* state);

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

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

    public: // osg::Node

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

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

    protected:
        using GeometryMap = std::unordered_map<GeometryKey, osg::ref_ptr<SharedGeometry>>;

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

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

        osg::ref_ptr<SharedGeometry> convertTileMeshToSharedGeometry(
            const TileMesh& mesh) const;

        bool _enabled;
        bool _debug;
    };

} } // namespace osgEarth::REX

#endif // OSGEARTH_DRIVERS_REX_TERRAIN_ENGINE_GEOMETRY_POOL
