﻿/**********************************************************
 * Author(作者)     : Qt君
 * 微信公众号        : Qt君
 * Website(网站)    : qthub.com
 * QQ交流群         : 1039852727
 * Email(邮箱)      : 2088201923@qq.com
 * Support(技术支持&合作) :2088201923(QQ)
 * Source Code(源码): https://github.com/aeagean/QtNetworkService
 * LISCENSE(开源协议): MIT
 * Demo(演示):
 ==========================================================
   static AeaQt::HttpClient client;
   client.get("https://qthub.com")
         .onSuccess([](QString result) { qDebug()<<"success!"; })
         .onFailed([](QString error) { qDebug()<<"failed!"; })
         .exec();
 ==========================================================
**********************************************************/
#ifndef QTHUB_COM_HTTPCLIENT_HPP
#define QTHUB_COM_HTTPCLIENT_HPP

#include <QRegularExpression>
#include <QNetworkReply>
#include <QHttpMultiPart>
#include <QAuthenticator>

#include <QJsonObject>
#include <QJsonDocument>

#include <QBuffer>
#include <QMetaEnum>
#include <QUrlQuery>
#include <QFileInfo>

#include <QTimer>
#include <QEventLoop>
#include <QDebug>

#include <functional>

namespace AeaQt {

class HttpClient;
class HttpRequest;
class HttpResponse;

class HttpClient : public QNetworkAccessManager
{
    Q_OBJECT
public:
    inline static HttpClient *instance();
    inline HttpClient(QObject *parent = nullptr);
    inline QString getVersion() {return "1.1.2";}

    inline HttpRequest head(const QString &url);
    inline HttpRequest get(const QString &url);
    inline HttpRequest post(const QString &url);
    inline HttpRequest put(const QString &url);

    inline HttpRequest send(const QString &url, Operation op = GetOperation);

private:
#if (QT_VERSION < QT_VERSION_CHECK(5, 8, 0))
    inline QNetworkReply *sendCustomRequest(const QNetworkRequest &request, const QByteArray &verb, const QByteArray &data);
    inline QNetworkReply *sendCustomRequest(const QNetworkRequest &request, const QByteArray &verb, QHttpMultiPart *multiPart);
#endif
    friend class HttpRequest;
};

class HttpRequest
{
public:
    enum LogLevel { Off, Fatal, Error, Warn, Debug, Info, Trace, All};

    inline explicit HttpRequest(QNetworkAccessManager::Operation op, HttpClient *httpClient);
    inline virtual ~HttpRequest();

    inline HttpRequest &url(const QString &url);

    inline HttpRequest &header(const QString &key, const QVariant &value);
    inline HttpRequest &header(QNetworkRequest::KnownHeaders header, const QVariant &value);
    inline HttpRequest &headers(const QMap<QString, QVariant> &headers);
    inline HttpRequest &headers(const QMap<QNetworkRequest::KnownHeaders, QVariant> &headers);

    inline HttpRequest &queryParam(const QString &key, const QVariant &value);
    inline HttpRequest &queryParams(const QMap<QString, QVariant> &params);

    /* Mainly used for identification */
    inline HttpRequest &userAttribute(const QVariant &value);
    inline HttpRequest &attribute(QNetworkRequest::Attribute attribute, const QVariant &value);

    inline HttpRequest &body(const QByteArray &raw);
    inline HttpRequest &bodyWithRaw(const QByteArray &raw);

    inline HttpRequest &body(const QJsonObject &json);
    inline HttpRequest &bodyWithJson(const QJsonObject &json);

    inline HttpRequest &body(const QVariantMap &formUrlencodedMap);
    inline HttpRequest &bodyWithFormUrlencoded(const QString &key, const QVariant &value);
    inline HttpRequest &bodyWithFormUrlencoded(const QVariantMap &keyValueMap);

    inline HttpRequest &bodyWithFormData(const QString &key, const QVariant &value);
    inline HttpRequest &bodyWithFormData(const QVariantMap &keyValueMap);

    inline HttpRequest &body(QHttpMultiPart *multiPart);
    inline HttpRequest &bodyWithMultiPart(QHttpMultiPart *multiPart);

    // multi-params
    inline HttpRequest &body(const QString &key, const QString &file);
    inline HttpRequest &bodyWithFile(const QString &key, const QString &file);
    inline HttpRequest &bodyWithFile(const QMap<QString/*key*/, QString/*file*/> &fileMap); // => QMap<key, file>; like: { "key": "/home/example/car.jpeg" }

    inline HttpRequest &ignoreSslErrors(const QList<QSslError> &errors);
    inline HttpRequest &sslConfiguration(const QSslConfiguration &config);

    inline HttpRequest &priority(QNetworkRequest::Priority priority);
    inline HttpRequest &maximumRedirectsAllowed(int maxRedirectsAllowed);
    inline HttpRequest &originatingObject(QObject *object);
    inline HttpRequest &readBufferSize(qint64 size);

    // Authentication
    inline HttpRequest &autoAuthenticationRequired(const QAuthenticator &authenticator);
    inline HttpRequest &autoAuthenticationRequired(const QString &user, const QString &password);

    // 超过身份验证计数则触发失败并中断请求。
    // count >= 0 => count
    // count < 0 => infinite
    inline HttpRequest &authenticationRequiredCount(int count = 1);

    /**
     * @brief msec <= 0, disable timeout
     *        msec >  0, enable timeout
     */
    inline HttpRequest &timeout(const int &second = -1);
    inline HttpRequest &timeoutMs(const int &msec = -1);

    inline HttpRequest &download();
    inline HttpRequest &download(const QString &file);
    inline HttpRequest &enabledBreakpointDownload(bool enabled = true);

    inline HttpRequest &retry(int count);
    inline HttpRequest &repeat(int count);

    /**
     * @brief Block(or sync) current thread, entering an event loop.
     */
    inline HttpRequest &block(bool isBlock = true);
    inline HttpRequest &sync(bool isSync = true);

    inline HttpRequest &logLevel(LogLevel level = Warn);

    inline HttpResponse *exec();

    // onFinished == onSuccess
    inline HttpRequest &onSuccess(const QObject *receiver, const char *method);
    inline HttpRequest &onSuccess(std::function<void (QNetworkReply*)> lambda);
    inline HttpRequest &onSuccess(std::function<void (QVariantMap)> lambda);
    inline HttpRequest &onSuccess(std::function<void (QJsonObject)> lambda);
    inline HttpRequest &onSuccess(std::function<void (QByteArray)> lambda);
    inline HttpRequest &onSuccess(std::function<void ()> lambda);

    // onFinished == onSuccess
    inline HttpRequest &onFinished(const QObject *receiver, const char *method);
    inline HttpRequest &onFinished(std::function<void (QNetworkReply*)> lambda);
    inline HttpRequest &onFinished(std::function<void (QVariantMap)> lambda);
    inline HttpRequest &onFinished(std::function<void (QJsonObject)> lambda);
    inline HttpRequest &onFinished(std::function<void (QByteArray)> lambda);
    inline HttpRequest &onFinished(std::function<void ()> lambda);

    // onError == onFailed
    inline HttpRequest &onError(const QObject *receiver, const char *method);
    inline HttpRequest &onError(std::function<void (QNetworkReply*)> lambda);
    inline HttpRequest &onError(std::function<void (QNetworkReply::NetworkError)> lambda);
    inline HttpRequest &onError(std::function<void (QByteArray)> lambda);
    inline HttpRequest &onError(std::function<void ()> lambda);

    // onError == onFailed
    inline HttpRequest &onFailed(const QObject *receiver, const char *method);
    inline HttpRequest &onFailed(std::function<void (QNetworkReply*)> lambda);
    inline HttpRequest &onFailed(std::function<void (QNetworkReply::NetworkError)> lambda);
    inline HttpRequest &onFailed(std::function<void (QByteArray)> lambda);
    inline HttpRequest &onFailed(std::function<void ()> lambda);

    inline HttpRequest &onReadyRead(const QObject *receiver, const char *method);
    inline HttpRequest &onReadyRead(std::function<void (QNetworkReply*)> lambda);

    inline HttpRequest &onHead(const QObject *receiver, const char *method);
    inline HttpRequest &onHead(std::function<void (QList<QNetworkReply::RawHeaderPair>)> lambda);
    inline HttpRequest &onHead(std::function<void (QMap<QString, QString>)> lambda);

    inline HttpRequest &onTimeout(const QObject *receiver, const char *method);
    inline HttpRequest &onTimeout(std::function<void (QNetworkReply*)> lambda);
    inline HttpRequest &onTimeout(std::function<void ()> lambda);

    inline HttpRequest &onUploadProgress(const QObject *receiver, const char *method);
    inline HttpRequest &onUploadProgress(std::function<void (qint64, qint64)> lambda);

    inline HttpRequest &onDownloadProgress(const QObject *receiver, const char *method);
    inline HttpRequest &onDownloadProgress(std::function<void (qint64, qint64)> lambda);

    /// file download interface
    inline HttpRequest &onDownloadFileNameChanged(const QObject *receiver, const char *method);
    inline HttpRequest &onDownloadFileNameChanged(std::function<void (QString/*fileName*/)> lambda);

    inline HttpRequest &onDownloadFileProgress(const QObject *receiver, const char *method);
    inline HttpRequest &onDownloadFileProgress(std::function<void (qint64, qint64)> lambda);

    inline HttpRequest &onDownloadFileSuccess(const QObject *receiver, const char *method);
    inline HttpRequest &onDownloadFileSuccess(std::function<void ()> lambda);
    inline HttpRequest &onDownloadFileSuccess(std::function<void (QString/*fileName*/)> lambda);

    inline HttpRequest &onDownloadFileFailed(const QObject *receiver, const char *method);
    inline HttpRequest &onDownloadFileFailed(std::function<void ()> lambda);
    inline HttpRequest &onDownloadFileFailed(std::function<void (QString/*fileName*/)> lambda);
    /// file download interface

