// tilemgr.cxx -- routines to handle dynamic management of scenery tiles
//
// Written by Curtis Olson, started January 1998.
//
// SPDX-FileCopyrightText: 1998 Curtis L. Olson
// SPDX-License-Identifier: GPL-2.0-or-later

#include <config.h>

#include <algorithm>
#include <functional>

#include <osgViewer/Viewer>
#include <osgDB/Registry>

#include <simgear/constants.h>
#include <simgear/debug/logstream.hxx>
#include <simgear/structure/exception.hxx>
#include <simgear/scene/model/modellib.hxx>
#include <simgear/scene/util/SGReaderWriterOptions.hxx>
#include <simgear/scene/tgdb/VPBTechnique.hxx>
#include <simgear/scene/tgdb/VPBLineFeatureRenderer.hxx>
#include <simgear/scene/tsync/terrasync.hxx>
#include <simgear/misc/strutils.hxx>
#include <simgear/scene/material/matlib.hxx>

#include <Main/fg_props.hxx>
#include <Main/globals.hxx>
#include <Main/sentryIntegration.hxx>
#include <Model/validateSharedModels.hxx>
#include <Scripting/NasalModelData.hxx>
#include <Scripting/NasalSys.hxx>
#include <Viewer/renderer.hxx>
#include <Viewer/splash.hxx>

#include "scenery.hxx"
#include "SceneryPager.hxx"
#include "tilemgr.hxx"

using flightgear::SceneryPager;

class FGTileMgr::TileManagerListener : public SGPropertyChangeListener
{
public:
    TileManagerListener(FGTileMgr* manager) :
        _manager(manager),
        _useVBOsProp(fgGetNode("/sim/rendering/use-vbos", true)),
        _enableCacheProp(fgGetNode("/sim/tile-cache/enable", true)),
        _pagedLODMaximumProp(fgGetNode("/sim/rendering/max-paged-lod", true)),
        _lodDetailed(fgGetNode("/sim/rendering/static-lod/detailed", true)),
        _lodRoughDelta(fgGetNode("/sim/rendering/static-lod/rough-delta", true)),
        _lodBareDelta(fgGetNode("/sim/rendering/static-lod/bare-delta", true)),
        _lodRough(fgGetNode("/sim/rendering/static-lod/rough", true)),
        _lodBare(fgGetNode("/sim/rendering/static-lod/bare", true))
    {
        _useVBOsProp->addChangeListener(this, true);

        _enableCacheProp->addChangeListener(this, true);
        if (_enableCacheProp->getType() == simgear::props::NONE) {
            _enableCacheProp->setBoolValue(true);
        }

        if (_pagedLODMaximumProp->getType() == simgear::props::NONE) {
            // not set, use OSG default / environment value variable
            osg::ref_ptr<osgViewer::View> view(globals->get_renderer()->getView());
            int current = view->getDatabasePager()->getTargetMaximumNumberOfPageLOD();
            _pagedLODMaximumProp->setIntValue(current);
        }
        _pagedLODMaximumProp->addChangeListener(this, true);
        _lodDetailed->addChangeListener(this, true);
        _lodBareDelta->addChangeListener(this, true);
        _lodRoughDelta->addChangeListener(this, true);
    }

    ~TileManagerListener()
    {
        _useVBOsProp->removeChangeListener(this);
        _enableCacheProp->removeChangeListener(this);
        _pagedLODMaximumProp->removeChangeListener(this);
        _lodDetailed->removeChangeListener(this);
        _lodBareDelta->removeChangeListener(this);
        _lodRoughDelta->removeChangeListener(this);
    }

