/* osgEarth
 * Copyright 2025 Pelican Mapping
 * MIT License
 */
#ifndef OSGEARTH_HTTP_CLIENT_H
#define OSGEARTH_HTTP_CLIENT_H 1

#include <osgEarth/Common>
#include <osgEarth/IOTypes>
#include <osg/ref_ptr>
#include <osg/Referenced>
#include <osgDB/ReaderWriter>
#include <sstream>
#include <iostream>
#include <string>
#include <map>
#include <vector>

namespace osgEarth
{
    class ProgressCallback;
}

namespace osgEarth { namespace Util
{
    using namespace osgEarth;

    /**
     * An HTTP request for use with the HTTPClient class.
     */
    class OSGEARTH_EXPORT HTTPRequest
    {
    public:
        /** Constructs a new HTTP request that will acces the specified base URL. */
        HTTPRequest( const std::string& url );

        /** copy constructor. */
        HTTPRequest( const HTTPRequest& rhs );

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

        /** Adds an HTTP parameter to the request query string. */
        void addParameter( const std::string& name, const std::string& value );
        void addParameter( const std::string& name, int value );
        void addParameter( const std::string& name, double value );

        using Parameters = std::unordered_map<std::string, std::string>;

        /** Ready-only access to the parameter list (as built with addParameter) */
        const Parameters& getParameters() const;

        //! Add a header name/value pair to an HTTP request
        void addHeader( const std::string& name, const std::string& value );

        //! Collection of headers in this request
        const Headers& getHeaders() const;

        //! Collection of headers in this request
        Headers& getHeaders();

        //! Request headers in a config structure
        Config getHeadersAsConfig() const;

        //! Sets the last modified date of any locally cached data for this request.  This will
        //! automatically add a If-Modified-Since header to the request
        void setLastModified( const DateTime &lastModified );

        /** Gets a copy of the complete URL (base URL + query string) for this request */
        std::string getURL() const;

    private:
        Parameters _parameters;
        Headers _headers;
        std::string _url;
    };

    /**
     * An HTTP response object for use with the HTTPClient class - supports
     * multi-part mime responses.
     */
    class OSGEARTH_EXPORT HTTPResponse
    {
    public:
        enum Code {
            NONE         = 0,
            OK           = 200,
            NOT_MODIFIED = 304,
            BAD_REQUEST  = 400,
            FORBIDDEN    = 403,
            NOT_FOUND    = 404,
            CONFLICT     = 409,
            INTERNAL_SERVER_ERROR = 500
        };
        enum CodeCategory {
            CATEGORY_UNKNOWN   = 0,
            CATEGORY_INFORMATIONAL = 100,
            CATEGORY_SUCCESS       = 200,
            CATEGORY_REDIRECTION   = 300,
            CATEGORY_CLIENT_ERROR  = 400,
            CATEGORY_SERVER_ERROR  = 500
        };

    public:
        /** Constructs a response with the specified HTTP response code */
        HTTPResponse( long code =0L );

        /** Copy constructor */
        HTTPResponse( const HTTPResponse& rhs );

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

        /** Gets the HTTP response code (Code) in this response */
        unsigned getCode() const;

        /** Gets the HTTP response code category for this response */
        unsigned getCodeCategory() const;

        /** True is the HTTP response code is OK (200) */
        bool isOK() const;

        /** True if the request associated with this response was cancelled before it completed */
        void setCanceled(bool value) { _canceled = value; }
        bool isCanceled() const { return _canceled; }

        /** Gets the number of parts in a (possibly multipart mime) response */
        unsigned int getNumParts() const;

        /** Gets the input stream for the nth part in the response */
        std::istream& getPartStream( unsigned int n ) const;

        /** Gets the nth response part as a string */
        std::string getPartAsString( unsigned int n ) const;

        /** Gets the length of the nth response part */
        unsigned int getPartSize( unsigned int n ) const;

        /** Gets the HTTP header associated with the nth multipart/mime response part */
        const std::string& getPartHeader( unsigned int n, const std::string& name ) const;

        /** Gets the master mime-type returned by the request */
        void setMimeType(const std::string& value) { _mimeType = value; }
        const std::string& getMimeType() const;