    inline HttpRequest &onRetried(const QObject *receiver, const char *method);
    inline HttpRequest &onRetried(std::function<void ()> lambda);

    inline HttpRequest &onRepeated(const QObject *receiver, const char *method);
    inline HttpRequest &onRepeated(std::function<void ()> lambda);

    inline HttpRequest &onAuthenticationRequired(const QObject *receiver, const char *method);
    inline HttpRequest &onAuthenticationRequired(std::function<void (QAuthenticator *)> lambda);

    inline HttpRequest &onAuthenticationRequireFailed(const QObject *receiver, const char *method);
    inline HttpRequest &onAuthenticationRequireFailed(std::function<void ()> lambda);
    inline HttpRequest &onAuthenticationRequireFailed(std::function<void (QNetworkReply *)> lambda);

    // onResponse == onSuccess. note: DEPRECATED
    inline HttpRequest &onResponse(const QObject *receiver, const char *method);
    inline HttpRequest &onResponse(std::function<void (QNetworkReply*)> lambda);
    inline HttpRequest &onResponse(std::function<void (QVariantMap)> lambda);
    inline HttpRequest &onResponse(std::function<void (QByteArray)> lambda);

    enum HandleType {
        h_onFinished = 0,
        h_onError,
        h_onDownloadProgress,
        h_onUploadProgress,
        h_onTimeout,
        h_onReadyRead,
        h_onDownloadFileSuccess,
        h_onDownloadFileFailed,
        h_onEncrypted,
        h_onMetaDataChanged,
        h_onPreSharedKeyAuthenticationRequired,
        h_onRedirectAllowed,
        h_onRedirected,
        h_onSslErrors,
        h_onRetried,
        h_onRepeated,
        h_onAuthenticationRequired,
        h_onAuthenticationRequireFailed,
        h_onHead,
        h_onDownloadFileProgess,
        h_onDownloadFileNameChanged,
    };

    enum BodyType
    {
        None = 0,              // This request does not have a body.
        Raw,
        Raw_Json,              // application/json
        X_Www_Form_Urlencoded, // x-www-form-urlencoded
        FileMap,               // multipart/form-data
        MultiPart,             // multipart/form-data
        FormData               // multipart/form-data
    };


protected:
    struct Downloader
    {
        bool    isEnabled;
        QString fileName;
        bool    enabledBreakpointDownload;
        bool    isSupportBreakpointDownload;
        qint64  currentSize;
        qint64  totalSize;

        Downloader()
        {
            isEnabled = false;
            fileName = "";
            enabledBreakpointDownload = true;
            isSupportBreakpointDownload = false;
            totalSize = 0;
            currentSize = 0;
        }
    };

    QNetworkAccessManager::Operation m_op;
    HttpClient                      *m_httpClient = nullptr;
    QNetworkReply                   *m_reply = nullptr;
    QNetworkRequest                  m_request;

    QPair<BodyType, QVariant>        m_body = qMakePair(BodyType::None, QByteArray());
    int                              m_timeoutMs = -1; // ms
    bool                             m_isBlock = false;
    int                              m_retryCount = 0;
    bool                             m_enabledRetry = false;
    int                              m_repeatCount = 1;
    QAuthenticator                   m_authenticator;
    int                              m_authenticationRequiredCount = 1;

    qint64                                              m_readBufferSize = -1;
    QList<QSslError>                                    m_ignoreSslErrors;
    Downloader                                          m_downloader;
    QMap<HandleType, QList<QPair<QString, QVariant> > > m_handleMap;
    QVariantMap m_formUrlencodedMap;
    QVariantMap m_formDataMap;
    LogLevel m_logLevel = Warn;

protected:
    inline QString toString();

    inline HttpRequest &enabledRetry(bool isEnabled);
    inline HttpResponse *exec(const HttpRequest &httpRequest, HttpResponse *httpResponse = nullptr);

    friend class HttpResponse;
    friend class HttpDownloader;

private:
    inline HttpRequest() = delete;
    inline HttpRequest &onResponse(HandleType type, const QObject *receiver, const char *method);
    inline HttpRequest &onResponse(HandleType type, QVariant lambda);
    inline HttpRequest &onResponse(HandleType type, QString key, QVariant value);
};

class HttpResponse : public QObject
{
    Q_OBJECT
public:
    inline explicit HttpResponse(const HttpRequest &httpRequest, QObject *parent = nullptr);
    inline virtual ~HttpResponse();

    inline void setHttpRequest(const HttpRequest &httpRequest);
    inline QNetworkReply *reply() { return m_httpRequest.m_reply; }
    inline QString toString() const;
signals:
    void replyChanged(QNetworkReply *reply);
    void finished(QNetworkReply *reply);
    void finished();
    void finished(QByteArray result);
    void finished(QString result);
    void finished(QVariantMap resultMap);
    void finished(QJsonObject resultJsonObject);

    void downloadProgress(qint64, qint64);
    void uploadProgress(qint64, qint64);

    void error(QByteArray error);
    void error(QString error);
    void error();
    void error(QNetworkReply::NetworkError error);
    void error(QNetworkReply *reply);

    void timeout();
    void timeout(QNetworkReply *reply);

    void readyRead(QNetworkReply *reply);

    void downloadFileNameChanged(QString fileName);

    void downloadFileFinished();
    void downloadFileFinished(QString file);

    void downloadFileError();
    void downloadFileError(QString errorString);

    void encrypted();
    void metaDataChanged();

    void preSharedKeyAuthenticationRequired(QSslPreSharedKeyAuthenticator *authenticator);
    void redirectAllowed();
    void redirected(QUrl url);
    void sslErrors(QList<QSslError> errors);

    void retried();
    void repeated();

    void authenticationRequired(QAuthenticator *authentication);
    void authenticationRequireFailed();
    void authenticationRequireFailed(QNetworkReply *);

    void head(QList<QNetworkReply::RawHeaderPair>);
    void head(QMap<QString, QString>);

    void downloadFileProgress(qint64 bytesReceived, qint64 bytesTotal);

private slots:
    inline void onFinished();
    inline void onError(QNetworkReply::NetworkError error);
    inline void onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal);
    inline void onUploadProgress(qint64 bytesSent, qint64 bytesTotal);
    inline void onTimeout();
    inline void onReadyRead();
    inline void onReadOnceReplyHeader();

    inline void onEncrypted();
    inline void onMetaDataChanged();
    inline void onPreSharedKeyAuthenticationRequired(QSslPreSharedKeyAuthenticator *authenticator);
    inline void onRedirectAllowed();
    inline void onRedirected(const QUrl &url);
    inline void onSslErrors(const QList<QSslError> &errors);

    inline void onAuthenticationRequired(QNetworkReply *reply, QAuthenticator *authenticator);

    inline void onHandleHead();

private:
    HttpRequest         m_httpRequest;
    QFile               m_downloadFile;
    int                 m_retriesRemaining = 0;
    int                 m_authenticationCount = 0;
    bool                m_isHandleHead = false;
};

inline QString lineIndent(const QString &source, const QString &indentString);
inline QString networkOperation2String(QNetworkAccessManager::Operation o);
inline QString networkHeader2String(const QNetworkRequest &request);
inline QString networkBody2String(const QPair<HttpRequest::BodyType, QVariant> &body);
inline QString networkReplyHeader2String(const QNetworkReply *reply);

class HttpDownloader : public QObject
{
    Q_OBJECT
    HttpRequest m_httpRequest;
    bool m_isSupportContinueDownload = false;
    QString m_fileName;
    qint64 m_contentLength = 0;
    QList<QNetworkReply::RawHeaderPair> m_headerForPair;
    QMap<QString, QString> m_headerForMap;
    HttpResponse *m_response = nullptr;

public:
    HttpDownloader(const HttpRequest &httpRequest, QObject *parent) : m_httpRequest(httpRequest), QObject(parent)
    {
        m_fileName = this->m_httpRequest.m_request.url().fileName();
        if (m_fileName.isEmpty()) {
            m_fileName = this->m_httpRequest.m_request.url().host();
        }
    }

    virtual ~HttpDownloader() { }

    HttpResponse *exec()
    {
        // todo: fixme
        HttpClient &client = *HttpClient::instance();
        client.get(m_httpRequest.m_request.url().toString())
              .timeout(30)
              .block(m_httpRequest.m_isBlock)
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
              .attribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::ManualRedirectPolicy) // Qt 6
#else
              .attribute(QNetworkRequest::FollowRedirectsAttribute, true)
#endif
              .onHead(this, SLOT(onHead(QList<QNetworkReply::RawHeaderPair>)))
              .onHead(this, SLOT(onHead(QMap<QString,QString>)))
              .onReadyRead(this, SLOT(onReadyRead(QNetworkReply*)))
              .onFailed(this, SLOT(onResponse(QNetworkReply::NetworkError)))
              .exec();

        m_response = new HttpResponse(m_httpRequest, nullptr);
        return m_response;
    }

