/* -*-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 <osgEarth/TerrainOptions>
#include <osgEarth/TileKey>
#include <osgEarth/Threading>
#include <osgEarth/ResourceReleaser>
#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;

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

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

        META_Node(osgEarthRex, SharedGeometry);

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

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

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

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

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

        void setDrawElements(osg::DrawElements* array) { _drawElements = array; }
        osg::DrawElements* getDrawElements() { return _drawElements.get(); }
        const osg::DrawElements* 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 compileGLObjects(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(); }

    protected:

        virtual ~SharedGeometry();

        osg::ref_ptr<osg::Array>        _vertexArray;
        osg::ref_ptr<osg::Array>        _normalArray;
        osg::ref_ptr<osg::Array>        _colorArray;
        osg::ref_ptr<osg::Array>        _texcoordArray;
        osg::ref_ptr<osg::Array>        _neighborArray;
        osg::ref_ptr<osg::Array>        _neighborNormalArray;
        osg::ref_ptr<osg::DrawElements> _drawElements;
        bool _hasConstraints;

    private:

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

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

        /** Sets an object to release unused GL resources */
        void setReleaser(ResourceReleaser* releaser);

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

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

        typedef std::unordered_map<GeometryKey, osg::ref_ptr<SharedGeometry>, GeometryKey> 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::Mutex _geometryMapMutex;
        GeometryMap _geometryMap;
        osg::ref_ptr<ResourceReleaser> _releaser;
        osg::ref_ptr<osg::DrawElements> _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
        osg::DrawElements* 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