    void valueChanged(SGPropertyNode* prop) override
    {
        if (prop == _useVBOsProp) {
            bool useVBOs = prop->getBoolValue();
            _manager->_options->setPluginStringData("SimGear::USE_VBOS",
                                                    useVBOs ? "ON" : "OFF");
        } else if (prop == _enableCacheProp) {
            _manager->_enableCache = prop->getBoolValue();
        } else if (prop == _pagedLODMaximumProp) {
            int                             v = prop->getIntValue();
            osg::ref_ptr<osgViewer::View> view(globals->get_renderer()->getView());
            if (view) {
              osgDB::DatabasePager* pager = view->getDatabasePager();
              if (pager) pager->setTargetMaximumNumberOfPageLOD(v);
            }
        } else if (prop == _lodDetailed || prop == _lodBareDelta || prop == _lodRoughDelta) {
            // compatibility with earlier versions; set the static LOD ranges appropriately as otherwise (bad) self managed
            // LOD on scenery with range animations doesn't work.
            // see also /sim/rendering/enable-range-lod-animations - which is false by default in > 2019.2 which also fixes
            // the scenery but in a more efficient way.
            _lodRough->setDoubleValue(_lodDetailed->getDoubleValue() + _lodRoughDelta->getDoubleValue());
            _lodBare->setDoubleValue(_lodRough->getDoubleValue() + _lodBareDelta->getDoubleValue());
        }

        flightgear::addSentryBreadcrumb("Property:" + prop->getNameString() + " is now " +
                                            prop->getStringValue(),
                                        "info");
    }

private:
    FGTileMgr* _manager;
    SGPropertyNode_ptr _useVBOsProp,
      _enableCacheProp,
      _pagedLODMaximumProp,
      _lodDetailed,
      _lodRoughDelta,
      _lodBareDelta,
      _lodRough,
      _lodBare
        ;
};

FGTileMgr::FGTileMgr():
    state( Start ),
    last_state( Running ),
    scheduled_visibility(100.0),
    _visibilityMeters(fgGetNode("/environment/visibility-m", true)),
    _lodDetailed(fgGetNode("/sim/rendering/static-lod/detailed", true)),
    _lodRoughDelta(fgGetNode("/sim/rendering/static-lod/rough-delta", true)),
    _lodBareDelta(fgGetNode("/sim/rendering/static-lod/bare-delta", true)),
    _disableNasalHooks(fgGetNode("/sim/temp/disable-scenery-nasal", true)),
    _scenery_loaded(fgGetNode("/sim/sceneryloaded", true)),
    _scenery_override(fgGetNode("/sim/sceneryloaded-override", true)),
    _pager(FGScenery::getPagerSingleton()),
    _enableCache(true),
    _use_vpb(false)
{
}


FGTileMgr::~FGTileMgr() = default;

// Initialize the Tile Manager subsystem
void FGTileMgr::init()
{
    reinit();
}

void FGTileMgr::shutdown()
{
    _listener.reset();

    FGScenery* scenery = globals->get_scenery();
    if (scenery && scenery->get_terrain_branch()) {
        osg::Group* group = scenery->get_terrain_branch();
        group->removeChildren(0, group->getNumChildren());
    }
    // clear OSG cache
    osgDB::Registry::instance()->clearObjectCache();
    state = Start; // need to init again
}

