/* osgEarth - Geospatial SDK for OpenSceneGraph
* Copyright 2018 Pelican Mapping
* MIT License
*/

#ifndef OSGEARTH_DRIVERS_MP_TERRAIN_ENGINE_TILE_MODEL
#define OSGEARTH_DRIVERS_MP_TERRAIN_ENGINE_TILE_MODEL 1

#include "Common"
#include <osgEarth/Common>
#include <osgEarth/Map>
#include <osgEarth/ImageLayer>
#include <osgEarth/TileKey>
#include <osgEarth/Locators>
#include <osgEarth/HeightFieldUtils>
#include <osgEarth/MapInfo>
#include <osgTerrain/Locator>
#include <osgTerrain/Layer>
#include <osg/Image>
#include <osg/StateSet>
#include <osg/Texture2D>
#include <osg/State>
#include <osg/NodeVisitor>
#include <map>

namespace osgEarth { namespace Drivers { namespace MPTerrainEngine
{
    using namespace osgEarth;

    class TileModel : public osg::Referenced
    {
    public:
        void resizeGLObjectBuffers(unsigned maxSize);
        void releaseGLObjects(osg::State* state) const;

    public:
        // do not change the order of these.
        enum Neighbor
        {
            NORTHWEST = 0,
            NORTH     = 1,
            NORTHEAST = 2,
            WEST      = 3,
            EAST      = 4,
            SOUTHWEST = 5,
            SOUTH     = 6,
            SOUTHEAST = 7
        };

        class ElevationData
        {
        public:
            ElevationData() : _fallbackData(true) { }
            ElevationData(const ElevationData& rhs);

            virtual ~ElevationData() { }

            ElevationData(
                osg::HeightField* hf,
                GeoLocator*       locator,
                bool              fallbackData);

            osg::HeightField* getHeightField() const { return _hf.get(); }
            GeoLocator* getLocator() const { return _locator.get(); }
            bool isFallbackData() const { return _fallbackData; }

            // get a height value using a normalized coord and a locator.
            bool getHeight( const osg::Vec3d& ndc, const GeoLocator* ndcLocator, float& output, RasterInterpolation interp ) const;

            // get a normal vector (in woord space)
            bool getNormal( const osg::Vec3d& ndc, const GeoLocator* ndcLocator, osg::Vec3& output, RasterInterpolation interp ) const;
            
            osg::HeightField* getNeighbor(int xoffset, int yoffset) const
            {
                return _neighbors.getNeighbor(xoffset, yoffset);
            }

            void setNeighbor(int xoffset, int yoffset, osg::HeightField* hf )
            {
                _neighbors.setNeighbor(xoffset, yoffset, hf);
            }

            const HeightFieldNeighborhood& getNeighborhood() const 
            {
                return _neighbors;
            }

            void setParent(osg::HeightField* hf)
            {
                _parent = hf;
            }

            osg::HeightField* getParent() const
            {
                return _parent.get();
            }

        private:
            osg::ref_ptr<osg::HeightField> _hf;
            osg::ref_ptr<GeoLocator>       _locator;
            bool                           _fallbackData;
            osg::ref_ptr<osg::HeightField> _parent;

            HeightFieldNeighborhood _neighbors;
        };

        class NormalData
        {
        public:
            NormalData() : _fallbackData(true), _unit(0) { }
            NormalData(const NormalData& rhs);

            virtual ~NormalData() { }

            NormalData(
                osg::HeightField* hf,
                GeoLocator*       locator,
                bool              fallbackData);

            osg::HeightField* getHeightField() const { return _hf.get(); }
            GeoLocator* getLocator() const { return _locator.get(); }
            bool isFallbackData() const { return _fallbackData; }

            void setNeighbor(int xoffset, int yoffset, osg::HeightField* hf )
            {
                _neighbors.setNeighbor(xoffset, yoffset, hf);
            }

            void setParent(osg::HeightField* hf)
            {
                _parent = hf;
            }

            osg::HeightField* getParent() const
            {
                return _parent.get();
            }

            const HeightFieldNeighborhood& getNeighborhood() const 
            {
                return _neighbors;
            }

            int getUnit() const
            {
                return _unit;
            }

        public:
            osg::ref_ptr<osg::HeightField> _hf;
            osg::ref_ptr<GeoLocator>       _locator;
            bool                           _fallbackData;
            osg::ref_ptr<osg::HeightField> _parent;
            int                            _unit;

