/* osgEarth
 * Copyright 2025 Pelican Mapping
 * MIT License
 */
#pragma once

#include <osgEarth/Common>
#include <osgEarth/Containers>
#include <osgEarth/IOTypes>
#include <osg/Image>
#include <osg/Node>
#include <osgDB/Options>
#include <osgDB/ReaderWriter>

namespace osgEarth
{
    class URI;
    class ProgressCallback;

    using namespace Threading;

    /**
     * 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);

        /** Copy constructor. */
        URIContext(const URIContext& rhs);

        /** 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 store( 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;

        //! Add a header name/value pair to use when requesting a remote URL
        void addHeader(const std::string& name, const std::string& value);

        //! Headers in this URIContext
        const Headers& getHeaders() const;

        //! Headers in this URIContext
        Headers& getHeaders();


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

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

    /**
     * Stream container for reading a URI directly from a stream
     */
    class OSGEARTH_EXPORT URIStream
    {
    public:
        URIStream(const URI& uri, std::ios_base::openmode mode =std::ios_base::in);

        virtual ~URIStream();

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

    protected:
        friend class URI;
        std::istream* _instream;
    };

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

    /**
     * Represents the location of a resource, providing the raw (original, possibly
     * relative) and absolute forms.
     *
     * URI is serializable and may be used in an earth file, like in the following
     * example. Note that in earth files, the URI is actually called "url"; this is
     * simply because of an old convention and we wish to avoid breaking backwards
     * compatibility.
     *
     *   <url>../path/relative/to/earth/file</url>
     *
     * Note also that a relative URI will be relative to the location of the 
     * parent resource (usually the earth file itself). 
     *
     * You can also specify osgDB plugin options; for example:
     *
     *   <url options_string="JPEG_QUALITY 60">../path/to/image.jpg</url>
     *
     * Of course, options are particular to OSG plugins, so please consult the
     * code for your plugin for more information.
     */
    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; }

        /** osgDB::Options option string (plugin options) */
        optional<std::string>& optionString() { return _optionString; }
        const optional<std::string>& optionString() const { return _optionString; }

    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; }

        bool operator == ( const URI& rhs ) const { return _fullURI.compare(rhs._fullURI) == 0; }

        bool operator != ( const URI& rhs ) const { return _fullURI.compare(rhs._fullURI) != 0; }

    public:
        /** Copier */
        URI( const URI& rhs );


    public: // config methods

        Config getConfig() const;

        void mergeConfig(const Config& conf);

    public: // Static convenience methods

        /** Encodes text to URL safe test. Escapes special charaters */
        inline static std::string urlEncode(const std::string &value);

    protected:
        std::string _baseURI;
        std::string _fullURI;
        std::string _cacheKey;
        URIContext  _context;
        optional<std::string> _optionString;

        void ctorCacheKey();
    };
}

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

namespace std {
    // std::hash specialization for URI
    template<> struct hash<osgEarth::URI> {
        inline size_t operator()(const osgEarth::URI& value) const {
            return hash<string>()(value.full());
        }
    };
}

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

namespace osgEarth
{
    /**
     * 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 ? const_cast<URIAliasMap*>(static_cast<const URIAliasMap*>(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()
            : LRUCache<URI,ReadResult>(128u) { }

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

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

    // Config specialization for URI:

    template<> inline
    Config& Config::set<URI>( const std::string& key, const optional<URI>& opt ) {
        if ( opt.isSet() ) {
            remove(key);
            set( key, opt->getConfig() );
        }
        return *this;
    }

    template<> inline
    bool Config::get<URI>( const std::string& key, optional<URI>& output ) const {
        if ( hasChild(key) ) {
            const Config& uriconf = child(key);
            if (!uriconf.value().empty()) {
                URI uri(uriconf.value(), uriconf.referrer());
                uri.mergeConfig(uriconf);
                output = uri;
                return true;
            }
            else return false;
        }
        else return false;
    }

    // Config specialization for URIAliasMap

    template <> inline
    Config& Config::set<URIAliasMap>( const std::string& key, const optional<URIAliasMap>& map ) {
        remove( key );
        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 );
            }
            set(conf);
        }
        return *this;
    }

    template <> inline
    bool Config::get<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.mutable_value().insert( source, target );
            }
            return true;
        }
        else {
            return false;
        }
    }


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


    // Ref.: https://stackoverflow.com/questions/154536/encode-decode-urls-in-c
    std::string URI::urlEncode(const std::string &value)
    {
        std::ostringstream escaped;
        escaped.fill('0');
        escaped << std::hex;

        for (std::string::const_iterator i = value.begin(), n = value.end(); i != n; ++i) {
            std::string::value_type c = (*i);

            // Keep alphanumeric and other accepted characters intact
            if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
                escaped << c;
                continue;
            }

            // Any other characters are percent-encoded
            escaped << std::uppercase;
            escaped << '%' << std::setw(2) << int((unsigned char)c);
            escaped << std::nouppercase;
        }

        return escaped.str();
    }
}
