/* -*-c++-*- */
/* osgEarth - Dynamic map generation toolkit for OpenSceneGraph
 * Copyright 2008-2013 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_URI
#define OSGEARTH_URI 1

#include <osgEarth/Common>
#include <osgEarth/CacheBin>
#include <osgEarth/CachePolicy>
#include <osgEarth/Containers>
#include <osgEarth/IOTypes>
#include <osg/Image>
#include <osg/Node>
#include <osgDB/ReaderWriter>
#include <iostream>
#include <sstream>

namespace osgEarth
{
    class URI;
    class ProgressCallback;

    /**
     * Context for resolving relative URIs.
     *
     * This object provides "context" for a relative URI. In other words, it provides
     * all of the information the system needs to resolve it to an absolute location from
     * which OSG can load data.
     *
     * The "referrer" is the location of an object that "points" to the object in the 
     * corresponding URI. The location conveyed by the URI will be relative to the location of
     * its referrer. For example, a referrer of "http://server/folder/hello.xml" applied
     * to the URI "there.jpg" will resolve to "http://server/folder/there.jpg". NOTE that referrer
     * it not itself a location (like a folder); rather it's the object that referred to the URI
     * being contextualized.
     */
    class OSGEARTH_EXPORT URIContext
    {
    public:
        /** Creates an empty context. */
        URIContext() { }

        /** Creates a context that specifies a referring object. */
        URIContext( const std::string& referrer ) : _referrer(referrer) { }

        /** Copy constructor. */
        URIContext( const URIContext& rhs ) : _referrer(rhs._referrer) { }

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

        /** Serializes this context to an Options structure. This is useful when passing context information
            to an osgDB::ReaderWriter that takes a stream as input -- the stream is anonymous, therefore this
            is the preferred way to communicate the context information. */
        void apply( osgDB::Options* options );

        /** Creates a context from the serialized version in an Options structure (see above) */
        URIContext( const osgDB::Options* options );

        /** Returns the referring object. */
        const std::string& referrer() const { return _referrer; }

        /** True if the context is empty */
        bool empty() const { return _referrer.empty(); }

        /** Parents the input context with the current object, placing the subContext's information
            under it. Used to re-parent relative locations with a higher-level referrer object. */
        URIContext add( const URIContext& subContext ) const;

        /** Returns a new context with the sub path appended to the current referrer path. */
        URIContext add( const std::string& subPath ) const;

        /** creates a string suitable for passing to an osgDB::ReaderWriter implementation */
        std::string getOSGPath( const std::string& target ) const;

    private:
        friend class URI;
        std::string _referrer;
    };

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

    /**
     * Stream container for reading a URI directly from a stream
     */
    class OSGEARTH_EXPORT URIStream
    {
    public:
        URIStream( const URI& uri );

        virtual ~URIStream();

    public:
        // auto-cast to istream
        operator std::istream& ();

    protected:
        friend class URI;
        std::istream*     _fileStream;
        std::stringstream _bufStream;
    };

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

    /**
     * Represents the location of a resource, providing the raw (original, possibly
     * relative) and absolute forms.
     */
    class OSGEARTH_EXPORT URI
    {
    public:
        /**
         * Constructs an empty (and invalid) URI.
         */
        URI();

        /** 
         * Constructs a new URI from a location (typically an absolute url)
         */
        URI( const std::string& location );
        
        /**
         * Constructs a new URI from a location and an existing context.
         */
        URI( const std::string& location, const URIContext& context );

        /**
         * Constructs a new URI from a string.
         */
        URI( const char* location );

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

    public:

        /** The base (possibly relative) location string. */
        const std::string& base() const { return _baseURI; }

        /** The fully qualified location string. */
        const std::string& full() const { return _fullURI; }

        /** The dereference operator also returns the fully qualified URI, since it's a common operation. */
        const std::string& operator * () const { return _fullURI; }

        /** Context with which this URI was created. */
        const URIContext& context() const { return _context; }

        /** Whether the URI is empty */
        bool empty() const { return _baseURI.empty(); }

        /** Whether the object of the URI is cacheable. */
        bool isRemote() const;

        /** Returns a copy of this URI with the suffix appended */
        URI append( const std::string& suffix ) const;

        /** String used for keying the cache */
        const std::string& cacheKey() const { return !_cacheKey.empty() ? _cacheKey : _fullURI; }


    public:

        /** Sets a cache key. By default the cache key is the full URI, but you can override that. */
        void setCacheKey( const std::string& key ) { _cacheKey = key; }


    public: // read methods return a ReadResult object

        ReadResult readObject(
            const osgDB::Options* dbOptions   =0L,
            ProgressCallback*     progress    =0L ) const;

        ReadResult readImage(
            const osgDB::Options* dbOptions   =0L,
            ProgressCallback*     progress    =0L ) const;

        ReadResult readNode(
            const osgDB::Options* dbOptions   =0L,
            ProgressCallback*     progress    =0L ) const;

        ReadResult readString(
            const osgDB::Options* dbOptions   =0L,
            ProgressCallback*     progress    =0L ) const;

        ReadResult readConfig(
            const osgDB::Options* dbOptions   =0L,
            ProgressCallback*     progress    =0L ) const;

    public: // get methods call the read* methods, then just return the raw data.

        osg::Object* getObject(
            const osgDB::Options* dbOptions   =0L,
            ProgressCallback*     progress    =0L ) const { return readObject(dbOptions, progress).releaseObject(); }

        osg::Image* getImage(
            const osgDB::Options* dbOptions   =0L,
            ProgressCallback*     progress    =0L ) const { return readImage(dbOptions, progress).releaseImage(); }

        osg::Node* getNode(
            const osgDB::Options* dbOptions   =0L,
            ProgressCallback*     progress    =0L ) const { return readNode(dbOptions, progress).releaseNode(); }

        std::string getString(
            const osgDB::Options* dbOptions   =0L,
            ProgressCallback*     progress    =0L ) const { return readString(dbOptions, progress).getString(); }

    public:

        bool operator < ( const URI& rhs ) const { return _fullURI < rhs._fullURI; }

    public:
        /** Copier */
        URI( const URI& rhs ) : _baseURI(rhs._baseURI), _fullURI(rhs._fullURI), _context(rhs._context) { }

    public:
        //TODO: methods to load data from the URI.

    protected:
        std::string _baseURI;
        std::string _fullURI;
        std::string _cacheKey;
        URIContext  _context;
    };
    

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


    /**
     * A lookup table that maps URI references to other URI references. This
     * is used as an optional resource mapping table. (See KML's ResourceMap
     * for usage example)
     *
     * WARNING: osgDB::Options will only store a raw pointer to the class, so
     * make sure the scope of the osgDB::Options does not exceed the scope of
     * the embedded alias map!
     */
    class OSGEARTH_EXPORT URIAliasMap
    {
    public:
        /**
         * Inserts a key-value pair into the map.
         */
        void insert( const std::string& key, const std::string& value );

        /**
         * Resolves the input address into a URI string.
         */
        std::string resolve(const std::string& input, const URIContext& context) const;

        /**
         * True if there are no mappings
         */
        bool empty() const { return _map.empty(); }

        /**
         * Clears out the map.
         */
        void clear() { _map.clear(); }

        /**
         * Loads an alias map from an Options.
         */
        static URIAliasMap* from( const osgDB::Options* options ) {
            return options ? static_cast<URIAliasMap*>(const_cast<osgDB::Options*>(options)->getPluginData("osgEarth::URIAliasMap")) : 0L;
        }

        /**
         * Stores an alias map in an Options
         */
        void apply( osgDB::Options* options ) {
            if ( options ) options->setPluginData("osgEarth::URIAliasMap", this);
        }

    protected:
        std::map<std::string,std::string> _map;
        friend class Config;
    };


    /**
     * A custom read callback (that you can set in an osgDB::Options) that will 
     * attempt to resolve pathnames using a URI alias map.
     */
    class OSGEARTH_EXPORT URIAliasMapReadCallback : public osgDB::ReadFileCallback
    {
    public:
        URIAliasMapReadCallback( const URIAliasMap& aliasMap, const URIContext& context );

        virtual osgDB::ReaderWriter::ReadResult openArchive(const std::string& filename, osgDB::ReaderWriter::ArchiveStatus status, unsigned int indexBlockSizeHint, const osgDB::Options* useObjectCache);
        virtual osgDB::ReaderWriter::ReadResult readObject(const std::string& filename, const osgDB::Options* options);
        virtual osgDB::ReaderWriter::ReadResult readImage(const std::string& filename, const osgDB::Options* options);
        virtual osgDB::ReaderWriter::ReadResult readHeightField(const std::string& filename, const osgDB::Options* options);
        virtual osgDB::ReaderWriter::ReadResult readNode(const std::string& filename, const osgDB::Options* options);
        virtual osgDB::ReaderWriter::ReadResult readShader(const std::string& filename, const osgDB::Options* options);

    protected:
        const URIAliasMap& _aliasMap;
        URIContext         _context;
    };


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

    /**
     * A URI result cache that you can embed in an osgDB::Options, and if found,
     * URI will attempt to use it. 
     *
     * WARNING: osgDB::Options will only store a raw pointer to the class, so
     * make sure the scope of the osgDB::Options does not exceed the scope of
     * the embedded cache!
     */
    struct /*header-only*/ URIResultCache : public LRUCache<URI, ReadResult>
    {
        URIResultCache( bool threadsafe =true )
            : LRUCache<URI,ReadResult>( threadsafe ) { }

        static URIResultCache* from(const osgDB::Options* options) {
            return options ? static_cast<URIResultCache*>(const_cast<osgDB::Options*>(options)->getPluginData("osgEarth::URIResultCache")) : 0L;
        }

        void apply( osgDB::Options* options ) {
            if ( options ) options->setPluginData("osgEarth::URIResultCache", this);
        }
    };


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

    /**
     * You can install a post-read callback in a osgDB::Options and the URI
     * class till invoke it on a ReadResult before returning.
     */
    class /*header-only*/ URIPostReadCallback : public osg::Referenced
    {
    public:
        URIPostReadCallback() { }
        virtual ~URIPostReadCallback() { }

        virtual void operator()( ReadResult& result ) =0;

    public:
        void apply(osgDB::Options* options) {
            if ( options ) options->setPluginData("osgEarth::URIPostReadCallback", this);
        }

        static URIPostReadCallback* from(const osgDB::Options* options) {
            return options ? static_cast<URIPostReadCallback*>(const_cast<osgDB::Options*>(options)->getPluginData("osgEarth::URIPostReadCallback")) : 0L;
        }
    };

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

    // Config specialization for URI:

    template <> inline
    void Config::addIfSet<URI>( const std::string& key, const optional<URI>& opt ) {
        if ( opt.isSet() ) {
            Config conf(key, opt->base());
            conf.setReferrer(opt->context().referrer());
            add( conf );
        }
    }

    template<> inline
    void Config::updateIfSet<URI>( const std::string& key, const optional<URI>& opt ) {
        if ( opt.isSet() ) {
            remove(key);
            Config conf(key, opt->base());
            conf.setReferrer(opt->context().referrer());
            add( conf );
        }
    }

    template<> inline
    bool Config::getIfSet<URI>( const std::string& key, optional<URI>& output ) const {
        if ( hasValue(key) ) {
            output = URI( value(key), referrer(key) );
            return true;
        }
        else
            return false;
    }

    // Config specialization for URIAliasMap

    template <> inline
    void Config::addIfSet<URIAliasMap>( const std::string& key, const optional<URIAliasMap>& map ) {
        if ( map.isSet() ) {
            Config conf( key );
            for( std::map<std::string,std::string>::const_iterator i = map->_map.begin(); i != map->_map.end(); ++i ) {
                Config alias( "alias" );
                alias.add( "source", i->first );
                alias.add( "target", i->second );
                conf.add( alias );
            }
            add(conf);
        }
    }

    template <> inline
    void Config::updateIfSet<URIAliasMap>( const std::string& key, const optional<URIAliasMap>& map ) {
        if ( map.isSet() ) {
            remove( key );
            addIfSet( key, map );
        }
    }

    template <> inline
    bool Config::getIfSet<URIAliasMap>( const std::string& key, optional<URIAliasMap>& output ) const {
        Config alias = child(key);
        if ( !alias.empty() ) {
            for( ConfigSet::const_iterator i = alias.children().begin(); i != alias.children().end(); ++i ) {
                std::string source = i->value("source");
                std::string target = i->value("target");
                if ( !source.empty() && !target.empty() )
                    output->insert( source, target );
            }
            return true;
        }
        else {
            return false;
        }
    }
}

#endif // OSGEARTH_URI