        /** How long did it take to fetch this response (in seconds) */
        void setDuration(double value) { _duration_s = value; }
        double getDuration() const { return _duration_s; }

        void setMessage(const std::string& value) { _message = value; }
        const std::string& getMessage() const { return _message; }

        void setLastModified(TimeStamp value) { _lastModified = value; }
        TimeStamp getLastModified() const { return _lastModified; }

        bool getFromCache() const { return _fromCache; }
        void setFromCache(bool fromCache) { _fromCache = fromCache; }

        struct Part : public osg::Referenced
        {
            Part() : _size(0) { }
            Headers _headers;
            unsigned int _size;
            std::stringstream _stream;
        };
        typedef std::vector< osg::ref_ptr<Part> > Parts;

        Parts& getParts() { return _parts; }

        Config getHeadersAsConfig() const;

    private:
        Parts       _parts;
        long        _response_code;
        std::string _mimeType;
        bool        _canceled;
        double      _duration_s;
        TimeStamp   _lastModified;
        std::string _message;
        bool        _fromCache;

        void setHeadersFromConfig(const Config& conf);

        friend class HTTPClient;
    };

    /**
     * Object that lets you modify and incoming URL before it's passed to the server
     */
    struct OSGEARTH_EXPORT URLRewriter : public osg::Referenced
    {
        virtual std::string rewrite( const std::string& url ) = 0;
    };

	/**
	 * A configuration handler to apply settings. It can be used for setting client certificates
	 */
	struct OSGEARTH_EXPORT ConfigHandler : public osg::Referenced
	{
		virtual void onInitialize(void* handle) = 0;
		virtual void onGet(void* handle) = 0;
	};

	/**
     * Utility class for making HTTP requests.
     */
    class OSGEARTH_EXPORT HTTPClient
    {
    public:
        //! Interface for pluggable HTTP implementations
        class Implementation : public osg::Referenced
        {
        public:
            virtual void initialize() = 0;

            virtual HTTPResponse doGet(
                const HTTPRequest&    request,
                const osgDB::Options* options,
                ProgressCallback*     progress ) const = 0;

            virtual void setUserAgent(const std::string&) { }

            virtual void setTimeout(long) { }

            virtual void setConnectTimeout(long) { }

            //! Implementation-specific handle if applicable
            virtual void* getHandle() const { return NULL; }

        protected:
            virtual ~Implementation() {}
        };

        //! Factory object to create implementation instances.
        class ImplementationFactory
        {
        public:
            virtual Implementation* create() const = 0;

            virtual ~ImplementationFactory() {};
        };

        //! Install an implementation factory. Do this before anything else
        static void setImplementationFactory(ImplementationFactory* factory);

        /**
         * Returns true is the result code represents a recoverable situation,
         * i.e. one in which retrying might work.
         */
        static bool isRecoverable(ReadResult::Code code)
        {
            return
                code == ReadResult::RESULT_OK ||
                code == ReadResult::RESULT_SERVER_ERROR ||
                code == ReadResult::RESULT_TIMEOUT ||
                code == ReadResult::RESULT_CANCELED;
        }

        /** Gets the user-agent string that all HTTP requests will use. */
        static const std::string& getUserAgent();

        /** Sets a user-agent string to use in all HTTP requests. */
        static void setUserAgent(const std::string& userAgent);

        /** Sets up proxy info to use in all HTTP requests. */
		static void setProxySettings( const optional<ProxySettings> &proxySettings );

        /** Gets up proxy info to use in all HTTP requests. */
        static const optional<ProxySettings> & getProxySettings();

        /**
           Gets the timeout in seconds to use for HTTP requests.*/
        static long getTimeout();

        /**
           Sets the timeout in seconds to use for HTTP requests.
           Setting to 0 (default) is infinite timeout */
        static void setTimeout( long timeout );

        /** Sets the suggested delay (in seconds) before a retry should be attempted
            in the case of a canceled request */
        static void setRetryDelay(float value_seconds);
        static float getRetryDelay();

        /**
           Gets the timeout in seconds to use for HTTP connect requests.*/
        static long getConnectTimeout();

