/**
 * @file _httplibcurl.cpp
 * @brief Internal definitions of the Http libcurl thread
 *
 * $LicenseInfo:firstyear=2012&license=viewerlgpl$
 * Second Life Viewer Source Code
 * Copyright (C) 2012-2014, Linden Research, Inc.
 *
 * 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;
 * version 2.1 of the License only.
 *
 * 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
 *
 * Linden Research, Inc., 945 Battery Street, San Francisco, CA  94111  USA
 * $/LicenseInfo$
 */

#include "_httplibcurl.h"

#include "httpheaders.h"
#include "bufferarray.h"
#include "_httpoprequest.h"
#include "_httppolicy.h"

#include "llhttpconstants.h"

namespace
{

// Error testing and reporting for libcurl status codes
void check_curl_multi_code(CURLMcode code);
void check_curl_multi_code(CURLMcode code, int curl_setopt_option);

// This is a template because different 'option' values require different
// types for 'ARG'. Just pass them through unchanged (by value).
template <typename ARG>
void check_curl_multi_setopt(CURLM* handle, CURLMoption option, ARG argument)
{
    CURLMcode code = curl_multi_setopt(handle, option, argument);
    check_curl_multi_code(code, option);
}

static const char * const LOG_CORE("CoreHttp");

} // end anonymous namespace


