/*
 * Copyright (C) 2013 Apple Inc.  All rights reserved.
 * Copyright (C) 2018 Sony Interactive Entertainment Inc.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 */

#pragma once

#include "CurlProxySettings.h"
#include "CurlSSLHandle.h"

#include <wtf/Lock.h>
#include <wtf/NeverDestroyed.h>
#include <wtf/Noncopyable.h>
#include <wtf/Seconds.h>
#include <wtf/Threading.h>
#include <wtf/URL.h>

#if OS(WINDOWS)
#include <windows.h>
#include <winsock2.h>
#endif

#include <curl/curl.h>

namespace WebCore {

// Values taken from http://www.browserscope.org/ following
// the rule "Do What Every Other Modern Browser Is Doing".
const long CurlDefaultMaxConnects { -1 }; // -1 : Does not set CURLMOPT_MAXCONNECTS
const long CurlDefaultMaxTotalConnections { 17 };
const long CurlDefaultMaxHostConnections { 6 };

// CurlGlobal --------------------------------------------
// to make the initialization of libcurl happen before other initialization of CurlContext

class CurlGlobal {
protected:
    CurlGlobal()
    {
        curl_global_init(CURL_GLOBAL_ALL);
    }
    
    virtual ~CurlGlobal()
    {
        curl_global_cleanup();
    }
};

// CurlShareHandle --------------------------------------------

class CurlShareHandle {
    WTF_MAKE_NONCOPYABLE(CurlShareHandle);

public:
    CurlShareHandle();
    ~CurlShareHandle();

    CURLSH* handle() const { return m_shareHandle; }

private:
    static void lockCallback(CURL*, curl_lock_data, curl_lock_access, void*);
    static void unlockCallback(CURL*, curl_lock_data, void*);
    static Lock* mutexFor(curl_lock_data);

    CURLSH* m_shareHandle { nullptr };
};

// CurlContext --------------------------------------------

class CurlRequestScheduler;

class CurlContext : public CurlGlobal {
    WTF_MAKE_NONCOPYABLE(CurlContext);
    friend NeverDestroyed<CurlContext>;
public:
    WEBCORE_EXPORT static CurlContext& singleton();

    virtual ~CurlContext();

    const CurlShareHandle& shareHandle() { return m_shareHandle; }

    CurlRequestScheduler& scheduler() { return *m_scheduler; }

    // Proxy
    const CurlProxySettings& proxySettings() const { return m_proxySettings; }
    void setProxySettings(CurlProxySettings&& settings) { m_proxySettings = WTFMove(settings); }
    void setProxyUserPass(const String& user, const String& password) { m_proxySettings.setUserPass(user, password); }
    void setDefaultProxyAuthMethod() { m_proxySettings.setDefaultAuthMethod(); }
    void setProxyAuthMethod(long authMethod) { m_proxySettings.setAuthMethod(authMethod); }

    // SSL
    CurlSSLHandle& sslHandle() { return m_sslHandle; }

    // HTTP/2
    bool isHttp2Enabled() const;

    // Timeout
    Seconds dnsCacheTimeout() const { return m_dnsCacheTimeout; }
    Seconds connectTimeout() const { return m_connectTimeout; }
    Seconds defaultTimeoutInterval() const { return m_defaultTimeoutInterval; }

#ifndef NDEBUG
    FILE* getLogFile() const { return m_logFile; }
    bool isVerbose() const { return m_verbose; }
#endif

private:
    CurlContext();
    void initShareHandle();

    CurlProxySettings m_proxySettings;
    CurlShareHandle m_shareHandle;
    CurlSSLHandle m_sslHandle;
    std::unique_ptr<CurlRequestScheduler> m_scheduler;

    Seconds m_dnsCacheTimeout { Seconds::fromMinutes(5) };
    Seconds m_connectTimeout { 30.0 };
    Seconds m_defaultTimeoutInterval { 60.0 };

#ifndef NDEBUG
    FILE* m_logFile { nullptr };
    bool m_verbose { false };
#endif
};

// CurlMultiHandle --------------------------------------------

class CurlMultiHandle {
    WTF_MAKE_NONCOPYABLE(CurlMultiHandle);

public:
    CurlMultiHandle();
    ~CurlMultiHandle();

    void setMaxConnects(long);
    void setMaxTotalConnections(long);
    void setMaxHostConnections(long);

    CURLMcode addHandle(CURL*);
    CURLMcode removeHandle(CURL*);

    CURLMcode getFdSet(fd_set&, fd_set&, fd_set&, int&);
    CURLMcode perform(int&);
    CURLMsg* readInfo(int&);

private:
    CURLM* m_multiHandle { nullptr };
};

// CurlSList -------------------------------------------------

class CurlSList {
public:
    CurlSList() { }
    ~CurlSList() { clear(); }