private slots:
    void onResponse(QNetworkReply::NetworkError)
    {
        HttpResponse *response = m_httpRequest.exec(m_httpRequest, m_response);
        this->setParent(response);
    }

    void onHead(QList<QNetworkReply::RawHeaderPair> headerForPair)
    {
        m_headerForPair = headerForPair;
    }

    void onHead(QMap<QString, QString> headerForMap)
    {
        m_headerForMap = headerForMap;
        for (auto each: m_headerForMap.toStdMap()) {
            QString key = each.first;
            QString value = each.second;

            if (key.contains("Content-Disposition", Qt::CaseInsensitive)) {
                QString dispositionHeader = value;
                // fixme rx
                QRegularExpression rx("attachment;\\s*filename=([\\S]+)");
                QRegularExpressionMatch match = rx.match(dispositionHeader);
                if (match.hasMatch()) {
                    m_fileName = match.captured(1);
                }
            }

            if (key.contains("Content-Length", Qt::CaseInsensitive)) {
                m_contentLength = value.toLongLong();
            }

            if (key.contains("Content-Range", Qt::CaseInsensitive) ||
                    key.contains("Accept-Ranges", Qt::CaseInsensitive)) {
                m_isSupportContinueDownload = true;
            }
        }
    }

    void onReadyRead(QNetworkReply *reply)
    {
        HttpResponse *response = new HttpResponse(m_httpRequest, m_httpRequest.m_reply);
        if (m_httpRequest.m_downloader.fileName.isEmpty()) {
            m_httpRequest.m_downloader.fileName = m_fileName;
            emit response->downloadFileNameChanged(m_fileName);
        }

        m_httpRequest.m_downloader.totalSize = m_contentLength;
        m_httpRequest.m_downloader.isSupportBreakpointDownload = m_isSupportContinueDownload;

        if (m_httpRequest.m_downloader.enabledBreakpointDownload &&
            m_httpRequest.m_downloader.isSupportBreakpointDownload)
        {
            QFile file(m_httpRequest.m_downloader.fileName);
            if (file.exists() && file.open(QIODevice::ReadOnly)) {
                m_httpRequest.m_downloader.currentSize = file.size();

                if (file.size() == m_contentLength) {
                    emit response->head(m_headerForPair);
                    emit response->head(m_headerForMap);

                    emit response->downloadFileProgress(m_contentLength, m_contentLength);

                    emit response->downloadFileFinished();
                    emit response->downloadFileFinished(m_httpRequest.m_downloader.fileName);

                    emit response->finished();
                    emit response->finished(QString(""));
                    emit response->finished(QByteArray(""));
                    emit response->finished(QVariantMap{});
                    emit response->finished(nullptr);

                    response->deleteLater();
                }
                else if (file.size() > m_contentLength) {
                    file.close();
                    // Clear file content
                    file.open(QIODevice::Truncate);
                    file.close();
                }
                else {
                    m_httpRequest.m_request.setRawHeader("Range", QString("bytes=%1-").arg(file.size()).toUtf8());
                }
            }
        }

        response->deleteLater();
        reply->abort();
    }
};

class HttpResponseTimeout : public QObject
{
    Q_OBJECT
public:
    HttpResponseTimeout(HttpResponse *parent, const int timeout = -1) : QObject(parent)
    {
        if (timeout > 0) {
            QTimer::singleShot(timeout, parent, SLOT(onTimeout()));
        }
        else {
            // do nothing
        }
    }
};

class HttpBlocker : public QEventLoop
{
    Q_OBJECT
public:
    HttpBlocker(QNetworkReply *reply) : QEventLoop(nullptr)
    {
        if (reply) {
            connect(reply, SIGNAL(finished()), this, SLOT(quit()));
            this->exec();
        }
        this->deleteLater();
    }
};

#define _logger(l1, l2, str) \
do { \
    if (l1 >= l2) { \
        if (l2 >= HttpRequest::Debug) { \
            qDebug().noquote() << str; \
        } \
        else if (l2 == HttpRequest::Warn) { \
            qWarning().noquote() << str; \
        } \
        else if (l2 == HttpRequest::Error) { \
            qCritical().noquote() << str; \
        } \
        else if (l2 == HttpRequest::Fatal) { \
            qFatal("%s\n", str); \
        } \
    } \
} while(0);

#define printTrace(level, str) _logger(level, HttpRequest::Trace, str)
#define printInfo(level, str)  _logger(level, HttpRequest::Info,  str)
#define printDebug(level, str) _logger(level, HttpRequest::Debug, str)
#define printWarn(level, str)  _logger(level, HttpRequest::Warn,  str)
#define printError(level, str) _logger(level, HttpRequest::Error, str)
#define printFatal(level, str) _logger(level, HttpRequest::Fatal, str)

HttpRequest::~HttpRequest()
{
}

HttpRequest::HttpRequest(QNetworkAccessManager::Operation op, HttpClient *httpClient)
{
    m_op = op;
    m_httpClient = httpClient;
}

HttpRequest &HttpRequest::url(const QString &url)
{
    m_request.setUrl(QUrl(url));
    return *this;
}

HttpRequest &HttpRequest::header(QNetworkRequest::KnownHeaders header, const QVariant &value)
{
    m_request.setHeader(header, value);
    return *this;
}

HttpRequest &HttpRequest::headers(const QMap<QNetworkRequest::KnownHeaders, QVariant> &headers)
{
   QMapIterator<QNetworkRequest::KnownHeaders, QVariant> iter(headers);
   while (iter.hasNext()) {
       iter.next();
       header(iter.key(), iter.value());
   }

   return *this;
}

HttpRequest &HttpRequest::header(const QString &key, const QVariant &value)
{
    m_request.setRawHeader(QByteArray(key.toStdString().data()), QByteArray(value.toString().toStdString().data()));
    return *this;
}

HttpRequest &HttpRequest::headers(const QMap<QString, QVariant> &headers)
{
   QMapIterator<QString, QVariant> iter(headers);
   while (iter.hasNext()) {
       iter.next();
       header(iter.key(), iter.value());
   }

   return *this;
}

HttpRequest &HttpRequest::body(const QVariantMap &keyValueMap)
{
    return bodyWithFormUrlencoded(keyValueMap);
}

HttpRequest &HttpRequest::bodyWithFormUrlencoded(const QString &key, const QVariant &value)
{
    QVariantMap map;
    map[key] = value;

    return bodyWithFormUrlencoded(map);
}

HttpRequest &HttpRequest::bodyWithFormUrlencoded(const QVariantMap &keyValueMap)
{
    // merge map
    for (auto each : keyValueMap.toStdMap()) {
        m_formUrlencodedMap[each.first] = each.second;
    }

    QString value;
    QMapIterator<QString, QVariant> i(m_formUrlencodedMap);
    while (i.hasNext()) {
        i.next();

        value += QString("%1=%2")
                .arg(QUrl::toPercentEncoding(i.key()).data())
                .arg(QUrl::toPercentEncoding(i.value().toString()).data());
        if (i.hasNext()) {
            value += "&";
        }
    }

    m_body = qMakePair(X_Www_Form_Urlencoded, value);
    m_request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");

    return *this;
}

HttpRequest &HttpRequest::bodyWithFormData(const QString &key, const QVariant &value)
{
    QVariantMap map;
    map[key] = value;

    return bodyWithFormData(map);
}

HttpRequest &HttpRequest::bodyWithFormData(const QVariantMap &keyValueMap)
{
    // merge map
    for (auto each : keyValueMap.toStdMap()) {
        m_formDataMap[each.first] = each.second;
    }

    m_body = qMakePair(FormData, m_formDataMap);
    m_request.setHeader(QNetworkRequest::ContentTypeHeader, "multipart/form-data");
    return *this;
}

HttpRequest &HttpRequest::body(const QJsonObject &json)
{
    return bodyWithJson(json);
}

HttpRequest &HttpRequest::bodyWithJson(const QJsonObject &json)
{
    const QByteArray &value = QJsonDocument(json).toJson();
    m_body = qMakePair(Raw_Json, value);
    m_request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");

    return *this;
}

HttpRequest &HttpRequest::body(const QByteArray &raw)
{
    return bodyWithRaw(raw);
}

HttpRequest &HttpRequest::bodyWithRaw(const QByteArray &raw)
{
    m_body = qMakePair(Raw, raw);
    return *this;
}

HttpRequest &HttpRequest::body(QHttpMultiPart *multiPart)
{
    return bodyWithMultiPart(multiPart);
}

HttpRequest &HttpRequest::bodyWithMultiPart(QHttpMultiPart *multiPart)
{
    m_body = qMakePair(MultiPart, QVariant::fromValue(multiPart));
    return *this;
}

HttpRequest &HttpRequest::download()
{
    return download("");
}

HttpRequest &HttpRequest::download(const QString &file)
{
#if (QT_VERSION >= QT_VERSION_CHECK(5, 9, 0))
    this->attribute(QNetworkRequest::RedirectPolicyAttribute, true);
#else
    this->attribute(QNetworkRequest::FollowRedirectsAttribute, true);
#endif

    m_downloader.isEnabled = true;
    m_downloader.fileName = file;
    return *this;
}

HttpRequest &HttpRequest::enabledBreakpointDownload(bool enabled)
{
    m_downloader.enabledBreakpointDownload = enabled;
    return *this;
}

HttpRequest &HttpRequest::onDownloadFileProgress(const QObject *receiver, const char *method)
{
    return onResponse(h_onDownloadFileProgess, receiver, method);
}

HttpRequest &HttpRequest::onDownloadFileProgress(std::function<void (qint64, qint64)> lambda)
{
    return onResponse(h_onDownloadFileProgess, QVariant::fromValue(lambda));
}

HttpRequest &HttpRequest::body(const QString &key, const QString &file)
{
    return bodyWithFile(key, file);
}

HttpRequest &HttpRequest::bodyWithFile(const QString &key, const QString &filePath)
{
    QMap<QString, QString> map;
    map[key] = filePath;

    return bodyWithFile(map);
}

HttpRequest &HttpRequest::bodyWithFile(const QMap<QString, QString> &fileMap)
{
    auto &body = m_body;
    auto map = body.second.value<QMap<QString, QString>>();
    for (auto each : fileMap.toStdMap()) {
        map[each.first] = each.second;
    }

    body.first = BodyType::FileMap;
    body.second = QVariant::fromValue(map);
    return *this;
}

HttpRequest &HttpRequest::ignoreSslErrors(const QList<QSslError> &errors)
{
    m_ignoreSslErrors = errors;
    return *this;
}

