///////////////////////////////////////////////////////////////////////////////
// Name:        wx/webrequest_curl.h
// Purpose:     wxWebRequest implementation using libcurl
// Author:      Tobias Taschner
// Created:     2018-10-25
// Copyright:   (c) 2018 wxWidgets development team
// Licence:     wxWindows licence
///////////////////////////////////////////////////////////////////////////////

#ifndef _WX_WEBREQUEST_CURL_H
#define _WX_WEBREQUEST_CURL_H

#if wxUSE_WEBREQUEST_CURL

#include "wx/private/webrequest.h"

#include "wx/thread.h"
#include "wx/vector.h"
#include "wx/timer.h"

#include "curl/curl.h"

#include <unordered_map>
#include <vector>

class wxWebRequestCURL;
class wxWebResponseCURL;
class wxWebSessionCURL;
class wxWebSessionSyncCURL;
class SocketPoller;

class wxWebAuthChallengeCURL : public wxWebAuthChallengeImpl
{
public:
    wxWebAuthChallengeCURL(wxWebAuthChallenge::Source source,
                           wxWebRequestCURL& request);

    void SetCredentials(const wxWebCredentials& cred) override;

private:
    wxWebRequestCURL& m_request;

    wxDECLARE_NO_COPY_CLASS(wxWebAuthChallengeCURL);
};

class wxWebRequestCURL : public wxWebRequestImpl
{
public:
    // Ctor for async requests: creates a new libcurl handle and owns it.
    wxWebRequestCURL(wxWebSession& session,
                     wxWebSessionCURL& sessionImpl,
                     wxEvtHandler* handler,
                     const wxString& url,
                     int id);

    // Ctor for sync requests: uses the libcurl handle from the session and
    // doesn't own it.
    wxWebRequestCURL(wxWebSessionSyncCURL& sessionImpl,
                     const wxString& url);

    ~wxWebRequestCURL();

    wxWebRequest::Result Execute() override;

    void Start() override;

    wxWebResponseImplPtr GetResponse() const override
        { return m_response; }

    wxWebAuthChallengeImplPtr GetAuthChallenge() const override
        { return m_authChallenge; }

    wxFileOffset GetBytesSent() const override;

    wxFileOffset GetBytesExpectedToSend() const override;

    CURL* GetHandle() const { return m_handle; }

    wxWebRequestHandle GetNativeHandle() const override
    {
        return (wxWebRequestHandle)GetHandle();
    }

    bool StartRequest();

    void HandleCompletion();

    wxString GetError() const;

    // Method called from libcurl callback
    size_t CURLOnRead(char* buffer, size_t size);

private:
    // Common initialization for sync and async requests performed when the
    // request is created.
    void DoStartPrepare(const wxString& url);

    // This function is again common for sync and async requests, but is called
    // right before starting, or executing, the request.
    //
    // If it returns result with State_Failed, the request should be aborted.
    wxWebRequest::Result DoFinishPrepare();

    // Convert the status of the completed request to our result structure and,
    // if necessary, initialize m_authChallenge.
    wxWebRequest::Result DoHandleCompletion();

    void DoCancel() override;

    // This is only used for async requests.
    wxWebSessionCURL* const m_sessionCURL;

    // This pointer is only owned by this object when using async requests.
    CURL* const m_handle;
    char m_errorBuffer[CURL_ERROR_SIZE];
    struct curl_slist *m_headerList = nullptr;
    wxObjectDataPtr<wxWebResponseCURL> m_response;
    wxObjectDataPtr<wxWebAuthChallengeCURL> m_authChallenge;
    wxFileOffset m_bytesSent;

    wxDECLARE_NO_COPY_CLASS(wxWebRequestCURL);
};

class wxWebResponseCURL : public wxWebResponseImpl
{
public:
    explicit wxWebResponseCURL(wxWebRequestCURL& request);

    wxFileOffset GetContentLength() const override;

    wxString GetURL() const override;

    wxString GetHeader(const wxString& name) const override;

    std::vector<wxString> GetAllHeaderValues(const wxString& name) const override;

    int GetStatus() const override;

    wxString GetStatusText() const override { return m_statusText; }


    // Methods called from libcurl callbacks
    size_t CURLOnWrite(void *buffer, size_t size);
    size_t CURLOnHeader(const char* buffer, size_t size);
    int CURLOnProgress(curl_off_t);

private:
    // We can receive multiple headers with the same name (classic example is
    // "Set-Cookie:"), so we can't use wxWebRequestHeaderMap here and need to
    // define our own "multi-map" for headers: it maps the header name to a
    // collection of all its values, possibly from multiple header lines.
    using AllHeadersMap = std::unordered_map<wxString, std::vector<wxString>>;
    AllHeadersMap m_headers;
    wxString m_statusText;
    wxFileOffset m_knownDownloadSize;