            HeightFieldNeighborhood _neighbors;
        };


        class ColorData
        {
        public:
            ColorData() : _fallbackData(true), _order(0), _hasAlpha(false) { }

            /** Copy ctor - shallow */
            ColorData(const ColorData& rhs);

            /** dtor */
            virtual ~ColorData() { }

            ColorData(
                const osgEarth::ImageLayer* imageLayer,
                unsigned                    order,
                osg::Image*                 image,
                GeoLocator*                 locator,
                bool                        fallbackData =false );


            osgEarth::UID getUID() const {
                return _layer->getUID();
            }

            unsigned getOrder() const {
                return _order;
            }

            const GeoLocator* getLocator() const {
                return _locator.get();
            }

            osg::Texture* getTexture() const {
                return _texture.get();
            }

            const osgEarth::ImageLayer* getMapLayer() const {
                return _layer.get(); }

            bool isFallbackData() const {
                return _fallbackData;
            }
            void setIsFallbackData(bool value) {
                _fallbackData = value;
            }

            bool hasAlpha() const {
                return _hasAlpha;
            }


            osg::BoundingSphere computeBound() const {
                osg::BoundingSphere bs;
                osg::Vec3d v;
                if (getLocator()->convertLocalToModel(osg::Vec3d(0.5,0.5,0.0), v)) {
                    bs.center() = v;
                }
                if (getLocator()->convertLocalToModel(osg::Vec3d(0.0,0.0,0.0), v)) {
                    bs.radius() = (bs.center() - v).length();
                }
                return bs;
            }


            osg::ref_ptr<const osgEarth::ImageLayer> _layer;
            osg::ref_ptr<GeoLocator>                 _locator;
            osg::ref_ptr<osg::Texture>               _texture;
            bool                                     _fallbackData;
            unsigned                                 _order;
            bool                                     _hasAlpha;
        };

        class ColorDataRef : public osg::Referenced
        {
        public:
            ColorDataRef( const ColorData& layer ) : _layer(layer) { }
            ColorData _layer;
        };


        typedef std::map<UID, ColorData> ColorDataByUID;


    public:
        TileModel( const osgEarth::Revision& mapModelRevision, const MapInfo& mapInfo )
            : _revision(mapModelRevision), _mapInfo(mapInfo), _useParentData(false) { }
        TileModel(const TileModel& rhs);
        virtual ~TileModel() { }

        /**
         * Creates a TileModel representing a quadrant of this tile model.
         * Used for upsampling.
         */
        TileModel* createQuadrant(unsigned q) const;

        /** Map revision used to build this model */
        const Revision& getMapRevision() const { return _revision; }

        /** Whether this tile contains any real data (versus being comprised entirely of fallback data) */
        bool hasRealData() const;

        /** Parent model pointer. */
        void setParentTileModel(const TileModel* parent);
        const TileModel* getParentTileModel() const { return _parentModel.get(); }

        /** Whether there is a heightfield */
        bool hasElevation() const { return _elevationData.getHeightField() != 0L; }

        /** Whether there's a normal map */
        bool hasNormalMap() const { return _normalData.getHeightField() != 0L; }

        /** Whether to use parent data if it's available. */
        bool useParentData() const { return _useParentData; }

        MapInfo                      _mapInfo;
        Revision                     _revision;
        TileKey                      _tileKey;
        osg::ref_ptr<GeoLocator>     _tileLocator;
        ColorDataByUID               _colorData;
        ElevationData                _elevationData;
        NormalData                   _normalData;
        osg::ref_ptr<osg::Texture>   _elevationTexture;
        osg::ref_ptr<osg::Texture>   _normalTexture;
        bool                         _useParentData;
        
        osg::ref_ptr<osg::StateSet>        _parentStateSet;
        osg::observer_ptr<const TileModel> _parentModel;

        // convenience function to pull out a layer by its UID.
        bool getColorData( UID layerUID, ColorData& out ) const {
            ColorDataByUID::const_iterator i = _colorData.find( layerUID );
            if ( i != _colorData.end() ) {
                out = i->second;
                return true;
            }
            return false;
        }

        bool requiresUpdateTraverse() const;

        void updateTraverse(osg::NodeVisitor&) const;

        void generateElevationTexture();

        void generateNormalTexture();
    };

} } } // namespace osgEarth::Drivers::MPTerrainEngine

#endif // OSGEARTH_DRIVERS_MP_TERRAIN_ENGINE_TILE_MODEL