HttpRequest &HttpRequest::sslConfiguration(const QSslConfiguration &config)
{
    m_request.setSslConfiguration(config);
    return *this;
}

HttpRequest &HttpRequest::priority(QNetworkRequest::Priority priority)
{
    m_request.setPriority(priority);
    return *this;
}

HttpRequest &HttpRequest::maximumRedirectsAllowed(int maxRedirectsAllowed)
{
    m_request.setMaximumRedirectsAllowed(maxRedirectsAllowed);
    return *this;
}

HttpRequest &HttpRequest::originatingObject(QObject *object)
{
    m_request.setOriginatingObject(object);
    return *this;
}

HttpRequest &HttpRequest::readBufferSize(qint64 size)
{
    m_readBufferSize = size;
    return *this;
}

HttpRequest &HttpRequest::autoAuthenticationRequired(const QAuthenticator &authenticator)
{
    m_authenticator = authenticator;
    return *this;
}

HttpRequest &HttpRequest::autoAuthenticationRequired(const QString &user, const QString &password)
{
    QAuthenticator a;
    a.setUser(user);
    a.setPassword(password);

    return autoAuthenticationRequired(a);
}

HttpRequest &HttpRequest::authenticationRequiredCount(int count)
{
    m_authenticationRequiredCount = count;
    return *this;
}

HttpRequest &HttpRequest::timeout(const int &second) { return timeoutMs(second * 1000); }
HttpRequest &HttpRequest::timeoutMs(const int &msec) { m_timeoutMs = msec; return *this; }

HttpRequest &HttpRequest::retry(int count) { m_retryCount = count; return *this; }

HttpRequest &HttpRequest::repeat(int count) { m_repeatCount = count; return *this; }

HttpRequest &HttpRequest::block(bool isBlock) { m_isBlock = isBlock; return *this; }
HttpRequest &HttpRequest::sync(bool isSync) { return block(isSync); }

HttpRequest &HttpRequest::logLevel(HttpRequest::LogLevel level) { m_logLevel = level; return *this; }

HttpRequest &HttpRequest::onFinished(const QObject *receiver, const char  *method) { return onResponse(h_onFinished, receiver, method); }
HttpRequest &HttpRequest::onFinished(std::function<void (QNetworkReply *)> lambda) { return onResponse(h_onFinished, QVariant::fromValue(lambda)); }
HttpRequest &HttpRequest::onFinished(std::function<void (QVariantMap)>     lambda) { return onResponse(h_onFinished, QVariant::fromValue(lambda)); }
HttpRequest &HttpRequest::onFinished(std::function<void (QJsonObject)>     lambda) { return onResponse(h_onFinished, QVariant::fromValue(lambda)); }
HttpRequest &HttpRequest::onFinished(std::function<void (QByteArray)>      lambda) { return onResponse(h_onFinished, QVariant::fromValue(lambda)); }
HttpRequest &HttpRequest::onFinished(std::function<void ()>                lambda) { return onResponse(h_onFinished, QVariant::fromValue(lambda)); }

HttpRequest &HttpRequest::onSuccess(const QObject *receiver, const char  *method) { return onFinished(receiver, method); }
HttpRequest &HttpRequest::onSuccess(std::function<void (QNetworkReply *)> lambda) { return onFinished(lambda); }
HttpRequest &HttpRequest::onSuccess(std::function<void (QVariantMap)>     lambda) { return onFinished(lambda); }
HttpRequest &HttpRequest::onSuccess(std::function<void (QJsonObject)>     lambda) { return onFinished(lambda); }
HttpRequest &HttpRequest::onSuccess(std::function<void (QByteArray)>      lambda) { return onFinished(lambda); }
HttpRequest &HttpRequest::onSuccess(std::function<void ()>                lambda) { return onFinished(lambda); }

HttpRequest &HttpRequest::onFailed(const QObject *receiver,              const char *method) { return onError(receiver, method); }
HttpRequest &HttpRequest::onFailed(std::function<void (QNetworkReply *)>             lambda) { return onError(lambda); }
HttpRequest &HttpRequest::onFailed(std::function<void (QNetworkReply::NetworkError)> lambda) { return onError(lambda); }
HttpRequest &HttpRequest::onFailed(std::function<void (QByteArray)>                  lambda) { return onError(lambda); }
HttpRequest &HttpRequest::onFailed(std::function<void ()>                            lambda) { return onError(lambda); }

HttpRequest &HttpRequest::onError(const QObject *receiver,              const char *method) { return onResponse(h_onError, receiver, method); }
HttpRequest &HttpRequest::onError(std::function<void (QNetworkReply *)>             lambda) { return onResponse(h_onError, QVariant::fromValue(lambda)); }
HttpRequest &HttpRequest::onError(std::function<void (QNetworkReply::NetworkError)> lambda) { return onResponse(h_onError, QVariant::fromValue(lambda)); }
HttpRequest &HttpRequest::onError(std::function<void (QByteArray)>                  lambda) { return onResponse(h_onError, QVariant::fromValue(lambda)); }
HttpRequest &HttpRequest::onError(std::function<void ()>                            lambda) { return onResponse(h_onError, QVariant::fromValue(lambda)); }

HttpRequest &HttpRequest::onReadyRead(const QObject *receiver, const char  *method) { return onResponse(h_onReadyRead, receiver, method); }
HttpRequest &HttpRequest::onReadyRead(std::function<void (QNetworkReply *)> lambda) { return onResponse(h_onReadyRead, QVariant::fromValue(lambda)); }

HttpRequest &HttpRequest::onHead(const QObject *receiver, const char *method) { return onResponse(h_onHead, receiver, method); }
HttpRequest &HttpRequest::onHead(std::function<void (QList<QNetworkReply::RawHeaderPair>)> lambda) { return onResponse(h_onHead, QVariant::fromValue(lambda)); }
HttpRequest &HttpRequest::onHead(std::function<void (QMap<QString, QString>)> lambda) { return onResponse(h_onHead, QVariant::fromValue(lambda)); }

HttpRequest &HttpRequest::onDownloadProgress(const QObject *receiver, const char *method) { return onResponse(h_onDownloadProgress, receiver, method); }
HttpRequest &HttpRequest::onDownloadProgress(std::function<void (qint64, qint64)> lambda) { return onResponse(h_onDownloadProgress, QVariant::fromValue(lambda)); }

HttpRequest &HttpRequest::onDownloadFileNameChanged(const QObject *receiver, const char *method) { return onResponse(h_onDownloadFileNameChanged, receiver, method); }
HttpRequest &HttpRequest::onDownloadFileNameChanged(std::function<void (QString)> lambda) { return onResponse(h_onDownloadFileNameChanged, QVariant::fromValue(lambda)); }

HttpRequest &HttpRequest::onDownloadFileSuccess(const QObject *receiver, const char *method) { return onResponse(h_onDownloadFileSuccess, receiver, method); }
HttpRequest &HttpRequest::onDownloadFileSuccess(std::function<void ()>               lambda) { return onResponse(h_onDownloadFileSuccess, QVariant::fromValue(lambda)); }
HttpRequest &HttpRequest::onDownloadFileSuccess(std::function<void (QString)>        lambda) { return onResponse(h_onDownloadFileSuccess, QVariant::fromValue(lambda)); }

HttpRequest &HttpRequest::onDownloadFileFailed(const QObject *receiver, const char *method) { return onResponse(h_onDownloadFileFailed, receiver, method); }
HttpRequest &HttpRequest::onDownloadFileFailed(std::function<void ()>               lambda) { return onResponse(h_onDownloadFileFailed, QVariant::fromValue(lambda)); }
HttpRequest &HttpRequest::onDownloadFileFailed(std::function<void (QString)>        lambda) { return onResponse(h_onDownloadFileFailed, QVariant::fromValue(lambda)); }

HttpRequest &HttpRequest::onUploadProgress(const QObject *receiver, const char *method) { return onResponse(h_onUploadProgress, receiver, method); }
HttpRequest &HttpRequest::onUploadProgress(std::function<void (qint64, qint64)> lambda) { return onResponse(h_onUploadProgress, QVariant::fromValue(lambda)); }

HttpRequest &HttpRequest::onTimeout(const QObject *receiver, const char  *method) { return onResponse(h_onTimeout, receiver, method); }
HttpRequest &HttpRequest::onTimeout(std::function<void (QNetworkReply *)> lambda) { return onResponse(h_onTimeout, QVariant::fromValue(lambda)); }
HttpRequest &HttpRequest::onTimeout(std::function<void ()>                lambda) { return onResponse(h_onTimeout, QVariant::fromValue(lambda)); }

HttpRequest &HttpRequest::onRetried(const QObject *receiver, const char  *method) { return onResponse(h_onRetried, receiver, method); }
HttpRequest &HttpRequest::onRetried(std::function<void ()>                lambda) { return onResponse(h_onRetried, QVariant::fromValue(lambda)); }

HttpRequest &HttpRequest::onRepeated(const QObject *receiver, const char *method) { return onResponse(h_onRepeated, receiver, method); }
HttpRequest &HttpRequest::onRepeated(std::function<void ()>               lambda) { return onResponse(h_onRepeated, QVariant::fromValue(lambda)); }

HttpRequest &HttpRequest::onAuthenticationRequired(const QObject *receiver, const char *method)   { return onResponse(h_onAuthenticationRequired, receiver, method); }
HttpRequest &HttpRequest::onAuthenticationRequired(std::function<void (QAuthenticator *)> lambda) { return onResponse(h_onAuthenticationRequired, QVariant::fromValue(lambda)); }

