/* osgEarth - Geospatial SDK for OpenSceneGraph
* Copyright 2018 Pelican Mapping
* MIT License
*/
#include "SingleKeyNodeFactory"
#include "DynamicLODScaleCallback"
#include "FileLocationCallback"
#include "TilePagedLOD"
#include "TileGroup"

#include <osgEarth/Registry>
#include <osgEarth/HeightFieldUtils>
#include <osgEarth/Progress>
#include <osgEarth/Containers>
#include <osgEarth/Horizon>

#include <osgUtil/CullVisitor>

#include <osg/LOD>

using namespace osgEarth::Drivers::MPTerrainEngine;
using namespace osgEarth;

#define LC "[SingleKeyNodeFactory] "



SingleKeyNodeFactory::SingleKeyNodeFactory(const Map*                    map,
                                           TileModelFactory*             modelFactory,
                                           TileModelCompiler*            modelCompiler,
                                           TileNodeRegistry*             liveTiles,
                                           ResourceReleaser*             releaser,
                                           const MPTerrainEngineOptions& options,
                                           TerrainEngine*                engine ) :
_frame           ( map ),
_modelFactory    ( modelFactory ),
_modelCompiler   ( modelCompiler ),
_liveTiles       ( liveTiles ),
_releaser        ( releaser ),
_options         ( options ),
_engine          ( engine )
{
    //nop
}

unsigned
SingleKeyNodeFactory::getMinimumRequiredLevel()
{
    // highest required level in the map:
    unsigned minLevel = _frame.getHighestMinLevel();

    return _options.minLOD().isSet() ?
        osg::maximum( _options.minLOD().value(), minLevel ) :
        minLevel;
}

//Experimental: this speeds up tile loading a lot; it's sort of an alternative
// to the OSG_MAX_PAGEDLOD cache.
//#define EXPERIMENTAL_TILE_NODE_CACHE
#ifdef EXPERIMENTAL_TILE_NODE_CACHE
namespace
{
    typedef LRUCache<TileKey, osg::ref_ptr<TileNode> > TileNodeCache;
    TileNodeCache cache(true, 16384);
}
#endif

osg::Node*
SingleKeyNodeFactory::createTile(TileModel*        model,
                                 bool              setupChildrenIfNecessary,
                                 ProgressCallback* progress)
{
#ifdef EXPERIMENTAL_TILE_NODE_CACHE
    osg::ref_ptr<TileNode> tileNode;
    TileNodeCache::Record rec;
    cache.get(model->_tileKey, rec);
    if ( rec.valid() )
    {
        tileNode = rec.value().get();
    }
    else
    {
        tileNode = _modelCompiler->compile( model, _frame );
        cache.insert(model->_tileKey, tileNode);
    }
#else
    // compile the model into a node:
    osg::ref_ptr<TileNode> tileNode = _modelCompiler->compile(model, _frame, progress);
#endif

    // see if this tile might have children.
    bool prepareForChildren =
        setupChildrenIfNecessary &&
        model->_tileKey.getLOD() < *_options.maxLOD();

    osg::Node* result = 0L;

    if ( prepareForChildren )
    {
        osg::BoundingSphere bs = tileNode->getBound();
        TilePagedLOD* plod = new TilePagedLOD( _engine->getUID(), _liveTiles.get(), _releaser.get() );
        plod->setCenter  ( bs.center() );
        plod->addChild   ( tileNode.get() );
        plod->setFileName( 1, Stringify() << tileNode->getKey().str() << "." << _engine->getUID() << ".osgearth_engine_mp_tile" );
        
        double rangeFactor = _options.minTileRangeFactor().get();
        //if (_options.adaptivePolarRangeFactor() == true)
        //{
        //    double lat = model->_tileKey.getExtent().yMin() < 0 ? -model->_tileKey.getExtent().yMax() : model->_tileKey.getExtent().yMin();
        //    double latRad = osg::DegreesToRadians(lat);
        //    rangeFactor -= (rangeFactor - 1.0)*sin(latRad)*sin(latRad);
        //}
        plod->setRangeFactor(rangeFactor);

        // Setup expiration.
        if (_options.minExpiryFrames().isSet())
        {
            plod->setMinimumExpiryFrames(1, *_options.minExpiryFrames());
        }
        
        if (_options.minExpiryTime().isSet())
        {         
            plod->setMinimumExpiryTime(1, *_options.minExpiryTime());
        }      

        if ( _options.rangeMode().value() == osg::LOD::DISTANCE_FROM_EYE_POINT )
        {
            //Compute the min range based on the 2D size of the tile
            GeoExtent extent = model->_tileKey.getExtent();
            double radius = 0.0;
      
            GeoPoint lowerLeft(extent.getSRS(), extent.xMin(), extent.yMin(), 0.0, ALTMODE_ABSOLUTE);
            GeoPoint upperRight(extent.getSRS(), extent.xMax(), extent.yMax(), 0.0, ALTMODE_ABSOLUTE);
            osg::Vec3d ll, ur;
            lowerLeft.toWorld( ll );
            upperRight.toWorld( ur );
            double radiusDiag = (ur - ll).length() / 2.0;

            if (_options.adaptivePolarRangeFactor() == true )
            {
                GeoPoint left(extent.getSRS(), extent.xMin(), extent.yMin()+extent.height()*0.5, 0.0, ALTMODE_ABSOLUTE);
                GeoPoint right(extent.getSRS(), extent.xMax(), extent.yMin()+extent.height()*0.5, 0.0, ALTMODE_ABSOLUTE);
                osg::Vec3d l, r;
                left.toWorld(l);
                right.toWorld(r);
                double radiusHoriz = 1.4142 * (r - l).length() / 2.0;
                
                double lat = model->_tileKey.getExtent().yMin() < 0 ? -model->_tileKey.getExtent().yMax() : model->_tileKey.getExtent().yMin();
                double latRad = osg::DegreesToRadians(lat);

                // mix between diagonal radius and horizontal radius based on latitude
                double t = cos(latRad);
                t = 1.0-(1.0-t)*(1.0-t);    // decelerate t to weight the mix in favor of equator (diag radius)
                radius = t*radiusDiag + (1.0-t)*radiusHoriz;
            }
            else
            {
                radius = radiusDiag;
            }
          
            float minRange = radius;

            plod->setRange( 0, minRange, FLT_MAX );
            plod->setRange( 1, 0, minRange );
            plod->setRangeMode( osg::LOD::DISTANCE_FROM_EYE_POINT );
        }
        else
        {
            // the *2 is because we page in 4-tile sets, not individual tiles.
            float size = 2.0f * _options.tilePixelSize().value();
            plod->setRange( 0, 0.0f, size );
            plod->setRange( 1, size, FLT_MAX );
            plod->setRangeMode( osg::LOD::PIXEL_SIZE_ON_SCREEN );
        }
        
        // Install a tile-aligned bounding box in the pager node itself so we can do
        // visibility testing before paging in subtiles.
        plod->setChildBoundingBoxAndMatrix(
            1,
            tileNode->getTerrainBoundingBox(),
            tileNode->getMatrix() );

        // DBPager will set a priority based on the ratio range/maxRange.
        // This will offset that number with a full LOD #, giving LOD precedence.
        // Experimental.
        //plod->setPriorityScale( 1, model->_tileKey.getLOD()+1 );

#if USE_FILELOCATIONCALLBACK
        osgDB::Options* options = plod->getOrCreateDBOptions();
        options->setFileLocationCallback( new FileLocationCallback() );
#endif
        
        result = plod;

        // this one rejects back-facing tiles:
        if ( _frame.getMapInfo().isGeocentric() && _options.clusterCulling() == true )
        {
            osg::HeightField* hf =
                model->_elevationData.getHeightField();

            result->addCullCallback( HeightFieldUtils::createClusterCullingCallback(
                hf,
                tileNode->getKey().getProfile()->getSRS()->getEllipsoid(),
                *_options.verticalScale() ) );
        }
    }
    else
    {
        result = tileNode.release();
    }

    return result;
}