    operator struct curl_slist** () { return &m_list; }

    const struct curl_slist* head() const { return m_list; }
    bool isEmpty() const { return !m_list; }
    void clear()
    {
        if (m_list) {
            curl_slist_free_all(m_list);
            m_list = nullptr;
        }
    }

    void append(const char* str) { m_list = curl_slist_append(m_list, str); }
    void append(const String& str) { append(str.latin1().data()); }

private:
    struct curl_slist* m_list { nullptr };
};

// CurlHandle -------------------------------------------------

class CertificateInfo;
class CurlSSLVerifier;
class HTTPHeaderMap;
class NetworkLoadMetrics;

class CurlHandle {
    WTF_MAKE_NONCOPYABLE(CurlHandle);

public:
    enum class VerifyPeer {
        Disable = 0L,
        Enable = 1L
    };

    enum class VerifyHost {
        LooseNameCheck = 0,
        StrictNameCheck = 2
    };

    CurlHandle();
    virtual ~CurlHandle();

    CURL* handle() const { return m_handle; }
    const URL& url() const { return m_url; }

    CURLcode perform();
    CURLcode pause(int);

    static const String errorDescription(CURLcode);

    void enableShareHandle();

    void setUrl(const URL&);
    void enableSSLForHost(const String&);

    void appendRequestHeaders(const HTTPHeaderMap&);
    void appendRequestHeader(const String& name, const String& value);
    void appendRequestHeader(const String& name);
    void removeRequestHeader(const String& name);

    void enableHttp();
    void enableHttpGetRequest();
    void enableHttpHeadRequest();
    void enableHttpPostRequest();
    void setPostFields(const char*, long);
    void setPostFieldLarge(curl_off_t);
    void enableHttpPutRequest();
    void setInFileSizeLarge(curl_off_t);
    void setHttpCustomRequest(const String&);

    void enableConnectionOnly();

    void enableAcceptEncoding();
    void enableAllowedProtocols();

    void setHttpAuthUserPass(const String&, const String&, long authType = CURLAUTH_ANY);

    void disableServerTrustEvaluation();
    void setCACertPath(const char*);
    void setSslVerifyPeer(VerifyPeer);
    void setSslVerifyHost(VerifyHost);
    void setSslCert(const char*);
    void setSslCertType(const char*);
    void setSslKeyPassword(const char*);
    void setSslCipherList(const char*);

    void enableProxyIfExists();

    void setDnsCacheTimeout(Seconds);
    void setConnectTimeout(Seconds);
    void setTimeout(Seconds);

    // Callback function
    void setHeaderCallbackFunction(curl_write_callback, void*);
    void setWriteCallbackFunction(curl_write_callback, void*);
    void setReadCallbackFunction(curl_read_callback, void*);
    void setSslCtxCallbackFunction(curl_ssl_ctx_callback, void*);
    void setDebugCallbackFunction(curl_debug_callback, void*);

    // Status
    Optional<String> getProxyUrl();
    Optional<long> getResponseCode();
    Optional<long> getHttpConnectCode();
    Optional<long long> getContentLength();
    Optional<long> getHttpAuthAvail();
    Optional<long> getProxyAuthAvail();
    Optional<long> getHttpVersion();
    Optional<NetworkLoadMetrics> getNetworkLoadMetrics(const WTF::Seconds& domainLookupStart);
    void addExtraNetworkLoadMetrics(NetworkLoadMetrics&);

    int sslErrors() const;
    Optional<CertificateInfo> certificateInfo() const;

    static long long maxCurlOffT();

#ifndef NDEBUG
    void enableVerboseIfUsed();
    void enableStdErrIfUsed();
#endif

private:
    void enableRequestHeaders();
    static int expectedSizeOfCurlOffT();

    static CURLcode willSetupSslCtxCallback(CURL*, void* sslCtx, void* userData);
    CURLcode willSetupSslCtx(void* sslCtx);

    CURL* m_handle { nullptr };
    char m_errorBuffer[CURL_ERROR_SIZE] { };

    URL m_url;
    CurlSList m_requestHeaders;
    std::unique_ptr<CurlSSLVerifier> m_sslVerifier;
};

class CurlSocketHandle : public CurlHandle {
    WTF_MAKE_NONCOPYABLE(CurlSocketHandle);

public:
    struct WaitResult {
        bool readable { false };
        bool writable { false };
    };

    CurlSocketHandle(const URL&, Function<void(CURLcode)>&& errorHandler);

    bool connect();
    size_t send(const uint8_t*, size_t);
    Optional<size_t> receive(uint8_t*, size_t);
    Optional<WaitResult> wait(const Seconds& timeout, bool alsoWaitForWrite);

private:
    Function<void(CURLcode)> m_errorHandler;
};

} // namespace WebCore