HttpRequest &HttpRequest::onAuthenticationRequireFailed(const QObject *receiver, const char *method) { return onResponse(h_onAuthenticationRequireFailed, receiver, method); }
HttpRequest &HttpRequest::onAuthenticationRequireFailed(std::function<void ()> lambda) { return onResponse(h_onAuthenticationRequireFailed, QVariant::fromValue(lambda)); }
HttpRequest &HttpRequest::onAuthenticationRequireFailed(std::function<void (QNetworkReply *)> lambda) { return onResponse(h_onAuthenticationRequireFailed, QVariant::fromValue(lambda)); }

HttpRequest &HttpRequest::onResponse(const QObject *receiver, const char *method) { return onResponse(h_onFinished, receiver, method); }
HttpRequest &HttpRequest::onResponse(std::function<void (QNetworkReply*)> lambda) { return onResponse(h_onFinished, QVariant::fromValue(lambda)); }
HttpRequest &HttpRequest::onResponse(std::function<void (QVariantMap)>    lambda) { return onResponse(h_onFinished, QVariant::fromValue(lambda)); }
HttpRequest &HttpRequest::onResponse(std::function<void (QByteArray)>     lambda) { return onResponse(h_onFinished, QVariant::fromValue(lambda)); }
HttpRequest &HttpRequest::onResponse(HandleType type, QVariant            lambda) { return onResponse(type, lambda.typeName(), lambda); }
HttpRequest &HttpRequest::onResponse(HandleType type, const QObject *receiver, const char *method) { return onResponse(type, method, QVariant::fromValue((QObject *)receiver)); }
HttpRequest &HttpRequest::onResponse(HandleType type, QString key, QVariant value)
{
    if (!m_handleMap.contains(type)) {
        QList<QPair<QString, QVariant> > handleList;
        m_handleMap.insert(type, handleList);
    }

    auto handleList = m_handleMap[type];
    handleList.append({key, value});

    m_handleMap.insert(type, handleList);
    return *this;
}

QString HttpRequest::toString()
{
    QString str = \
            "General: \n" \
            "    Request URL: %{url} \n" \
            "    Request Method: %{method} \n" \
            "Request Headers: \n" \
            "%{requestHeaders} \n" \
            "Request Body: \n" \
            "%{requestBody}";

    str.replace("%{url}", m_request.url().toString());
    str.replace("%{method}", networkOperation2String(m_op));
    str.replace("%{requestHeaders}", lineIndent(networkHeader2String(m_request), "    "));
    str.replace("%{requestBody}", lineIndent(networkBody2String(m_body), "    "));

    return str;
}

HttpResponse *HttpRequest::exec(const HttpRequest &_httpRequest, HttpResponse *httpResponse)
{
    HttpRequest httpRequest = _httpRequest;

    QByteArray op = networkOperation2String(httpRequest.m_op).toUtf8();
    if (op.isEmpty()) {
        QString str = QString("Url: [%1]; Method: [%2] not support!").arg(httpRequest.m_request.url().toString()).arg(QString(op));
        printError(httpRequest.m_logLevel, str);
        return nullptr;
    }

    using BodyType = HttpRequest::BodyType;
    BodyType        bodyType = httpRequest.m_body.first;
    QVariant            body = httpRequest.m_body.second;
    QNetworkRequest  request = httpRequest.m_request;
    HttpClient   *httpClient = httpRequest.m_httpClient;

    if (bodyType == BodyType::MultiPart) {
        QHttpMultiPart *multiPart = body.value<QHttpMultiPart*>();
        QString       contentType = QString("multipart/form-data;boundary=%1").arg(multiPart->boundary().data());

        request.setHeader(QNetworkRequest::ContentTypeHeader, contentType);

        httpRequest.m_reply = httpRequest.m_httpClient->sendCustomRequest(request, op, multiPart);
        multiPart->setParent(httpRequest.m_reply);

    }
    else if (bodyType == BodyType::FileMap) {
        QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
        QString contentType = QString("multipart/form-data;boundary=%1").arg(multiPart->boundary().data());
        request.setHeader(QNetworkRequest::ContentTypeHeader, contentType);

        const auto &fileMap = body.value<QMap<QString, QString>>();
        for (const auto &each : fileMap.toStdMap()) {
            const QString &key      = each.first;
            const QString &filePath = each.second;

            QFile *file = new QFile(filePath);
            file->open(QIODevice::ReadOnly);
            file->setParent(multiPart);

            // todo
            // part.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("text/plain"));

            // note: "form-data; name=\"%1\";filename=\"%2\"" != "form-data; name=\"%1\";filename=\"%2\";"
            QString dispositionHeader = QString("form-data; name=\"%1\";filename=\"%2\"")
                    .arg(key)
                    .arg(QFileInfo(filePath).fileName());
            QHttpPart part;
            part.setHeader(QNetworkRequest::ContentDispositionHeader, dispositionHeader);
            part.setBodyDevice(file);

            multiPart->append(part);
        }

        httpRequest.m_reply = httpClient->sendCustomRequest(request, op, multiPart);
        if (httpRequest.m_reply)
            multiPart->setParent(httpRequest.m_reply);
        else
            delete multiPart;

    }
    else if (bodyType == BodyType::FormData) {
        QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
        QString       contentType = QString("multipart/form-data;boundary=%1").arg(multiPart->boundary().data());
        request.setHeader(QNetworkRequest::ContentTypeHeader, contentType);

        const auto &formDataMap = body.value<QMap<QString, QVariant>>();
        for (const auto &each : formDataMap.toStdMap()) {
            const QString &key      = each.first;
            const QString &value    = each.second.toString();

            QString dispositionHeader = QString("form-data; name=\"%1\"").arg(key);

            QHttpPart part;
            part.setHeader(QNetworkRequest::ContentDispositionHeader, dispositionHeader);
            part.setBody(value.toUtf8());

            multiPart->append(part);
        }

        httpRequest.m_reply = httpClient->sendCustomRequest(request, op, multiPart);

        if (httpRequest.m_reply)
            multiPart->setParent(httpRequest.m_reply);
        else
            delete multiPart;
    }
    else {
        httpRequest.m_reply = httpClient->sendCustomRequest(request, op, body.toByteArray());
    }

    if (httpRequest.m_reply == nullptr) {
        // fixme: todo onError
        printError(httpRequest.m_logLevel, "Http reply invalid");
        Q_ASSERT(httpRequest.m_reply);
        return nullptr;
    }

    static int count = 0;
    httpRequest.m_reply->setProperty("count", count++);

    // fixme
    if (!httpRequest.m_ignoreSslErrors.isEmpty()) {
        httpRequest.m_reply->ignoreSslErrors(httpRequest.m_ignoreSslErrors);
    }

    if (httpRequest.m_readBufferSize >= 0) {
        httpRequest.m_reply->setReadBufferSize(httpRequest.m_readBufferSize);
    }

    printDebug(httpRequest.m_logLevel, toString());

    if (httpResponse) {
        httpResponse->setParent(httpRequest.m_reply);
        httpResponse->setHttpRequest(httpRequest);
        return httpResponse;
    }
    else {
        return new HttpResponse(httpRequest, httpRequest.m_reply);
    }
}

inline QDebug operator<<(QDebug debug, const QNetworkAccessManager::Operation &op)
{
    QDebugStateSaver saver(debug);
    debug.nospace();

    switch (op) {
        case QNetworkAccessManager::HeadOperation: return debug  << "HeadOperation";
        case QNetworkAccessManager::GetOperation:  return debug  << "GetOperation";
        case QNetworkAccessManager::PostOperation: return debug  << "PostOperation";
        case QNetworkAccessManager::PutOperation:  return debug  << "PutOperation";
        case QNetworkAccessManager::DeleteOperation: return debug  << "DeleteOperation";
        case QNetworkAccessManager::CustomOperation: return debug  << "CustomOperation";
        default: return debug  << "UnknownOperation";
    }
}

inline QDebug operator<<(QDebug debug, const HttpRequest::HandleType &handleType)
{
    QDebugStateSaver saver(debug);
    debug.nospace();

    switch (handleType) {
        case HttpRequest::h_onFinished: return debug << "onFinished";
        case HttpRequest::h_onError:    return debug << "onError";
        case HttpRequest::h_onDownloadProgress: return debug << "onDownloadProgress";
        case HttpRequest::h_onUploadProgress:   return debug << "onUploadProgress";
        // todo: onUploadProgressSuccess and onUploadProgressFaied
        case HttpRequest::h_onDownloadFileSuccess:  return debug << "onDownloadFileSuccess";
        case HttpRequest::h_onDownloadFileFailed:   return debug << "onDownloadFileFailed";
        case HttpRequest::h_onTimeout:          return debug << "onTimeout";
        case HttpRequest::h_onReadyRead:        return debug << "onReadyRead";
        case HttpRequest::h_onEncrypted:        return debug << "onEncrypted";
        case HttpRequest::h_onMetaDataChanged:  return debug << "onMetaChanged";
        case HttpRequest::h_onPreSharedKeyAuthenticationRequired: return debug << "onPreSharedKeyAuthenticationRequired";
        case HttpRequest::h_onRedirectAllowed:  return debug << "onRedirectAllowed";
        case HttpRequest::h_onRedirected:       return debug << "onRedirected";
        case HttpRequest::h_onSslErrors:        return debug << "onSslErrors";
        case HttpRequest::h_onRetried:          return debug << "onRetried";
        case HttpRequest::h_onRepeated:         return debug << "onRepeated";
        case HttpRequest::h_onAuthenticationRequired:      return debug << "onAuthenticationRequired";
        case HttpRequest::h_onAuthenticationRequireFailed: return debug << "onAuthenticationRequireFailed";
        case HttpRequest::h_onHead:                    return debug << "onHead";
        case HttpRequest::h_onDownloadFileProgess:     return debug << "onDownloadFileProgress";
        case HttpRequest::h_onDownloadFileNameChanged: return debug << "onDownloadFileNameChanged";
        default: return debug << "Unknow";
    }
}

