/* osgEarth
 * Copyright 2025 Pelican Mapping
 * MIT License
 */
#include <osgEarth/ResourceLibrary>
#include <osgEarth/ModelResource>
#include <osgEarth/IconResource>
#include <osgEarth/Threading>
#include <osgEarth/XmlUtils>
#include <iterator>

#define LC "[ResourceLibrary] "

using namespace osgEarth;

//------------------------------------------------------------------------

ResourceLibrary::ResourceLibrary(const Config& conf) :
    _initialized(false)
{
    mergeConfig(conf);
    // Load the library immediately
    initialize(nullptr);
}

ResourceLibrary::ResourceLibrary(const std::string& name,
    const URI& uri) :
    _name(name),
    _uri(uri),
    _initialized(false)
{
    _uri = uri;
    // Load the library immediately
    initialize(nullptr);
}

void
ResourceLibrary::mergeConfig( const Config& conf )
{
    if (_name.empty())
        _name = conf.value( "name" );

    conf.get( "url", _uri );

    for( ConfigSet::const_iterator i = conf.children().begin(); i != conf.children().end(); ++i )
    {
        const Config& child = *i;
        if ( child.key() == "skin" )
        {
            addResource( new SkinResource(child) );
        }
        else if ( child.key() == "model" )
        {
            addResource( new ModelResource(child) );
        }
        else if ( child.key() == "icon" )
        {
            addResource( new IconResource(child) );
        }
    }
}

Config
ResourceLibrary::getConfig() const
{
    Config conf("resources");
    {
        Threading::ScopedReadLock shared( const_cast<ResourceLibrary*>(this)->_mutex );

        if ( !_name.empty() )
        {
            conf.set( "name", _name );
        }

        if ( _uri.isSet() )
        {
            conf.set( "url", _uri );
        }
        else
        {
            for( ResourceMap<SkinResource>::const_iterator i = _skins.begin(); i != _skins.end(); ++i )
            {
                SkinResource* res = i->second.get();
                conf.add( res->getConfig() );
            }

            for( ResourceMap<InstanceResource>::const_iterator i = _instances.begin(); i != _instances.end(); ++i )
            {
                InstanceResource* res = i->second.get();
                conf.add( res->getConfig() );
            }
        }
    }
    return conf;
}

void
ResourceLibrary::addResource( Resource* resource )
{
    if ( dynamic_cast<SkinResource*>( resource ) )
    {
        Threading::ScopedWriteLock exclusive(_mutex);
        _skins[resource->name()] = static_cast<SkinResource*>(resource);
    }
    else if ( dynamic_cast<InstanceResource*>(resource) )
    {
        Threading::ScopedWriteLock exclusive(_mutex);
        _instances[resource->name()] = static_cast<InstanceResource*>(resource);

        // Load the bounding box for a ModelResource immediately.  This helps
        // prevent unexepected stalls at runtime the first time a model is used.
        ModelResource* model = dynamic_cast<ModelResource*>(resource);
        if (model)
        {
            model->getBoundingBox(nullptr);
        }
    }
    else
    {
        OE_WARN << LC << "Added a resource type that is not supported; ignoring." << std::endl;
    }
}

void
ResourceLibrary::removeResource( Resource* resource )
{
    if ( dynamic_cast<SkinResource*>( resource ) )
    {
        Threading::ScopedWriteLock exclusive(_mutex);
        _skins.erase( resource->name() );
    }
    else if ( dynamic_cast<InstanceResource*>( resource ) )
    {
        Threading::ScopedWriteLock exclusive(_mutex);
        _instances.erase( resource->name() );
    }
}

namespace
{
    static std::mutex s_initMutex;
}

bool
ResourceLibrary::initialize( const osgDB::Options* dbOptions )
{
    bool ok = true;

    if ( !_initialized )
    {
        std::lock_guard<std::mutex> exclusive(s_initMutex);
        if ( !_initialized )
        {
            ok = false;

            if ( _uri.isSet() )
            {
                OE_INFO << LC << "Loading library from " << _uri->full() << std::endl;
                osg::ref_ptr<XmlDocument> xml = XmlDocument::load( *_uri, dbOptions );
                if ( xml.valid() )
                {
                    ok = true;

                    Config conf = xml->getConfig();
                    if ( conf.key() == "resources" )
                    {
                        mergeConfig( conf );
                    }
                    else
                    {
                        const Config& child = conf.child("resources");
                        if ( !child.empty() )
                            mergeConfig( child );
                    }

                    OE_INFO << LC << "Found " << _skins.size() << " textures, " << _instances.size() << " models" << std::endl;
                }
                else
                {
                    OE_WARN << LC << "Failed to load library from XML\n";
                }
            }
            _initialized = true;
        }
    }

    return ok;
}

SkinResource*
ResourceLibrary::getSkin( const std::string& name, const osgDB::Options* dbOptions ) const
{
    const_cast<ResourceLibrary*>(this)->initialize( dbOptions );
    Threading::ScopedReadLock shared( _mutex );
    ResourceMap<SkinResource>::const_iterator i = _skins.find( name );
    return i != _skins.end() ? i->second.get() : 0L;
}

void
ResourceLibrary::getSkins( SkinResourceVector& output, const osgDB::Options* dbOptions ) const
{
    const_cast<ResourceLibrary*>(this)->initialize( dbOptions );
    Threading::ScopedReadLock shared( _mutex );
    output.reserve( _skins.size() );
    for( ResourceMap<SkinResource>::const_iterator i = _skins.begin(); i != _skins.end(); ++i )
        output.push_back( i->second.get() );
}