void FGTileMgr::reinit()
{
    SG_LOG( SG_TERRAIN, SG_INFO, "Initializing Tile Manager subsystem." );
    auto terraSync = globals->get_subsystem<simgear::SGTerraSync>();

    // drops the previous options reference
    _options = new simgear::SGReaderWriterOptions;
    _listener.reset(new TileManagerListener(this));

    materialLibChanged();
    _options->setPropertyNode(globals->get_props());

    osgDB::FilePathList &fp = _options->getDatabasePathList();
    const PathList &sc = globals->get_fg_scenery();
    fp.clear();
    for (auto it = sc.begin(); it != sc.end(); ++it) {
        fp.push_back(it->utf8Str());
    }
    _options->setPluginStringData("SimGear::FG_ROOT", globals->get_fg_root().utf8Str());

    if (terraSync) {
        _options->setPluginStringData("SimGear::TERRASYNC_ROOT", globals->get_terrasync_dir().utf8Str());
    }

    if (!_disableNasalHooks->getBoolValue())
      _options->setModelData(new FGNasalModelDataProxy);

    double detailed = fgGetDouble("/sim/rendering/static-lod/detailed", SG_OBJECT_RANGE_DETAILED);
    double rough    = fgGetDouble("/sim/rendering/static-lod/rough-delta", SG_OBJECT_RANGE_ROUGH) + detailed;
    double bare     = fgGetDouble("/sim/rendering/static-lod/bare", SG_OBJECT_RANGE_BARE) + rough;
    double tile_min_expiry = fgGetDouble("/sim/rendering/plod-minimum-expiry-time-secs", SG_TILE_MIN_EXPIRY);
    flightgear::addSentryBreadcrumb("PLod-minimum-expiry time=" + std::to_string(tile_min_expiry), "info");

    _use_vpb = fgGetBool("/scenery/use-vpb");

    _options->setPluginStringData("SimGear::LOD_RANGE_BARE", std::to_string(bare));
    _options->setPluginStringData("SimGear::LOD_RANGE_ROUGH", std::to_string(rough));
    _options->setPluginStringData("SimGear::LOD_RANGE_DETAILED", std::to_string(detailed));
    _options->setPluginStringData("SimGear::PAGED_LOD_EXPIRY", std::to_string(tile_min_expiry));

    string_list scenerySuffixes;
    for (auto node : fgGetNode("/sim/rendering/", true)->getChildren("scenery-path-suffix")) {
        if (node->getBoolValue("enabled", true)) {
            scenerySuffixes.push_back(node->getStringValue("name"));
        }
    }

    if (scenerySuffixes.empty()) {
        // if preferences didn't load, use some default
        scenerySuffixes = {"Objects", "Terrain"}; // default values
    }

    if (terraSync) {
        terraSync->setSceneryPathSuffixes(scenerySuffixes);
    }
    _options->setSceneryPathSuffixes(scenerySuffixes);

    if (state != Start)
    {
      // protect against multiple scenery reloads and properly reset flags,
      // otherwise aircraft fall through the ground while reloading scenery
      if (_scenery_loaded->getBoolValue() == false) {
        SG_LOG( SG_TERRAIN, SG_INFO, "/sim/sceneryloaded already false, avoiding duplicate re-init of tile manager" );
        return;
      }
    }

    _scenery_loaded->setBoolValue(false);
    fgSetDouble("/sim/startup/splash-alpha", 1.0);

    materialLibChanged();

    // remove all old scenery nodes from scenegraph and clear cache
    osg::Group* group = globals->get_scenery()->get_terrain_branch();
    group->removeChildren(0, group->getNumChildren());
    tile_cache.init();

    // clear OSG cache, except on initial start-up
    if (state != Start)
    {
        osgDB::Registry::instance()->clearObjectCache();
    }

    state = Inited;

    previous_bucket.make_bad();
    current_bucket.make_bad();
    scheduled_visibility = 100.0;

    // force an update now
    update(0.0);
}

void FGTileMgr::materialLibChanged()
{
    _options->setMaterialLib(globals->get_matlib());
}

/* schedule a tile for loading, keep request for given amount of time.
 * Returns true if tile is already loaded. */
bool FGTileMgr::sched_tile( const SGBucket& b, double priority, bool current_view, double duration)
{
    // see if tile already exists in the cache
    STGTileEntry *t = tile_cache.get_stg_tile( b );
    if (!t)
    {
        // create a new entry
        t = new STGTileEntry( b );
        SG_LOG( SG_TERRAIN, SG_INFO, "sched_tile: new STG tile entry for:" << b );

        // insert the tile into the cache, update will generate load request
        if ( tile_cache.insert_tile( t ) )
        {
            // Attach to scene graph

            t->addToSceneGraph(globals->get_scenery()->get_terrain_branch());
        } else
        {
            // insert failed (cache full with no available entries to
            // delete.)  Try again later
            delete t;
            return false;
        }

        SG_LOG( SG_TERRAIN, SG_DEBUG, "  New tile cache size " << (int)tile_cache.get_size() );
    }

    // update tile's properties
    tile_cache.request_tile(t,priority,current_view,duration);

    if (_use_vpb) {
        VPBTileEntry *v = tile_cache.get_vpb_tile( b );

        if (!v)
        {
            // create a new entry
            v = new VPBTileEntry( b, _options );
            SG_LOG( SG_TERRAIN, SG_INFO, "sched_tile: new VPB tile entry for:" << b );

            // insert the tile into the cache, update will generate load request
            if ( tile_cache.insert_tile( v ) )
            {
                // Attach to scene graph
                v->addToSceneGraph(globals->get_scenery()->get_terrain_branch());
            } else {
                // insert failed (cache full with no available entries to
                // delete.)  Try again later
                delete v;
                return false;
            }

            SG_LOG( SG_TERRAIN, SG_DEBUG, "  New tile cache size " << (int)tile_cache.get_size() );
        }

        // update tile's properties.  We ensure VPB tiles have maximum priority - priority is calculated as
        // _negative_ the square of the distance from the viewer to the tile.
        // so by multiplying by 0.1 we increase the number towards 0.
        tile_cache.request_tile(v,priority * 0.1,current_view,duration);
    }

    return t->is_loaded();
}