    CURL* GetHandle() const
    { return static_cast<wxWebRequestCURL&>(m_request).GetHandle(); }

    wxDECLARE_NO_COPY_CLASS(wxWebResponseCURL);
};

// Common base class for synchronous and asynchronous sessions.
class wxWebSessionBaseCURL : public wxWebSessionImpl
{
public:
    explicit wxWebSessionBaseCURL(Mode mode);
    ~wxWebSessionBaseCURL();

    wxVersionInfo GetLibraryVersionInfo() const override;

    static bool CurlRuntimeAtLeastVersion(unsigned int, unsigned int,
                                          unsigned int);

protected:
    static int ms_activeSessions;
    static unsigned int ms_runtimeVersion;
};

// Sync session implementation uses libcurl "easy" API.
class wxWebSessionSyncCURL : public wxWebSessionBaseCURL
{
public:
    wxWebSessionSyncCURL();
    ~wxWebSessionSyncCURL();

    wxWebRequestImplPtr
    CreateRequest(wxWebSession& WXUNUSED(session),
                  wxEvtHandler* WXUNUSED(handler),
                  const wxString& WXUNUSED(url),
                  int WXUNUSED(id)) override
    {
        wxFAIL_MSG("This method should not be called for synchronous sessions");

        return wxWebRequestImplPtr{};
    }

    wxWebRequestImplPtr
    CreateRequestSync(wxWebSessionSync& session, const wxString& url) override;

    wxWebSessionHandle GetNativeHandle() const override
    {
        return (wxWebSessionHandle)m_handle;
    }

    CURL* GetHandle() const { return m_handle; }

private:
    CURL* m_handle = nullptr;

    wxDECLARE_NO_COPY_CLASS(wxWebSessionSyncCURL);
};

// Async session implementation uses libcurl "multi" API.
class wxWebSessionCURL : public wxEvtHandler, public wxWebSessionBaseCURL
{
public:
    wxWebSessionCURL();

    ~wxWebSessionCURL();

    wxWebRequestImplPtr
    CreateRequest(wxWebSession& session,
                  wxEvtHandler* handler,
                  const wxString& url,
                  int id = wxID_ANY) override;

    wxWebRequestImplPtr
    CreateRequestSync(wxWebSessionSync& WXUNUSED(session),
                      const wxString& WXUNUSED(url)) override
    {
        wxFAIL_MSG("This method should not be called for asynchronous sessions");

        return wxWebRequestImplPtr{};
    }

    wxWebSessionHandle GetNativeHandle() const override
    {
        return (wxWebSessionHandle)m_handle;
    }

    bool StartRequest(wxWebRequestCURL& request);

    void CancelRequest(wxWebRequestCURL* request);

    void RequestHasTerminated(wxWebRequestCURL* request);

private:
    static int TimerCallback(CURLM*, long, void*);
    static int SocketCallback(CURL*, curl_socket_t, int, void*, void*);

    void ProcessTimerCallback(long);
    void TimeoutNotification(wxTimerEvent&);
    void ProcessTimeoutNotification();
    void ProcessSocketCallback(CURL*, curl_socket_t, int);
    void ProcessSocketPollerResult(wxThreadEvent&);
    void CheckForCompletedTransfers();
    void FailRequest(CURL*, const wxString&);
    void StopActiveTransfer(CURL*);
    void RemoveActiveSocket(CURL*);

    using TransferSet = std::unordered_map<CURL*, wxWebRequestCURL*>;
    using CurlSocketMap = std::unordered_map<CURL*, curl_socket_t>;

    TransferSet m_activeTransfers;
    CurlSocketMap m_activeSockets;

    SocketPoller* m_socketPoller = nullptr;
    wxTimer m_timeoutTimer;
    CURLM* m_handle = nullptr;

    wxDECLARE_NO_COPY_CLASS(wxWebSessionCURL);
};

class wxWebSessionFactoryCURL : public wxWebSessionFactory
{
public:
    wxWebSessionImpl* Create() override
    {
        return new wxWebSessionCURL();
    }

    wxWebSessionImpl* CreateSync() override
    {
        return new wxWebSessionSyncCURL();
    }
};

#endif // wxUSE_WEBREQUEST_CURL

#endif