namespace LLCore
{


HttpLibcurl::HttpLibcurl(HttpService * service)
    : mService(service),
      mHandleCache(),
      mPolicyCount(0),
      mMultiHandles(NULL),
      mActiveHandles(NULL),
      mDirtyPolicy(NULL)
{}


HttpLibcurl::~HttpLibcurl()
{
    shutdown();

    mService = NULL;
}


void HttpLibcurl::shutdown()
{
    while (! mActiveOps.empty())
    {
        HttpOpRequest::ptr_t op(* mActiveOps.begin());
        mActiveOps.erase(mActiveOps.begin());

        cancelRequest(op);
    }

    if (mMultiHandles)
    {
        for (int policy_class(0); policy_class < mPolicyCount; ++policy_class)
        {
            if (mMultiHandles[policy_class])
            {
                curl_multi_cleanup(mMultiHandles[policy_class]);
                mMultiHandles[policy_class] = 0;
            }
        }

        delete [] mMultiHandles;
        mMultiHandles = NULL;

        delete [] mActiveHandles;
        mActiveHandles = NULL;

        delete [] mDirtyPolicy;
        mDirtyPolicy = NULL;
    }

    mPolicyCount = 0;
}


void HttpLibcurl::start(int policy_count)
{
    LL_PROFILE_ZONE_SCOPED_CATEGORY_NETWORK;
    llassert_always(policy_count <= HTTP_POLICY_CLASS_LIMIT);
    llassert_always(! mMultiHandles);                   // One-time call only

    mPolicyCount = policy_count;
    mMultiHandles = new CURLM * [mPolicyCount];
    mActiveHandles = new int [mPolicyCount];
    mDirtyPolicy = new bool [mPolicyCount];

    for (int policy_class(0); policy_class < mPolicyCount; ++policy_class)
    {
        if (NULL == (mMultiHandles[policy_class] = curl_multi_init()))
        {
            LL_ERRS(LOG_CORE) << "Failed to allocate multi handle in libcurl."
                              << LL_ENDL;
        }
        mActiveHandles[policy_class] = 0;
        mDirtyPolicy[policy_class] = false;
        policyUpdated(policy_class);
    }
}


// Give libcurl some cycles, invoke it's callbacks, process
// completed requests finalizing or issuing retries as needed.
//
// If active list goes empty *and* we didn't queue any
// requests for retry, we return a request for a hard
// sleep otherwise ask for a normal polling interval.
HttpService::ELoopSpeed HttpLibcurl::processTransport()
{
    LL_PROFILE_ZONE_SCOPED_CATEGORY_NETWORK;
    HttpService::ELoopSpeed ret(HttpService::REQUEST_SLEEP);

    // Give libcurl some cycles to do I/O & callbacks
    for (int policy_class(0); policy_class < mPolicyCount; ++policy_class)
    {
        if (! mMultiHandles[policy_class])
        {
            // No handle, nothing to do.
            continue;
        }
        if (! mActiveHandles[policy_class])
        {
            // If we've gone quiet and there's a dirty update, apply it,
            // otherwise we're done.
            if (mDirtyPolicy[policy_class])
            {
                policyUpdated(policy_class);
            }
            continue;
        }

        int running(0);
        CURLMcode status(CURLM_CALL_MULTI_PERFORM);
        do
        {
            LL_PROFILE_ZONE_NAMED_CATEGORY_NETWORK("httppt - curl_multi_perform");
            running = 0;
            status = curl_multi_perform(mMultiHandles[policy_class], &running);
        }
        while (0 != running && CURLM_CALL_MULTI_PERFORM == status);

        // Run completion on anything done
        CURLMsg * msg(NULL);
        int msgs_in_queue(0);
        {
            LL_PROFILE_ZONE_NAMED_CATEGORY_NETWORK("httppt - curl_multi_info_read");
            while ((msg = curl_multi_info_read(mMultiHandles[policy_class], &msgs_in_queue)))
            {
                if (CURLMSG_DONE == msg->msg)
                {
                    CURL* handle(msg->easy_handle);
                    CURLcode result(msg->data.result);

                    completeRequest(mMultiHandles[policy_class], handle, result);
                    handle = NULL;                  // No longer valid on return
                    ret = HttpService::NORMAL;      // If anything completes, we may have a free slot.
                                                    // Turning around quickly reduces connection gap by 7-10mS.
                }
                else if (CURLMSG_NONE == msg->msg)
                {
                    // Ignore this... it shouldn't mean anything.
                    ;
                }
                else
                {
                    LL_WARNS_ONCE(LOG_CORE) << "Unexpected message from libcurl.  Msg code:  "
                        << msg->msg
                        << LL_ENDL;
                }
                msgs_in_queue = 0;
            }
        }
    }

    if (! mActiveOps.empty())
    {
        ret = HttpService::NORMAL;
    }
    return ret;
}


// Caller has provided us with a ref count on op.
void HttpLibcurl::addOp(const HttpOpRequest::ptr_t &op)
{
    LL_PROFILE_ZONE_SCOPED_CATEGORY_NETWORK;
    llassert_always(op->mReqPolicy < mPolicyCount);
    llassert_always(mMultiHandles[op->mReqPolicy] != NULL);

    // Create standard handle
    if (! op->prepareRequest(mService))
    {
        // Couldn't issue request, fail with notification
        // *TODO:  Need failure path
        return;
    }

    // Make the request live
    CURLMcode code;
    code = curl_multi_add_handle(mMultiHandles[op->mReqPolicy], op->mCurlHandle);
    if (CURLM_OK != code)
    {
        // *TODO:  Better cleanup and recovery but not much we can do here.
        check_curl_multi_code(code);
        return;
    }
    op->mCurlActive = true;
    mActiveOps.insert(op);
    ++mActiveHandles[op->mReqPolicy];

    if (op->mTracing > HTTP_TRACE_OFF)
    {
        HttpPolicy & policy(mService->getPolicy());

        LL_INFOS(LOG_CORE) << "TRACE, ToActiveQueue, Handle:  "
                            << op->getHandle()
                            << ", Actives:  " << mActiveOps.size()
                            << ", Readies:  " << policy.getReadyCount(op->mReqPolicy)
                            << LL_ENDL;
    }
}


// Implements the transport part of any cancel operation.
// See if the handle is an active operation and if so,
// use the more complicated transport-based cancellation
// method to kill the request.
bool HttpLibcurl::cancel(HttpHandle handle)
{
    LL_PROFILE_ZONE_SCOPED_CATEGORY_NETWORK;
    HttpOpRequest::ptr_t op = HttpOpRequest::fromHandle<HttpOpRequest>(handle);
    active_set_t::iterator it(mActiveOps.find(op));
    if (mActiveOps.end() == it)
    {
        return false;
    }

    // Cancel request
    cancelRequest(op);

    // Drop references
    mActiveOps.erase(it);
    --mActiveHandles[op->mReqPolicy];

    return true;
}


// *NOTE:  cancelRequest logic parallels completeRequest logic.
// Keep them synchronized as necessary.  Caller is expected to
// remove the op from the active list and release the op *after*
// calling this method.  It must be called first to deliver the
// op to the reply queue with refcount intact.
void HttpLibcurl::cancelRequest(const HttpOpRequest::ptr_t &op)
{
    LL_PROFILE_ZONE_SCOPED_CATEGORY_NETWORK;
    // Deactivate request
    op->mCurlActive = false;

    // Detach from multi and recycle handle
    curl_multi_remove_handle(mMultiHandles[op->mReqPolicy], op->mCurlHandle);
    mHandleCache.freeHandle(op->mCurlHandle);
    op->mCurlHandle = NULL;

    // Tracing
    if (op->mTracing > HTTP_TRACE_OFF)
    {
        LL_INFOS(LOG_CORE) << "TRACE, RequestCanceled, Handle:  "
                           << op->getHandle()
                           << ", Status:  " << op->mStatus.toTerseString()
                           << LL_ENDL;
    }

    // Cancel op and deliver for notification
    op->cancel();
}


// *NOTE:  cancelRequest logic parallels completeRequest logic.
// Keep them synchronized as necessary.
bool HttpLibcurl::completeRequest(CURLM * multi_handle, CURL * handle, CURLcode status)
{
    LL_PROFILE_ZONE_SCOPED_CATEGORY_NETWORK;
    HttpHandle ophandle(NULL);

    CURLcode ccode(CURLE_OK);

    ccode = curl_easy_getinfo(handle, CURLINFO_PRIVATE, &ophandle);
    if (ccode)
    {
        LL_WARNS(LOG_CORE) << "libcurl error: " << ccode << " Unable to retrieve operation handle from CURL handle" << LL_ENDL;
        return false;
    }
    HttpOpRequest::ptr_t op(HttpOpRequest::fromHandle<HttpOpRequest>(ophandle));

    if (!op)
    {
        LL_WARNS() << "Unable to locate operation by handle. May have expired!" << LL_ENDL;
        return false;
    }

    if (handle != op->mCurlHandle || ! op->mCurlActive)
    {
        LL_WARNS(LOG_CORE) << "libcurl handle and HttpOpRequest handle in disagreement or inactive request."
                           << "  Handle:  " << static_cast<HttpHandle>(handle)
                           << LL_ENDL;
        return false;
    }

    active_set_t::iterator it(mActiveOps.find(op));
    if (mActiveOps.end() == it)
    {
        LL_WARNS(LOG_CORE) << "libcurl completion for request not on active list.  Continuing."
                           << "  Handle:  " << static_cast<HttpHandle>(handle)
                           << LL_ENDL;
        return false;
    }

    // Deactivate request
    mActiveOps.erase(it);
    --mActiveHandles[op->mReqPolicy];
    op->mCurlActive = false;

    // Set final status of request if it hasn't failed by other mechanisms yet
    if (op->mStatus)
    {
        op->mStatus = HttpStatus(HttpStatus::EXT_CURL_EASY, status);
    }
    if (op->mStatus)
    {
        // note: CURLINFO_RESPONSE_CODE requires a long - https://curl.haxx.se/libcurl/c/CURLINFO_RESPONSE_CODE.html
        long http_status(HTTP_OK);

        if (handle)
        {
            ccode = curl_easy_getinfo(handle, CURLINFO_RESPONSE_CODE, &http_status);
            if (ccode == CURLE_OK)
            {
                if (http_status >= 100 && http_status <= 999)
                {
                    char * cont_type(NULL);
                    ccode = curl_easy_getinfo(handle, CURLINFO_CONTENT_TYPE, &cont_type);
                    if (ccode == CURLE_OK)
                    {
                        if (cont_type)
                        {
                            op->mReplyConType = cont_type;
                        }
                    }
                    else
                    {
                        LL_WARNS(LOG_CORE) << "CURL error:" << ccode << " Attempting to get content type." << LL_ENDL;
                    }
                    op->mStatus = HttpStatus(http_status);
                }
                else
                {
                    LL_WARNS(LOG_CORE) << "Invalid HTTP response code ("
                        << http_status << ") received from server."
                        << LL_ENDL;
                    op->mStatus = HttpStatus(HttpStatus::LLCORE, HE_INVALID_HTTP_STATUS);
                }
            }
            else
            {
                op->mStatus = HttpStatus(HttpStatus::LLCORE, HE_INVALID_HTTP_STATUS);
            }
        }
        else
        {
            LL_WARNS(LOG_CORE) << "Attempt to retrieve status from NULL handle!" << LL_ENDL;
        }
    }

    if (multi_handle && handle)
    {
        // Detach from multi and recycle handle
        curl_multi_remove_handle(multi_handle, handle);
        mHandleCache.freeHandle(op->mCurlHandle);
    }
    else
    {
        LL_WARNS(LOG_CORE) << "Curl multi_handle or handle is NULL on remove! multi:"
            << std::hex << multi_handle << " h:" << std::hex << handle << std::dec << LL_ENDL;
    }

    op->mCurlHandle = NULL;

    // Tracing
    if (op->mTracing > HTTP_TRACE_OFF)
    {
        LL_INFOS(LOG_CORE) << "TRACE, RequestComplete, Handle:  "
                            << op->getHandle()
                            << ", Status:  " << op->mStatus.toTerseString()
                            << LL_ENDL;
    }

    // Dispatch to next stage
    HttpPolicy & policy(mService->getPolicy());
    bool still_active(policy.stageAfterCompletion(op));

    return still_active;
}


int HttpLibcurl::getActiveCount() const
{
    return mActiveOps.size();
}


int HttpLibcurl::getActiveCountInClass(int policy_class) const
{
    llassert_always(policy_class < mPolicyCount);

    return mActiveHandles ? mActiveHandles[policy_class] : 0;
}

void HttpLibcurl::policyUpdated(int policy_class)
{
    LL_PROFILE_ZONE_SCOPED_CATEGORY_NETWORK;
    if (policy_class < 0 || policy_class >= mPolicyCount || ! mMultiHandles)
    {
        return;
    }

    HttpPolicy & policy(mService->getPolicy());

    if (! mActiveHandles[policy_class])
    {
        // Clear to set options.  As of libcurl 7.37.0, if a pipelining
        // multi handle has active requests and you try to set the
        // multi handle to non-pipelining, the library gets very angry
        // and goes off the rails corrupting memory.  A clue that you're
        // about to crash is that you'll get a missing server response
        // error (curl code 9).  So, if options are to be set, we let
        // the multi handle run out of requests, then set options, and
        // re-enable request processing.
        //
        // All of this stall mechanism exists for this reason.  If
        // libcurl becomes more resilient later, it should be possible
        // to remove all of this.  The connection limit settings are fine,
        // it's just that pipelined-to-non-pipelined transition that
        // is fatal at the moment.

        HttpPolicyClass & options(policy.getClassOptions(policy_class));
        CURLM * multi_handle(mMultiHandles[policy_class]);

        // Enable policy if stalled
        policy.stallPolicy(policy_class, false);
        mDirtyPolicy[policy_class] = false;

        if (options.mPipelining > 1)
        {
            // We'll try to do pipelining on this multihandle
            check_curl_multi_setopt(multi_handle,
                                     CURLMOPT_PIPELINING,
                                     1L);
            check_curl_multi_setopt(multi_handle,
                                     CURLMOPT_MAX_PIPELINE_LENGTH,
                                     long(options.mPipelining));
            check_curl_multi_setopt(multi_handle,
                                     CURLMOPT_MAX_HOST_CONNECTIONS,
                                     long(options.mPerHostConnectionLimit));
            check_curl_multi_setopt(multi_handle,
                                     CURLMOPT_MAX_TOTAL_CONNECTIONS,
                                     long(options.mConnectionLimit));
        }
        else
        {
            check_curl_multi_setopt(multi_handle,
                                     CURLMOPT_PIPELINING,
                                     0L);
            check_curl_multi_setopt(multi_handle,
                                     CURLMOPT_MAX_HOST_CONNECTIONS,
                                     0L);
            check_curl_multi_setopt(multi_handle,
                                     CURLMOPT_MAX_TOTAL_CONNECTIONS,
                                     long(options.mConnectionLimit));
        }
    }
    else if (! mDirtyPolicy[policy_class])
    {
        // Mark policy dirty and request a stall in the policy.
        // When policy goes idle, we'll re-invoke this method
        // and perform the change.  Don't allow this thread to
        // sleep while we're waiting for quiescence, we'll just
        // stop processing.
        mDirtyPolicy[policy_class] = true;
        policy.stallPolicy(policy_class, true);
    }
}

// ---------------------------------------
// HttpLibcurl::HandleCache
// ---------------------------------------

HttpLibcurl::HandleCache::HandleCache()
    : mHandleTemplate(NULL)
{
    mCache.reserve(50);
}


HttpLibcurl::HandleCache::~HandleCache()
{
    if (mHandleTemplate)
    {
        curl_easy_cleanup(mHandleTemplate);
        mHandleTemplate = NULL;
    }

    for (handle_cache_t::iterator it(mCache.begin()); mCache.end() != it; ++it)
    {
        curl_easy_cleanup(*it);
    }
    mCache.clear();
}


CURL * HttpLibcurl::HandleCache::getHandle()
{
    CURL * ret(NULL);

    if (! mCache.empty())
    {
        // Fastest path to handle
        ret = mCache.back();
        mCache.pop_back();
    }
    else if (mHandleTemplate)
    {
        // Still fast path
        ret = curl_easy_duphandle(mHandleTemplate);
    }
    else
    {
        // When all else fails
        ret = curl_easy_init();
    }

    return ret;
}


void HttpLibcurl::HandleCache::freeHandle(CURL * handle)
{
    if (! handle)
    {
        return;
    }

    curl_easy_reset(handle);
    if (! mHandleTemplate)
    {
        // Save the first freed handle as a template.
        mHandleTemplate = handle;
    }
    else
    {
        // Otherwise add it to the cache
        if (mCache.size() >= mCache.capacity())
        {
            mCache.reserve(mCache.capacity() + 50);
        }
        mCache.push_back(handle);
    }
}


// ---------------------------------------
// Free functions
// ---------------------------------------


struct curl_slist * append_headers_to_slist(const HttpHeaders::ptr_t &headers, struct curl_slist * slist)
{
    const HttpHeaders::const_iterator end(headers->end());
    for (HttpHeaders::const_iterator it(headers->begin()); end != it; ++it)
    {
        static const char sep[] = ": ";
        std::string header;
        header.reserve((*it).first.size() + (*it).second.size() + sizeof(sep));
        header.append((*it).first);
        header.append(sep);
        header.append((*it).second);

        slist = curl_slist_append(slist, header.c_str());
    }
    return slist;
}


}  // end namespace LLCore


namespace
{

void check_curl_multi_code(CURLMcode code, int curl_setopt_option)
{
    if (CURLM_OK != code)
    {
        LL_WARNS(LOG_CORE) << "libcurl multi error detected:  " << curl_multi_strerror(code)
                           << ", curl_multi_setopt option:  " << curl_setopt_option
                           << LL_ENDL;
    }
}


void check_curl_multi_code(CURLMcode code)
{
    if (CURLM_OK != code)
    {
        LL_WARNS(LOG_CORE) << "libcurl multi error detected:  " << curl_multi_strerror(code)
                           << LL_ENDL;
    }
}

}  // end anonymous namespace