        /**
           Sets the timeout in seconds to use for HTTP connect requests.
           Setting to 0 (default) is infinite timeout */
        static void setConnectTimeout( long timeout );

        /**
         * Gets the URLRewriter that is used to modify urls before sending them to the server
         */
        static URLRewriter* getURLRewriter();

        /**
         * Sets the URLRewriter that is used to modify urls before sending them to the server
         */
        static void setURLRewriter( URLRewriter* rewriter );

		static ConfigHandler* getConfigHandler();

		/**
		* Sets the CurlConfigHandler to configurate the CURL library. It can be used for apply client certificates
		*/
		static void setConfigHandler(ConfigHandler* handler);

		/**
         * One time thread safe initialization. In osgEarth, you don't need
         * to call this directly; osgEarth::Registry will call it at
         * startup.
         */
        static void globalInit();


    public:
        /**
         * Reads an image.
         */
        static ReadResult readImage(
            const HTTPRequest&    request,
            const osgDB::Options* dbOptions =0L,
            ProgressCallback*     progress  =0L );

        /**
         * Reads an osg::Node.
         */
        static ReadResult readNode(
            const HTTPRequest&    request,
            const osgDB::Options* dbOptions =0L,
            ProgressCallback*     progress  =0L );

        /**
         * Reads an object.
         */
        static ReadResult readObject(
            const HTTPRequest&    request,
            const osgDB::Options* dbOptions =0L,
            ProgressCallback*     progress  =0L );

        /**
         * Reads a string.
         */
        static ReadResult readString(
            const HTTPRequest&    request,
            const osgDB::Options* dbOptions =0L,
            ProgressCallback*     progress  =0L );

        /**
         * Downloads a file directly to disk.
         */
        static bool download(
            const std::string& uri,
            const std::string& localPath );

    public:

        /**
         * Performs an HTTP "GET".
         */
        static HTTPResponse get( const HTTPRequest&    request,
                                 const osgDB::Options* dbOptions =0L,
                                 ProgressCallback*     progress  =0L );

        static HTTPResponse get( const std::string&    url,
                                 const osgDB::Options* options  =0L,
                                 ProgressCallback*     progress =0L );

    public:
        HTTPClient();
        virtual ~HTTPClient();

    private:

        void readOptions( const osgDB::ReaderWriter::Options* options, std::string &proxy_host, std::string &proxy_port ) const;

        HTTPResponse doGet( const HTTPRequest&    request,
                            const osgDB::Options* options  =0L,
                            ProgressCallback*     callback =0L ) const;

        ReadResult doReadObject(
            const HTTPRequest&    request,
            const osgDB::Options* dbOptions,
            ProgressCallback*     progress );

        ReadResult doReadImage(
            const HTTPRequest&    request,
            const osgDB::Options* dbOptions,
            ProgressCallback*     progress );

        ReadResult doReadNode(
            const HTTPRequest&    request,
            const osgDB::Options* dbOptions,
            ProgressCallback*     progress );

        ReadResult doReadString(
            const HTTPRequest&    request,
            const osgDB::Options* dbOptions,
            ProgressCallback*     progress );

        /**
         * Convenience method for downloading a URL directly to a file
         */
        bool doDownload(const std::string& url, const std::string& filename);

    private:
        bool _initialized = false;
        void* _curl_handle = nullptr;
        long _simResponseCode = -1L;
        long _previousHttpAuthentication = 0L;

        osg::ref_ptr<Implementation> _impl;
        std::string _previousPassword;

        void initialize() const;
        void initializeImpl();

        static ImplementationFactory* _implFactory;

        static HTTPClient& getClient();
    };


    class OSGEARTH_EXPORT CURLHTTPImplementationFactory : public HTTPClient::ImplementationFactory
    {
    public:
        HTTPClient::Implementation* create() const;
    };

    class OSGEARTH_EXPORT WinInetHTTPImplementationFactory : public HTTPClient::ImplementationFactory
    {
    public:
        HTTPClient::Implementation* create() const;
    };
} }

#endif // OSGEARTH_HTTP_CLIENT_H
