/*
 * Copyright (C) 2009 Intel Corporation
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) version 3.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301  USA
 */

#include <syncevo/CurlTransportAgent.h>
#include <syncevo/SuspendFlags.h>

#ifdef ENABLE_LIBCURL

#include <algorithm>
#include <ctime>
#include <syncevo/util.h>

#include <syncevo/declarations.h>
SE_BEGIN_CXX


CurlTransportAgent::CurlTransportAgent() :
    m_easyHandle(easyInit()),
    m_slist(nullptr),
    m_status(INACTIVE),
    m_timeoutSeconds(0),
    m_reply(nullptr),
    m_replyLen(0),
    m_replySize(0)
{
#ifdef ENABLE_MAEMO /* hack because Maemo doesn't support IPv6 yet */
    curl_easy_setopt(m_easyHandle, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
#endif
    /*
     * set up for post where message is pushed into curl via
     * its read callback and reply is stored in write callback
     */
    CURLcode code;
    auto readDataCallback = [] (void *buffer, size_t size, size_t nmemb, void *stream) noexcept {
        return static_cast<CurlTransportAgent *>(stream)->readData(buffer, size * nmemb);
    };
    auto writeDataCallback = [] (void *buffer, size_t size, size_t nmemb, void *stream) noexcept {
        return static_cast<CurlTransportAgent *>(stream)->writeData(buffer, size * nmemb);
    };
    if ((code = curl_easy_setopt(m_easyHandle, CURLOPT_NOPROGRESS, false)) ||
        (code = curl_easy_setopt(m_easyHandle, CURLOPT_PROGRESSFUNCTION, progressCallback)) ||
        (code = curl_easy_setopt(m_easyHandle, CURLOPT_WRITEFUNCTION, writeDataCallback)) ||
        (code = curl_easy_setopt(m_easyHandle, CURLOPT_WRITEDATA, (void *)this)) ||
        (code = curl_easy_setopt(m_easyHandle, CURLOPT_READFUNCTION, readDataCallback)) ||
        (code = curl_easy_setopt(m_easyHandle, CURLOPT_READDATA, (void *)this)) ||
        (code = curl_easy_setopt(m_easyHandle, CURLOPT_ERRORBUFFER, this->m_curlErrorText )) ||
        (code = curl_easy_setopt(m_easyHandle, CURLOPT_AUTOREFERER, true)) ||
        (code = curl_easy_setopt(m_easyHandle, CURLOPT_POST, true)) ||
        (code = curl_easy_setopt(m_easyHandle, CURLOPT_FOLLOWLOCATION, true))) {
        /* error encountered, throw exception */
        curl_easy_cleanup(m_easyHandle);
        checkCurl(code);
    }
}

CURL *CurlTransportAgent::easyInit()
{
    static bool initialized = false;
    static CURLcode initres;

    if (!initialized) {
        initres = curl_global_init(CURL_GLOBAL_ALL);
        initialized = true;
    }

    if (initres) {
        SE_THROW_EXCEPTION(TransportException, "global curl initialization failed");
    }
    CURL *handle = curl_easy_init();
    if (!handle) {
        SE_THROW_EXCEPTION(TransportException, "no curl handle");
    }
    return handle;
}

CurlTransportAgent::~CurlTransportAgent()
{
    if (m_reply) {
        free(m_reply);
    }
    curl_easy_cleanup(m_easyHandle);
    curl_slist_free_all(m_slist);
}

void CurlTransportAgent::setURL(const std::string &url)
{
    m_url = url;
    CURLcode code = curl_easy_setopt(m_easyHandle, CURLOPT_URL, m_url.c_str());
    checkCurl(code);
}

void CurlTransportAgent::setProxy(const std::string &proxy)
{
    m_proxy = proxy;
    CURLcode code = curl_easy_setopt(m_easyHandle, CURLOPT_PROXY, m_proxy.c_str());
    checkCurl(code);
}

void CurlTransportAgent::setProxyAuth(const std::string &user, const std::string &password)
{
    m_auth = user + ":" + password;
    CURLcode code = curl_easy_setopt(m_easyHandle, CURLOPT_PROXYUSERPWD,
                                     m_auth.c_str());
    checkCurl(code);
}

void CurlTransportAgent::setContentType(const std::string &type)
{
    m_contentType = type;
}

void CurlTransportAgent::setUserAgent(const std::string &agent)
{
    m_agent = agent;
    CURLcode code = curl_easy_setopt(m_easyHandle, CURLOPT_USERAGENT,
                                     m_agent.c_str());
    checkCurl(code);
}

void CurlTransportAgent::setSSL(const std::string &cacerts,
                                bool verifyServer,
                                bool verifyHost)
{
    m_cacerts = cacerts;
    CURLcode code = CURLE_OK;

    if (!m_cacerts.empty()) {
        if (isDir(m_cacerts)) {
            // libcurl + OpenSSL does not work with a directory set in CURLOPT_CAINFO.
            // Must set the directory name as CURLOPT_CAPATH.
            //
            // Hopefully libcurl NSS also finds the directory name
            // here ("NSS-powered libcurl provides the option only for
            // backward compatibility. ").
            code = curl_easy_setopt(m_easyHandle, CURLOPT_CAPATH, m_cacerts.c_str());
        } else {
            code = curl_easy_setopt(m_easyHandle, CURLOPT_CAINFO, m_cacerts.c_str());
        }
    }
    if (!code) {
        code = curl_easy_setopt(m_easyHandle, CURLOPT_SSL_VERIFYPEER, (long)verifyServer);
    }
    if (!code) {
        code = curl_easy_setopt(m_easyHandle, CURLOPT_SSL_VERIFYHOST, (long)(verifyHost ? 2 : 0));
    }
    checkCurl(code);
}

void CurlTransportAgent::setTimeout(int seconds)
{
    m_timeoutSeconds = seconds;
}

void CurlTransportAgent::shutdown()
{
}

void CurlTransportAgent::send(const char *data, size_t len)
{
    CURLcode code;

    m_replyLen = 0;
    m_message = data;
    m_messageSent = 0;
    m_messageLen = len;

    curl_slist_free_all(m_slist);
    m_slist = nullptr;
    
    // Setting Expect explicitly prevents problems with certain
    // proxies: if curl is allowed to depend on Expect, then it will
    // send the POST header and wait for the servers reply that it is
    // allowed to continue. This will always be the case with a correctly
    // configured SyncML and because some proxies reject unknown Expect
    // requests, it is better not used.
    m_slist = curl_slist_append(m_slist, "Expect:");

    std::string contentHeader("Content-Type: ");
    contentHeader += m_contentType;
    m_slist = curl_slist_append(m_slist, contentHeader.c_str());

    m_status = ACTIVE;
    if (m_timeoutSeconds) {
        m_sendStartTime = Timespec::monotonic();
    }
    m_aborting = false;
    if ((code = curl_easy_setopt(m_easyHandle, CURLOPT_PROGRESSDATA, static_cast<void *> (this)))||
        (code = curl_easy_setopt(m_easyHandle, CURLOPT_HTTPHEADER, m_slist)) ||
        (code = curl_easy_setopt(m_easyHandle, CURLOPT_POSTFIELDSIZE, len))
       ){
        m_status = CANCELED;
        checkCurl(code);
    }

    if ((code = curl_easy_perform(m_easyHandle))) {
        m_status = FAILED;
        checkCurl(code, false);
    } else {
        m_status = GOT_REPLY;
    }
}

void CurlTransportAgent::cancel()
{
    /* nothing to do */
}

TransportAgent::Status CurlTransportAgent::wait(bool noReply)
{
    return m_status;
}

void CurlTransportAgent::getReply(const char *&data, size_t &len, std::string &contentType)
{
    data = m_reply;
    len = m_replyLen;
    const char *curlContentType;
    if (!curl_easy_getinfo(m_easyHandle, CURLINFO_CONTENT_TYPE, &curlContentType) &&
        curlContentType) {
        contentType = curlContentType;
    } else {
        // unknown
        contentType = "";
    }
}

size_t CurlTransportAgent::writeData(void *buffer, size_t size) throw()
{
    bool increase = false;
    while (m_replyLen + size > m_replySize) {
        m_replySize = m_replySize ? m_replySize * 2 : 64 * 1024;
        increase = true;
    }

    if (increase) {
        m_reply = (char *)realloc(m_reply, m_replySize);
        if (!m_reply) {
            m_replySize = 0;
            m_replyLen = 0;
            return 0;
        }
    }

    memcpy(m_reply + m_replyLen,
           buffer,
           size);
    m_replyLen += size;
    return size;
}

size_t CurlTransportAgent::readData(void *buffer, size_t size) throw()
{
    size_t curr = std::min(size, m_messageLen - m_messageSent);

    memcpy(buffer, m_message + m_messageSent, curr);
    m_messageSent += curr;
    return curr;
}

void CurlTransportAgent::checkCurl(CURLcode code, bool exception)
{
    if (code) {
        if(exception){
            SE_THROW_EXCEPTION(TransportException, m_curlErrorText);
        }else {
            SE_LOG_INFO(NULL, "CurlTransport Failure: %s", m_curlErrorText);
        }
    }
}

int CurlTransportAgent::progressCallback(void* transport, double, double, double, double)
{
    CurlTransportAgent *agent = static_cast<CurlTransportAgent *> (transport);
    SuspendFlags &flags = SuspendFlags::getSuspendFlags();
    // check signals and abort transfer?
    flags.printSignals();
    if (flags.getState() == SuspendFlags::ABORT) {
        agent->setAborting (true);
        return -1;
    }
    return agent->processCallback();
}

int CurlTransportAgent::processCallback()
{
    if (m_timeoutSeconds) {
        Timespec curTime = Timespec::monotonic();
        if (curTime > m_sendStartTime + m_timeoutSeconds) {
            m_status = TIME_OUT;
            return -1;
        }
    }
    return 0;
}

SE_END_CXX

#endif // ENABLE_LIBCURL