/* schedule needed buckets for the current view position for loading,
 * keep request for given amount of time */
void FGTileMgr::schedule_needed(const SGBucket& curr_bucket, double vis)
{
    // sanity check (unfortunately needed!)
    if (!curr_bucket.isValid() )
    {
        SG_LOG( SG_TERRAIN, SG_ALERT,
                "Attempting to schedule tiles for invalid bucket" );
        return;
    }

    double tile_width = curr_bucket.get_width_m();
    double tile_height = curr_bucket.get_height_m();
    SG_LOG( SG_TERRAIN, SG_INFO,
            "scheduling needed tiles for " << curr_bucket
           << ", tile-width-m:" << tile_width << ", tile-height-m:" << tile_height);


    // cout << "tile width = " << tile_width << "  tile_height = "
    //      << tile_height << endl;
    // starting with 2018.3 we will use deltas rather than absolutes as it is more intuitive for the user
    // and somewhat easier to visualise
    double maxTileRange = _lodDetailed->getDoubleValue() + _lodRoughDelta->getDoubleValue() + _lodBareDelta->getDoubleValue();

    double tileRangeM = std::min(vis, maxTileRange);
    int xrange = (int)(tileRangeM / tile_width) + 1;
    int yrange = (int)(tileRangeM / tile_height) + 1;
    if ( xrange < 1 ) { xrange = 1; }
    if ( yrange < 1 ) { yrange = 1; }

    // make the cache twice as large to avoid losing terrain when switching
    // between aircraft and tower views
    tile_cache.set_max_cache_size( (2*xrange + 2) * (2*yrange + 2) * 2 );
    // cout << "xrange = " << xrange << "  yrange = " << yrange << endl;
    // cout << "max cache size = " << tile_cache.get_max_cache_size()
    //      << " current cache size = " << tile_cache.get_size() << endl;

    // clear flags of all tiles belonging to the previous view set
    tile_cache.clear_current_view();

    // update timestamps, so all tiles scheduled now are *newer* than any tile previously loaded
    osg::FrameStamp* framestamp
            = globals->get_renderer()->getFrameStamp();
    tile_cache.set_current_time(framestamp->getReferenceTime());

    SGBucket b;

    int x, y;
    auto terraSync = globals->get_subsystem<simgear::SGTerraSync>();

    /* schedule all tiles, use distance-based loading priority,
     * so tiles are loaded in innermost-to-outermost sequence. */
    for ( x = -xrange; x <= xrange; ++x )
    {
        for ( y = -yrange; y <= yrange; ++y )
        {
            SGBucket b = curr_bucket.sibling(x, y);
            if (!b.isValid()) {
                continue;
            }

            float priority = (-1.0) * (x*x+y*y);
            sched_tile( b, priority, true, 0.0 );

            if (terraSync) {
                terraSync->scheduleTile(b);
            }
        }
    }
}

/**
 * Update the various queues maintained by the tilemgr (private
 * internal function, do not call directly.)
 */