static int extractCode(const char *member)
{
    /* extract code, ensure QMETHOD_CODE <= code <= QSIGNAL_CODE */
    return (((int)(*member) - '0') & 0x3);
}

static bool isMethod(const char *member)
{
    int ret = extractCode(member);
    return  ret >= QMETHOD_CODE && ret <= QSIGNAL_CODE;
}

template<typename M, typename L, typename T>
bool httpResponseConnect(L sender, T senderSignal, const QString &lambdaString, const QVariant &lambda)
{
    if (lambdaString == QVariant::fromValue(M()).typeName()) {
        return QObject::connect(sender, senderSignal, lambda.value<M>());
    }
    else if (isMethod(qPrintable(lambdaString))) {
        QString signal = QMetaMethod::fromSignal(senderSignal).methodSignature();
        signal.insert(0, "2");
        signal.replace("qlonglong", "qint64");

        const QObject *receiver = lambda.value<QObject*>();
        QString          method = QMetaObject::normalizedSignature(qPrintable(lambdaString)); // remove 'const', like: const QString => QString

        if (QMetaObject::checkConnectArgs(qPrintable(signal), qPrintable(method))) {
            return QObject::connect(sender, qPrintable(signal), receiver, qPrintable(method));
        }
        else {
            return false;
        }
    }
    else {
        return false;
    }
}

#define HTTP_RESPONSE_CONNECT_X(sender, senderSignal, lambdaString, lambda, ...) \
    httpResponseConnect< std::function<void (__VA_ARGS__)> > ( \
             sender, \
             static_cast<void (HttpResponse::*)(__VA_ARGS__)>(&HttpResponse::senderSignal), \
             lambdaString, \
             lambda);

HttpResponse *HttpRequest::exec()
{
    if (this->m_downloader.isEnabled) {
        HttpDownloader *downloader = new HttpDownloader(*this, nullptr);
        HttpResponse *response = downloader->exec();
        downloader->setParent(response);
        return response;
    }
    else {
        return exec(*this);
    }
}

HttpRequest &HttpRequest::enabledRetry(bool isEnabled)
{
    m_enabledRetry = isEnabled;
    return *this;
}

HttpRequest &HttpRequest::queryParam(const QString &key, const QVariant &value)
{
    QUrl url(m_request.url());
    QUrlQuery urlQuery(url);

    urlQuery.addQueryItem(key, value.toString());
    url.setQuery(urlQuery);

    m_request.setUrl(url);

    return *this;
}

HttpRequest &HttpRequest::queryParams(const QMap<QString, QVariant> &params)
{
    QMapIterator<QString, QVariant> iter(params);
    while (iter.hasNext()) {
        iter.next();
        queryParam(iter.key(), iter.value());
    }

    return *this;
}

HttpRequest &HttpRequest::userAttribute(const QVariant &value)
{
    m_request.setAttribute(QNetworkRequest::User, value);
    return *this;
}

HttpRequest &HttpRequest::attribute(QNetworkRequest::Attribute attribute, const QVariant &value)
{
    m_request.setAttribute(attribute, value);
    return *this;
}

HttpClient *HttpClient::instance()
{
    static HttpClient client;
    return &client;
}

HttpClient::HttpClient(QObject *parent) : QNetworkAccessManager(parent)
{
}

HttpRequest HttpClient::head(const QString &url)
{
    return HttpRequest(QNetworkAccessManager::HeadOperation, this).url(url);
}

HttpRequest HttpClient::get(const QString &url)
{
    return HttpRequest(QNetworkAccessManager::GetOperation, this).url(url);
}

HttpRequest HttpClient::post(const QString &url)
{
    return HttpRequest(QNetworkAccessManager::PostOperation, this).url(url);
}

HttpRequest HttpClient::put(const QString &url)
{
    return HttpRequest(QNetworkAccessManager::PutOperation, this).url(url);
}

HttpRequest HttpClient::send(const QString &url, QNetworkAccessManager::Operation op)
{
    return HttpRequest(op, this).url(url);
}

#if (QT_VERSION < QT_VERSION_CHECK(5, 8, 0))
QNetworkReply *HttpClient::sendCustomRequest(const QNetworkRequest &request, const QByteArray &verb, const QByteArray &data)
{
    QBuffer *buffer = new QBuffer;
    buffer->setData(data);
    buffer->open(QIODevice::ReadOnly);
    QNetworkReply *reply = QNetworkAccessManager::sendCustomRequest(request, verb, buffer);
    buffer->setParent(reply);

    return reply;
}

QNetworkReply *HttpClient::sendCustomRequest(const QNetworkRequest &request, const QByteArray &verb, QHttpMultiPart *multiPart)
{
    if (verb == "PUT") {
        return QNetworkAccessManager::put(request, multiPart);
    }
    else if (verb == "POST") {
        return QNetworkAccessManager::post(request, multiPart);
    }
    else {
        qWarning() << "not support " << verb << "multi part.";
        return nullptr;
    }
}
#endif

HttpResponse::HttpResponse(const HttpRequest &httpRequest, QObject *parent)
    : QObject(parent),
      m_httpRequest(httpRequest),
      m_retriesRemaining(httpRequest.m_retryCount)
{
    this->setHttpRequest(httpRequest);
}

HttpResponse::~HttpResponse()
{
}

void HttpResponse::setHttpRequest(const HttpRequest &httpRequest)
{
    if (httpRequest.m_timeoutMs > 0) {
        new HttpResponseTimeout(this, httpRequest.m_timeoutMs);
    }

    QNetworkReply *reply = httpRequest.m_reply;
    if (reply) {
        connect(reply, SIGNAL(finished()),                         this, SLOT(onFinished()));
        connect(reply, SIGNAL(finished()),                         this, SLOT(onHandleHead()));
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
        connect(reply, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), this, SLOT(onError(QNetworkReply::NetworkError)));
#else
        connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onError(QNetworkReply::NetworkError)));