osg::Node*
SingleKeyNodeFactory::createNode(const TileKey&    key, 
                                 bool              accumulate,
                                 bool              setupChildren,
                                 ProgressCallback* progress )
{
    if ( progress && progress->isCanceled() )
        return 0L;

    _frame.sync();
    
    OE_START_TIMER(create_model);

    osg::ref_ptr<TileModel> model[4];
    for(unsigned q=0; q<4; ++q)
    {
        if ( progress && progress->isCanceled() )
            return 0L;
        
        TileKey child = key.createChildKey(q);
        _modelFactory->createTileModel( child, _frame, accumulate, model[q], progress );

        // if any one of the TileModel creations fail, we will be unable to build
        // this quadtile. So goodbye.
        if ( !model[q].valid() )
        {
            OE_DEBUG << LC << "Bailed on key " << key.str() << " due to a NULL model." << std::endl;
            return 0L;
        }
    }

    if (progress)
        progress->stats()["create_tilemodel_time"] += OE_STOP_TIMER(create_model);

    bool makeTile;

    // If this is a request for a root tile, make it no matter what.
    if ( key.getLOD() == 0 || (key.getLOD()-1) == _options.firstLOD().value() )
    {
        makeTile = true;
    }

    // If there's a minimum LOD set, and we haven't reached it yet, make the tile.
    else if ( key.getLOD() <= getMinimumRequiredLevel() )
    {
        makeTile = true;
    }

    // Otherwise, only make the tile if at least one quadrant has REAL data
    // (not fallback data).
    else
    {
        makeTile = false;
        for(unsigned q=0; q<4; ++q)
        {
            if ( model[q]->hasRealData() )
            {
                makeTile = true;
                break;
            }
        }
    }
    
    if ( progress && progress->isCanceled() )
        return 0L;

    OE_START_TIMER(compile_tile);

    osg::ref_ptr<osg::Group> quad;

    if ( makeTile )
    {
        quad = new osg::Group();

        for( unsigned q=0; q<4; ++q )
        {
            osg::ref_ptr<osg::Node> tile = createTile(model[q].get(), setupChildren, progress);
            _engine->notifyOfTerrainTileNodeCreation( model[q]->_tileKey, tile.get() );
            quad->addChild( tile.get() );
        }
    }

    if (progress)
        progress->stats()["compile_tilemodel_time"] += OE_STOP_TIMER(compile_tile);

    return quad.release();
}