void FGTileMgr::update_queues(bool& isDownloadingScenery)
{
    osg::FrameStamp* framestamp = globals->get_renderer()->getFrameStamp();
    double current_time = framestamp->getReferenceTime();
    double vis = _visibilityMeters->getDoubleValue();
    TileEntry *e;
    int loading=0;
    int sz=0;

    tile_cache.set_current_time( current_time );
    tile_cache.reset_traversal();

    while ( ! tile_cache.at_end() )
    {
        e = tile_cache.get_current();
        if ( e )
        {
            // Prepare the ssg nodes corresponding to each tile.
            // Set the ssg transform and update it's range selector
            // based on current visibility
            e->prep_ssg_node(vis);

            if (!e->is_loaded()) {
                bool nonExpiredOrCurrent = !e->is_expired(current_time) || e->is_current_view();
                bool downloading = isTileDirSyncing(e->tileFileName);
                isDownloadingScenery |= downloading;
                if ( !downloading && nonExpiredOrCurrent) {
                    // schedule tile for loading with osg pager
                    _pager->queueRequest(e->tileFileName,
                                         e->getNode(),
                                         e->get_priority(),
                                         framestamp,
                                         e->getDatabaseRequest(),
                                         _options.get());
                    loading++;
                }
            } // of tile not loaded case
        } else {
            SG_LOG(SG_TERRAIN, SG_ALERT, "Warning: empty tile in cache!");
        }
        tile_cache.next();
        sz++;
    }

    int drop_count = sz - tile_cache.get_max_cache_size();
    bool dropTiles = false;
    if (_enableCache) {
      dropTiles = ( drop_count > 0 ) && ((loading==0)||(drop_count > 10));
    } else {
      dropTiles = true;
      drop_count = sz; // no limit on tiles to drop
    }

    if (dropTiles)
    {
        long drop_index = _enableCache ? tile_cache.get_drop_tile() :
                                         tile_cache.get_first_expired_tile();
        while ( drop_index > -1 )
        {
            // schedule tile for deletion with osg pager
            TileEntry* old = tile_cache.get_tile(drop_index);
            SG_LOG(SG_TERRAIN, SG_DEBUG, "Dropping:" << old->get_tile_bucket());

            tile_cache.clear_entry(drop_index);

            if (_use_vpb) {
                // Clear out any VPB data - e.g. roads
                simgear::VPBLineFeatureRenderer::unloadFeatures(old->get_tile_bucket());
            }

            osg::ref_ptr<osg::Object> subgraph = old->getNode();
            old->removeFromSceneGraph();
            delete old;
            // zeros out subgraph ref_ptr, so subgraph is owned by
            // the pager and will be deleted in the pager thread.
            _pager->queueDeleteRequest(subgraph);

            if (!_enableCache)
                drop_index = tile_cache.get_first_expired_tile();
            // limit tiles dropped to drop_count
            else if (--drop_count > 0)
                drop_index = tile_cache.get_drop_tile();
            else
               drop_index = -1;
        }
    } // of dropping tiles loop
}

// given the current lon/lat (in degrees), fill in the array of local
// chunks.  If the chunk isn't already in the cache, then read it from
// disk.
void FGTileMgr::update(double)
{
    double vis = _visibilityMeters->getDoubleValue();
    schedule_tiles_at(globals->get_view_position(), vis);

    bool waitingOnTerrasync = false;
    update_queues(waitingOnTerrasync);

    // scenery loading check, triggers after each sim (tile manager) reinit
    if (!_scenery_loaded->getBoolValue())
    {
        bool fdmInited = fgGetBool("sim/fdm-initialized");
        bool positionFinalized = fgGetBool("sim/position-finalized");
        bool sceneryOverride = _scenery_override->getBoolValue();


    // we are done if final position is set and the scenery & FDM are done.
    // scenery-override can ignore the last two, but not position finalization.
        if (positionFinalized && (sceneryOverride || (isSceneryLoaded() && fdmInited)))
        {
            _scenery_loaded->setBoolValue(true);
            fgSplashProgress("");

            // by this time Models/ should be in-sync, so run the check now.
            // we could do this in a listener on the property, but we already have
            // 'special' knowledge of Models/ in isTileDirSyncing so this feels
            // reasonable.
            flightgear::validateSharedModels();
        }
        else
        {
            if (!positionFinalized) {
                fgSplashProgress("finalize-position");
            } else if (waitingOnTerrasync) {
                fgSplashProgress("downloading-scenery");
            } else {
                fgSplashProgress("loading-scenery");
            }

            // be nice to loader threads while waiting for initial scenery, reduce to 20fps
            SGTimeStamp::sleepForMSec(50);
        }
    }
}