void
ResourceLibrary::getSkins(const SkinSymbol* symbol, SkinResourceVector& output, const osgDB::Options* dbOptions) const
{
    const_cast<ResourceLibrary*>(this)->initialize(dbOptions);
    Threading::ScopedReadLock shared(_mutex);

    if (symbol->name().isSet())
    {
        SkinResource* skin = getSkin(symbol->name()->literal(), dbOptions);
        if (skin)
        {
            output.push_back(skin);
        }
    }
    else
    {
        for (ResourceMap<SkinResource>::const_iterator i = _skins.begin(); i != _skins.end(); ++i)
        {
            SkinResource* skin = i->second.get();
            if (matches(symbol, skin))
            {
                output.push_back(skin);
            }
        }
    }
}

SkinResource*
ResourceLibrary::getSkin( const SkinSymbol* symbol, unsigned int rand, const osgDB::Options* dbOptions ) const
{
    const_cast<ResourceLibrary*>(this)->initialize( dbOptions );

    if (symbol->name().isSet())
    {
        return getSkin(symbol->name()->literal(), dbOptions);
    }

    SkinResourceVector candidates;
    getSkins( symbol, candidates );
    unsigned size = candidates.size();
    if ( size == 0 )
    {
        return 0L;
    }
    else if ( size == 1 )
    {
        return candidates[0].get();
    }
    else
    {
        return candidates[ rand % size ].get();
    }
}

bool
ResourceLibrary::matches( const SkinSymbol* q, SkinResource* s ) const
{
    if ( q->name().isSet() )
    {
        return osgEarth::ciEquals(q->name()->literal(), s->name());
    }

    if (q->objectHeight().isSet())
    {
        if (s->minObjectHeight().isSet() && 
            q->objectHeight().value() < s->minObjectHeight().value() )
        {
            return false;
        }
        if (s->maxObjectHeight().isSet() && 
            q->objectHeight().value() > s->maxObjectHeight().value() )
        {
            return false;
        }
    }

    if (q->minObjectHeight().isSet() && 
        s->maxObjectHeight().isSet() && 
        q->minObjectHeight().value() > s->maxObjectHeight().value() )
    {
        return false;
    }

    if (q->maxObjectHeight().isSet() && 
        s->minObjectHeight().isSet() &&
        q->maxObjectHeight().value() < s->minObjectHeight().value() )
    {
        return false;
    }

    if (q->isTiled().isSet() && 
        q->isTiled().value() != s->isTiled().value() )
    {
        return false;
    }

    if (q->tags().size() > 0 && !s->containsTags(q->tags()) )
    {
        return false;
    }

    return true;
}

InstanceResource*
ResourceLibrary::getInstance( const std::string& name, const osgDB::Options* dbOptions ) const
{
    const_cast<ResourceLibrary*>(this)->initialize( dbOptions );
    Threading::ScopedReadLock shared( _mutex );
    ResourceMap<InstanceResource>::const_iterator i = _instances.find( name );
    return i != _instances.end() ? i->second.get() : 0L;
}

ModelResource*
ResourceLibrary::getModel( const ModelSymbol* ms, const osgDB::Options* dbOptions ) const
{
    const_cast<ResourceLibrary*>(this)->initialize( dbOptions );
    Threading::ScopedReadLock shared( _mutex );
    ResourceMap<InstanceResource>::const_iterator i = _instances.find( ms->name()->literal() );
    return i != _instances.end() ? dynamic_cast<ModelResource*>(i->second.get()) : 0L;
}

void
ResourceLibrary::getModels( ModelResourceVector& output, const osgDB::Options* dbOptions ) const
{
    const_cast<ResourceLibrary*>(this)->initialize( dbOptions );
    Threading::ScopedReadLock shared( _mutex );
    output.reserve( _instances.size() );
    for( ResourceMap<InstanceResource>::const_iterator i = _instances.begin(); i != _instances.end(); ++i ) {
        ModelResource* m = dynamic_cast<ModelResource*>(i->second.get());
        if ( m ) output.push_back( m );
    }
}

void
ResourceLibrary::getModels(const ModelSymbol* ms, ModelResourceVector& output, const osgDB::Options* dbOptions) const
{
    if ( !ms ) return;

    // if a name is set, use the other getter.
    if ( ms->name().isSet() )
    {
        ModelResource* mr = getModel(ms, dbOptions);
        if ( mr )
            output.push_back( mr );
    }

    else
    {
        const_cast<ResourceLibrary*>(this)->initialize( dbOptions );

        Threading::ScopedReadLock shared( _mutex );

        output.reserve( _instances.size() );
        for( ResourceMap<InstanceResource>::const_iterator i = _instances.begin(); i != _instances.end(); ++i )
        {
            ModelResource* m = dynamic_cast<ModelResource*>(i->second.get());
            if ( m )
            {
                if (ms && ms->tags().size() > 0 && !m->containsTags(ms->tags()) )
                    continue;

                // if there's a size restriction, pre-load the model so we can know its size.
                if (ms && ms->maxSizeX().isSet() && ms->maxSizeY().isSet())
                {
                    const osg::BoundingBox& bbox = m->getBoundingBox( dbOptions );
                    if ( ms->maxSizeX().get() < (bbox.xMax() - bbox.xMin()) )
                        continue;
                    if ( ms->maxSizeY().get() < (bbox.yMax() - bbox.yMin()) )
                        continue;
                }

                // good, return it
                output.push_back( m );
            }
        }
    }
}