#endif

        connect(reply, SIGNAL(downloadProgress(qint64, qint64)),   this, SLOT(onDownloadProgress(qint64, qint64)));
        connect(reply, SIGNAL(uploadProgress(qint64, qint64)),     this, SLOT(onUploadProgress(qint64, qint64)));

        connect(reply, SIGNAL(readyRead()),                        this, SLOT(onReadOnceReplyHeader()));
        connect(reply, SIGNAL(readyRead()),                        this, SLOT(onHandleHead()));
        connect(reply, SIGNAL(readyRead()),                        this, SLOT(onReadyRead()));

        connect(reply, SIGNAL(encrypted()),                        this, SLOT(onEncrypted()));
        connect(reply, SIGNAL(metaDataChanged()),                  this, SLOT(onMetaDataChanged()));

        connect(reply, SIGNAL(redirected(QUrl)),                   this, SLOT(onRedirected(QUrl)));
        connect(reply, SIGNAL(sslErrors(QList<QSslError>)),        this, SLOT(onSslErrors(QList<QSslError>)));

    #if (QT_VERSION >= QT_VERSION_CHECK(5, 9, 0))
        connect(reply, SIGNAL(redirectAllowed()),                  this, SLOT(onRedirectAllowed()));
    #endif

        connect(reply, SIGNAL(preSharedKeyAuthenticationRequired(QSslPreSharedKeyAuthenticator*)), this, SLOT(onPreSharedKeyAuthenticationRequired(QSslPreSharedKeyAuthenticator*)));

        connect(reply->manager(), SIGNAL(authenticationRequired(QNetworkReply*, QAuthenticator*)), this, SLOT(onAuthenticationRequired(QNetworkReply*,QAuthenticator*)));

        // fixme: Too cumbersome
        for (auto each : httpRequest.m_handleMap.toStdMap()) {
            const HttpRequest::HandleType         &key   = each.first;
            const QList<QPair<QString, QVariant>> &value = each.second;

            for (auto iter : value) {
                const QVariant &lambda      = iter.second;
                const QString &lambdaString = iter.first;
                int ret = 0;

                if (key == HttpRequest::h_onFinished) {
                    ret += HTTP_RESPONSE_CONNECT_X(this, finished, lambdaString, lambda, void);
                    ret += HTTP_RESPONSE_CONNECT_X(this, finished, lambdaString, lambda, QByteArray);
                    ret += HTTP_RESPONSE_CONNECT_X(this, finished, lambdaString, lambda, QString);
                    ret += HTTP_RESPONSE_CONNECT_X(this, finished, lambdaString, lambda, QVariantMap);
                    ret += HTTP_RESPONSE_CONNECT_X(this, finished, lambdaString, lambda, QJsonObject);
                    ret += HTTP_RESPONSE_CONNECT_X(this, finished, lambdaString, lambda, QNetworkReply*);
                }
                else if (key == HttpRequest::h_onDownloadProgress) {
                    ret += HTTP_RESPONSE_CONNECT_X(this, downloadProgress, lambdaString, lambda, qint64, qint64);
                }
                else if (key == HttpRequest::h_onUploadProgress) {
                    ret += HTTP_RESPONSE_CONNECT_X(this, uploadProgress, lambdaString, lambda, qint64, qint64);
                }
                else if (key == HttpRequest::h_onError) {
                    ret += HTTP_RESPONSE_CONNECT_X(this, error, lambdaString, lambda, void);
                    ret += HTTP_RESPONSE_CONNECT_X(this, error, lambdaString, lambda, QByteArray);
                    ret += HTTP_RESPONSE_CONNECT_X(this, error, lambdaString, lambda, QString);
                    ret += HTTP_RESPONSE_CONNECT_X(this, error, lambdaString, lambda, QNetworkReply*);
                    ret += HTTP_RESPONSE_CONNECT_X(this, error, lambdaString, lambda, QNetworkReply::NetworkError);
                }
                else if (key == HttpRequest::h_onTimeout) {
                    ret += HTTP_RESPONSE_CONNECT_X(this, timeout, lambdaString, lambda, QNetworkReply*);
                    ret += HTTP_RESPONSE_CONNECT_X(this, timeout, lambdaString, lambda, void);
                }
                else if (key == HttpRequest::h_onReadyRead) {
                    ret += HTTP_RESPONSE_CONNECT_X(this, readyRead, lambdaString, lambda, QNetworkReply*);
                }
                else if (key == HttpRequest::h_onDownloadFileSuccess) {
                    ret += HTTP_RESPONSE_CONNECT_X(this, downloadFileFinished, lambdaString, lambda, void);
                    ret += HTTP_RESPONSE_CONNECT_X(this, downloadFileFinished, lambdaString, lambda, QString);
                }
                else if (key == HttpRequest::h_onDownloadFileFailed) {
                    ret += HTTP_RESPONSE_CONNECT_X(this, downloadFileError, lambdaString, lambda, void);
                    ret += HTTP_RESPONSE_CONNECT_X(this, downloadFileError, lambdaString, lambda, QString);
                }
                else if (key == HttpRequest::h_onEncrypted) {
                    ret += HTTP_RESPONSE_CONNECT_X(this, encrypted, lambdaString, lambda, void);
                }
                else if (key == HttpRequest::h_onMetaDataChanged) {
                    ret += HTTP_RESPONSE_CONNECT_X(this, metaDataChanged, lambdaString, lambda, void);
                }
                else if (key == HttpRequest::h_onPreSharedKeyAuthenticationRequired) {
                    ret += HTTP_RESPONSE_CONNECT_X(this, preSharedKeyAuthenticationRequired, lambdaString, lambda, QSslPreSharedKeyAuthenticator*);
                }
                else if (key == HttpRequest::h_onRedirectAllowed) {
                    ret += HTTP_RESPONSE_CONNECT_X(this, redirectAllowed, lambdaString, lambda, void);
                }
                else if (key == HttpRequest::h_onRedirected) {
                    ret += HTTP_RESPONSE_CONNECT_X(this, redirected, lambdaString, lambda, QUrl);
                }
                else if (key == HttpRequest::h_onSslErrors) {
                    ret += HTTP_RESPONSE_CONNECT_X(this, sslErrors, lambdaString, lambda, QList<QSslError>);
                }
                else if (key == HttpRequest::h_onRetried) {
                    ret += HTTP_RESPONSE_CONNECT_X(this, retried, lambdaString, lambda, void);
                }
                else if (key == HttpRequest::h_onRepeated) {
                    ret += HTTP_RESPONSE_CONNECT_X(this, repeated, lambdaString, lambda, void);
                }
                else if (key == HttpRequest::h_onAuthenticationRequired) {
                    ret += HTTP_RESPONSE_CONNECT_X(this, authenticationRequired, lambdaString, lambda, QAuthenticator*);
                }
                else if (key == HttpRequest::h_onAuthenticationRequireFailed) {
                    ret += HTTP_RESPONSE_CONNECT_X(this, authenticationRequireFailed, lambdaString, lambda, void);
                    ret += HTTP_RESPONSE_CONNECT_X(this, authenticationRequireFailed, lambdaString, lambda, QNetworkReply*);
                }
                else if (key == HttpRequest::h_onHead) {
                    ret += HTTP_RESPONSE_CONNECT_X(this, head, lambdaString, lambda, QList<QNetworkReply::RawHeaderPair>);
                    ret += HTTP_RESPONSE_CONNECT_X(this, head, lambdaString, lambda, QMap<QString, QString>);
                }
                else if (key == HttpRequest::h_onDownloadFileProgess) {
                    ret += HTTP_RESPONSE_CONNECT_X(this, downloadFileProgress, lambdaString, lambda, qint64, qint64);
                }
                else if (key == HttpRequest::h_onDownloadFileNameChanged) {
                    ret += HTTP_RESPONSE_CONNECT_X(this, downloadFileNameChanged, lambdaString, lambda, QString);
                }
                else {
                    printWarn(httpRequest.m_logLevel, QString("%1 unsupported").arg(key));
                }

                if (ret == 0) {
                    QString method = lambdaString;
                    if (isMethod(qPrintable(method)))
                        method.remove(0, 1);

                    printWarn(httpRequest.m_logLevel, QString("%1 method[%2] is invalid").arg(key).arg(method));
                }
            }
        }
    }

    const HttpRequest &oldRequest = m_httpRequest;
    m_httpRequest = httpRequest;

    if (oldRequest.m_reply != httpRequest.m_reply) {
        emit replyChanged(httpRequest.m_reply);
    }

    if (reply && httpRequest.m_isBlock) {
        new HttpBlocker(reply);
    }
}

QString HttpResponse::toString() const
{
    QString str = \
            "General: \n" \
            "    Request URL: %{url} \n" \
            "    Request Method: %{method} \n" \
            "    Request Status: %{status}(%{statusString}) \n" \
            "Request Headers: \n" \
            "%{requestHeaders} \n" \
            "Response Headers: \n" \
            "%{responseHeaders} \n" \
            "Request Body: \n" \
            "%{requestBody}";

    QNetworkReply *reply = this->m_httpRequest.m_reply;
    str.replace("%{url}", this->m_httpRequest.m_request.url().toString());
    str.replace("%{method}", networkOperation2String(m_httpRequest.m_op));
    str.replace("%{status}", QString::number(reply->error()));
    str.replace("%{statusString}", reply->errorString());
    str.replace("%{requestHeaders}", lineIndent(networkHeader2String(reply->request()), "    "));
    str.replace("%{responseHeaders}", lineIndent(networkReplyHeader2String(reply), "    "));
    str.replace("%{requestBody}", lineIndent(networkBody2String(m_httpRequest.m_body), "    "));
    return str;
}

void HttpResponse::onFinished()
{
    QNetworkReply *reply = m_httpRequest.m_reply;
    if (reply->error() != QNetworkReply::NoError) {
        return;
    }

    for (QObject *o : reply->children()) {
        HttpResponse *response = qobject_cast<HttpResponse*>(o);
        if (response) {
            printDebug(m_httpRequest.m_logLevel, response->toString());
        }
    }

    if (m_httpRequest.m_enabledRetry) {
        emit retried();
    }

    if (m_downloadFile.isOpen()) {
        emit downloadFileFinished();
        emit downloadFileFinished(m_downloadFile.fileName());

        m_downloadFile.close();
    }

    bool isAutoDelete = true;
    if (this->receivers(SIGNAL(finished(QNetworkReply*))) > 0) {
        emit finished(reply);
        isAutoDelete = false;
    }

    if (this->receivers(SIGNAL(finished())) > 0 ||
        this->receivers(SIGNAL(finished(QByteArray))) > 0 ||
        this->receivers(SIGNAL(finished(QString))) > 0 ||
        this->receivers(SIGNAL(finished(QVariantMap))) > 0 ||
        this->receivers(SIGNAL(finished(QJsonObject))) > 0
        )
    {
        QByteArray result = reply->readAll();
        emit finished();

        emit finished(result);

        emit finished(QString(result));

        QJsonObject json = QJsonDocument::fromJson(result).object();
        emit finished(json);

        QVariantMap resultMap = json.toVariantMap();
        emit finished(resultMap);
    }

    if (--m_httpRequest.m_repeatCount > 0) {
        HttpRequest httpRequest = m_httpRequest;
        httpRequest.repeat(m_httpRequest.m_repeatCount)
                   .exec();
    }
    else {
        emit repeated();
    }

    if (isAutoDelete) {
        reply->deleteLater();
    }
}

void HttpResponse::onError(QNetworkReply::NetworkError error)
{
    QNetworkReply *reply = m_httpRequest.m_reply;

    printInfo(m_httpRequest.m_logLevel, QString("%1 error: %2").arg(reply->url().toString()).arg(error));

    if ( m_retriesRemaining-- > 0) {
        HttpRequest httpRequest = m_httpRequest;
        httpRequest.retry(m_retriesRemaining)
                   .enabledRetry(true)
                   .exec();
        reply->deleteLater();
        return;
    }

    if (m_httpRequest.m_enabledRetry) {
        emit retried();
    }

    const QMetaObject &metaObject = QNetworkReply::staticMetaObject;
    QMetaEnum metaEnum = metaObject.enumerator(metaObject.indexOfEnumerator("NetworkError"));
    QString errorString = reply->errorString().isEmpty() ? metaEnum.valueToKey(error) : reply->errorString();

    if (m_httpRequest.m_downloader.isEnabled) {
        QString error = QString("Url: %1 file: %2 error: %3")
                .arg(m_httpRequest.m_request.url().toString()) // fixme
                .arg(m_downloadFile.fileName())
                .arg(errorString);

        emit downloadFileError();
        emit downloadFileError(error);

        m_downloadFile.close();
    }

    bool isAutoDelete = true;
    if (this->receivers(SIGNAL(error(QNetworkReply*))) > 0) {
        emit this->error(reply);
        isAutoDelete = false;
    }

    emit this->error();
    emit this->error(error);
    emit this->error(errorString);
    emit this->error(errorString.toLocal8Bit());

    if (--m_httpRequest.m_repeatCount > 0) {
        HttpRequest httpRequest = m_httpRequest;
        httpRequest.repeat(m_httpRequest.m_repeatCount)
                   .exec();
    }
    else {
        emit repeated();
    }

    if (isAutoDelete) {
        reply->deleteLater();
    }
}

void HttpResponse::onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal)
{
    emit this->downloadProgress(bytesReceived, bytesTotal);
}

void HttpResponse::onUploadProgress(qint64 bytesSent, qint64 bytesTotal)
{
    emit this->uploadProgress(bytesSent, bytesTotal);
}