// schedule tiles for the viewer bucket
// (FDM/AI/groundcache/... should use "schedule_scenery" instead)
void FGTileMgr::schedule_tiles_at(const SGGeod& location, double range_m)
{
    // SG_LOG( SG_TERRAIN, SG_DEBUG, "FGTileMgr::update() for "
    //         << longitude << " " << latitude );

    current_bucket = SGBucket( location );

    // schedule more tiles when visibility increased considerably
    // TODO Calculate tile size - instead of using fixed value (5000m)
    if (range_m - scheduled_visibility > 5000.0)
        previous_bucket.make_bad();

    // SG_LOG( SG_TERRAIN, SG_DEBUG, "Updating tile list for "
    //         << current_bucket );
    fgSetInt( "/environment/current-tile-id", current_bucket.gen_index() );

    // do tile load scheduling.
    // Note that we need keep track of both viewer buckets and fdm buckets.
    if ( state == Running ) {
        if (last_state != state)
        {
            SG_LOG( SG_TERRAIN, SG_DEBUG, "State == Running" );
        }
        if (current_bucket != previous_bucket) {
            // We've moved to a new bucket, we need to schedule any
            // needed tiles for loading.
            SG_LOG( SG_TERRAIN, SG_INFO, "FGTileMgr: at " << location << ", scheduling needed for:" << current_bucket
                   << ", visibility=" << range_m);
            scheduled_visibility = range_m;
            schedule_needed(current_bucket, range_m);
        }

        // save bucket
        previous_bucket = current_bucket;
    } else if ( state == Start || state == Inited ) {
        SG_LOG( SG_TERRAIN, SG_DEBUG, "State == Start || Inited" );
        // do not update bucket yet (position not valid in initial loop)
        state = Running;
        previous_bucket.make_bad();
    }
    last_state = state;
}

/** Schedules scenery for given position. Load request remains valid for given duration
 * (duration=0.0 => nothing is loaded).
 * Used for FDM/AI/groundcache/... requests. Viewer uses "schedule_tiles_at" instead.
 * Returns true when all tiles for the given position are already loaded, false otherwise.
 */
bool FGTileMgr::schedule_scenery(const SGGeod& position, double range_m, double duration)
{
    // sanity check (unfortunately needed!)
    if (!position.isValid())
        return false;
    const float priority = 0.0;
    bool available = true;

    SGBucket bucket(position);
    available = sched_tile( bucket, priority, false, duration );

    if ((!available)&&(duration==0.0)) {
        SG_LOG( SG_TERRAIN, SG_DEBUG, "schedule_scenery: Scheduling tile at bucket:" << bucket << " return false" );
        return false;
    }

    SGVec3d cartPos = SGVec3d::fromGeod(position);

    // Traverse all tiles required to be there for the given visibility.
    double tile_width = bucket.get_width_m();
    double tile_height = bucket.get_height_m();
    double tile_r = 0.5*sqrt(tile_width*tile_width + tile_height*tile_height);
    double max_dist = tile_r + range_m;
    double max_dist2 = max_dist*max_dist;

    int xrange = (int)fabs(range_m / tile_width) + 1;
    int yrange = (int)fabs(range_m / tile_height) + 1;

    for ( int x = -xrange; x <= xrange; ++x )
    {
        for ( int y = -yrange; y <= yrange; ++y )
        {
            // We have already checked for the center tile.
            if ( x != 0 || y != 0 )
            {
                SGBucket b = bucket.sibling(x, y );
                if (!b.isValid()) {
                    continue;
                }

                double distance2 = distSqr(cartPos, SGVec3d::fromGeod(b.get_center()));
                // Do not ask if it is just the next tile but way out of range.
                if (distance2 <= max_dist2)
                {
                    available &= sched_tile( b, priority, false, duration );
                    if ((!available)&&(duration==0.0))
                        return false;
                }
            }
        }
    }

    return available;
}

// Returns true if tiles around current view position have been loaded
bool FGTileMgr::isSceneryLoaded()
{
    double range_m = 100.0;
    if (scheduled_visibility < range_m)
        range_m = scheduled_visibility;

    return schedule_scenery(globals->get_view_position(), range_m, 0.0);
}

bool FGTileMgr::isTileDirSyncing(const std::string& tileFileName) const
{
    auto terraSync = globals->get_subsystem<simgear::SGTerraSync>();
    if (!terraSync) {
        return false;
    }

    // if Models is syncing, also wait for it, since otherwise
    // we get load errors
    if (terraSync->isDataDirPending("Models")) {
        return true;
    }

    std::string nameWithoutExtension = tileFileName.substr(0, tileFileName.size() - 4);
    long int bucketIndex = simgear::strutils::to_int(nameWithoutExtension);
    SGBucket bucket(bucketIndex);

    return terraSync->isTileDirPending(bucket.gen_base_path());
}