void HttpResponse::onTimeout()
{
    QNetworkReply *reply = m_httpRequest.m_reply;
    if (reply && reply->isRunning()) {
        reply->abort();

        bool isAutoDelete = true;
        if (this->receivers(SIGNAL(timeout(QNetworkReply*))) > 0) {
            emit this->timeout(reply);
            isAutoDelete = false;
        }

        if (this->receivers(SIGNAL(timeout())) > 0) {
            emit this->timeout();
        }

        if (isAutoDelete) {
            reply->deleteLater();
        }
    }
}

void HttpResponse::onReadyRead()
{
    QNetworkReply *reply = m_httpRequest.m_reply;
    if (m_httpRequest.m_downloader.isEnabled) {
        if (m_downloadFile.isOpen()){
            int size = m_downloadFile.write(reply->readAll());
            if (size == -1) {
                QString error = QString("Url: %1 %2 Write failed!")
                                .arg(m_httpRequest.m_request.url().toString())
                                .arg(m_downloadFile.fileName());
                emit downloadFileError();
                emit downloadFileError(error);
            }
            else {
                m_httpRequest.m_downloader.currentSize += size;
                emit downloadFileProgress(m_httpRequest.m_downloader.currentSize, m_httpRequest.m_downloader.totalSize);
            }
        }
        else {
            // do nothing
        }
    }
    else {
        // do nothing
    }

    emit readyRead(reply);
}

void HttpResponse::onReadOnceReplyHeader()
{
    if (! m_httpRequest.m_downloader.isEnabled)
        return;

    QNetworkReply *reply = m_httpRequest.m_reply;
    disconnect(reply, SIGNAL(readyRead()), this, SLOT(onReadOnceReplyHeader()));

    QString fileName = m_httpRequest.m_downloader.fileName;
    m_downloadFile.setFileName(fileName);

    QIODevice::OpenMode mode = QIODevice::WriteOnly;
    if (m_httpRequest.m_downloader.isSupportBreakpointDownload &&
        m_httpRequest.m_downloader.enabledBreakpointDownload &&
        QFile::exists(fileName))
    {
        mode = QIODevice::Append;
    }

    if (!m_downloadFile.open(mode)) {
        QString error = QString("Url: %1 %2 Non-Writable")
                .arg(m_httpRequest.m_request.url().toString())
                .arg(m_downloadFile.fileName());
        emit downloadFileError();
        emit downloadFileError(error);
    }
    else {
        // todo startDownload
    }
}

void HttpResponse::onEncrypted()
{
    emit encrypted();
}

void HttpResponse::onMetaDataChanged()
{
    emit metaDataChanged();
}

void HttpResponse::onPreSharedKeyAuthenticationRequired(QSslPreSharedKeyAuthenticator *authenticator)
{
    emit preSharedKeyAuthenticationRequired(authenticator);
}

void HttpResponse::onRedirectAllowed()
{
    emit redirectAllowed();
}

void HttpResponse::onRedirected(const QUrl &url)
{
    emit redirected(url);
}

void HttpResponse::onSslErrors(const QList<QSslError> &errors)
{
    emit sslErrors(errors);
}

void HttpResponse::onAuthenticationRequired(QNetworkReply *reply, QAuthenticator *authenticator)
{
    if (this->reply() != reply) {
        return;
    }

    m_authenticationCount++;

    bool isAuthenticationSuccessed = (m_authenticationCount >= 2);
    if (isAuthenticationSuccessed) {
        emit authenticationRequireFailed();
        emit authenticationRequireFailed(this->reply());
    }

    if (m_httpRequest.m_authenticationRequiredCount >= 0 &&
        m_authenticationCount > m_httpRequest.m_authenticationRequiredCount)
    {
        return;
    }

    if (m_httpRequest.m_authenticator.isNull()) {
        emit authenticationRequired(authenticator);
    }
    else {
        authenticator->setUser(m_httpRequest.m_authenticator.user());
        authenticator->setPassword(m_httpRequest.m_authenticator.password());
        // todo setOption....
    }
}

void HttpResponse::onHandleHead()
{
    if (m_isHandleHead) {
        return;
    }

    m_isHandleHead = true;

    QNetworkReply *reply = m_httpRequest.m_reply;
    if (this->receivers(SIGNAL(head(QList<QNetworkReply::RawHeaderPair>))) ||
        this->receivers(SIGNAL(head(QMap<QString, QString>)))
       )
    {
        emit head(reply->rawHeaderPairs());
        QMap<QString, QString> map;
        foreach (auto each, reply->rawHeaderPairs()) {
            map[each.first] = each.second;
        }

        emit head(map);
    }
}

inline QString lineIndent(const QString &source, const QString &indentString)
{
    QRegularExpression rx("^(.*)");
    QRegularExpression::PatternOptions patternOptions;
    patternOptions |= QRegularExpression::MultilineOption;
    rx.setPatternOptions(patternOptions);
    return QString(source).replace(rx, indentString + "\\1");
}

inline QString networkHeader2String(const QNetworkRequest &request)
{
    QString headerString;
    for (const QByteArray &each : request.rawHeaderList()) {
        QByteArray value = request.rawHeader(each);
        headerString += QString("%1: %2\n").arg(QString(each)).arg(QString(value));
    }

    if (headerString.isEmpty()) {
        return "null";
    }

    if (headerString.at(headerString.count()-1) == '\n') {
        headerString.chop(1);
    }

    return headerString;
}

inline QString networkReplyHeader2String(const QNetworkReply *reply)
{
    QString headerString;
    for (const QByteArray &each : reply->rawHeaderList()) {
        QByteArray value = reply->rawHeader(each);
        headerString += QString("%1: %2\n").arg(QString(each)).arg(QString(value));
    }

    if (headerString.isEmpty()) {
        return "null";
    }

    if (headerString.at(headerString.count()-1) == '\n') {
        headerString.chop(1);
    }

    return headerString;
}

inline QString networkBodyType2String(HttpRequest::BodyType t)
{
    if (t == HttpRequest::MultiPart) {
        return "MultiPart";
    }
    else if (t == HttpRequest::FileMap) {
        return "FileMap";
    }
    else if (t == HttpRequest::FormData) {
        return "FormData";
    }
    else if (t == HttpRequest::Raw) {
        return "Raw";
    }
    else if (t == HttpRequest::Raw_Json) {
        return "RawJson";
    }
    else if (t == HttpRequest::X_Www_Form_Urlencoded) {
        return "x_www_form_urlencoded";
    }
    else {
        return "None";
    }
}

inline QString networkBody2String(const QPair<HttpRequest::BodyType, QVariant> &body)
{
    QString bodyTypeString;
    bodyTypeString += "Type: " + networkBodyType2String(body.first) + "\n";
    bodyTypeString += "Data: \n";

    QString bodyDataString;
    if (body.first == HttpRequest::MultiPart) {
        QDebug d(&bodyDataString);
        d << body.second;
    }
    else if (body.first == HttpRequest::FileMap) {
        const auto &fileMap = body.second.value<QMap<QString, QString>>();
        for (const auto &each : fileMap.toStdMap()) {
            const QString &key      = each.first;
            const QString &filePath = each.second;
            bodyDataString += key + ": " + filePath + "\n";
        }
    }
    else if (body.first == HttpRequest::FormData) {
        const auto &formDataMap = body.second.value<QMap<QString, QVariant>>();
        for (const auto &each : formDataMap.toStdMap()) {
            const QString &key      = each.first;
            const QString &value    = each.second.toString();
            bodyDataString += key + ": " + value + "\n";
        }
    }
    else if (body.first == HttpRequest::X_Www_Form_Urlencoded ||
             body.first == HttpRequest::Raw ||
             body.first == HttpRequest::Raw_Json)
    {
        bodyDataString += body.second.toByteArray();
    }

    if (bodyDataString.isEmpty()) {
        bodyDataString = "null";
    }

    bodyDataString = lineIndent(bodyDataString, "=>    ");

    QString bodyString = bodyTypeString + bodyDataString;
    if (bodyString.at(bodyString.count()-1) == '\n') {
        bodyString.chop(1);
    }

    return bodyString;
}

inline QString networkOperation2String(QNetworkAccessManager::Operation o)
{
    static QMap<QNetworkAccessManager::Operation, QByteArray> verbMap =
    {
        {QNetworkAccessManager::HeadOperation, "HEAD"},
        {QNetworkAccessManager::GetOperation,  "GET"},
        {QNetworkAccessManager::PostOperation, "POST"},
        {QNetworkAccessManager::PutOperation,  "PUT"},
    };

    return verbMap.value(o, "");
}
}

#define HTTPRESPONSE_DECLARE_METATYPE(...) \
    Q_DECLARE_METATYPE(std::function<void (__VA_ARGS__)>)

HTTPRESPONSE_DECLARE_METATYPE(void)
HTTPRESPONSE_DECLARE_METATYPE(QByteArray)
HTTPRESPONSE_DECLARE_METATYPE(QString)
HTTPRESPONSE_DECLARE_METATYPE(QVariantMap)
HTTPRESPONSE_DECLARE_METATYPE(QJsonObject)
HTTPRESPONSE_DECLARE_METATYPE(QNetworkReply*)
HTTPRESPONSE_DECLARE_METATYPE(qint64, qint64)
HTTPRESPONSE_DECLARE_METATYPE(QNetworkReply::NetworkError)
HTTPRESPONSE_DECLARE_METATYPE(QSslPreSharedKeyAuthenticator*)
HTTPRESPONSE_DECLARE_METATYPE(QUrl)
HTTPRESPONSE_DECLARE_METATYPE(QList<QSslError>)
HTTPRESPONSE_DECLARE_METATYPE(QAuthenticator*)
HTTPRESPONSE_DECLARE_METATYPE(QList<QNetworkReply::RawHeaderPair>)
HTTPRESPONSE_DECLARE_METATYPE(QMap<QString, QString>)

#endif // QTHUB_COM_HTTPCLIENT_HPP
