/**
 * @file _httpservice.cpp
 * @brief Internal definitions of the Http service 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 "_httpservice.h"

#include <boost/bind.hpp>
#include <boost/function.hpp>

#include "_httpoperation.h"
#include "_httprequestqueue.h"
#include "_httppolicy.h"
#include "_httplibcurl.h"
#include "_thread.h"
#include "_httpinternal.h"

#include "lltimer.h"
#include "llthread.h"
#include "llexception.h"
#include "llmemory.h"

namespace
{

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

} // end anonymous namespace


namespace LLCore
{

const HttpService::OptionDescriptor HttpService::sOptionDesc[] =
{ //    isLong     isDynamic  isGlobal    isClass
    {   true,       true,       true,       true,       false   },      // PO_CONNECTION_LIMIT
    {   true,       true,       false,      true,       false   },      // PO_PER_HOST_CONNECTION_LIMIT
    {   false,      false,      true,       false,      false   },      // PO_CA_PATH
    {   false,      false,      true,       false,      false   },      // PO_CA_FILE
    {   false,      true,       true,       false,      false   },      // PO_HTTP_PROXY
    {   true,       true,       true,       false,      false   },      // PO_LLPROXY
    {   true,       true,       true,       false,      false   },      // PO_TRACE
    {   true,       true,       false,      true,       false   },      // PO_ENABLE_PIPELINING
    {   true,       true,       false,      true,       false   },      // PO_THROTTLE_RATE
    {   false,      false,      true,       false,      true    }       // PO_SSL_VERIFY_CALLBACK
};
HttpService * HttpService::sInstance(NULL);
volatile HttpService::EState HttpService::sState(NOT_INITIALIZED);

HttpService::HttpService()
    : mRequestQueue(NULL),
      mExitRequested(0U),
      mThread(NULL),
      mPolicy(NULL),
      mTransport(NULL),
      mLastPolicy(0)
{}


HttpService::~HttpService()
{
    LL_PROFILE_ZONE_SCOPED_CATEGORY_NETWORK;
    mExitRequested = 1U;
    if (RUNNING == sState)
    {
        // Trying to kill the service object with a running thread
        // is a bit tricky.
        if (mRequestQueue)
        {
            if (mRequestQueue->stopQueue())
            {
                // Give mRequestQueue a chance to finish
                ms_sleep(10);
            }
        }

        if (mThread)
        {
            if (! mThread->timedJoin(250))
            {
                // Failed to join, expect problems ahead so do a hard termination.
                LL_WARNS(LOG_CORE) << "Destroying HttpService with running thread.  Expect problems." << LL_NEWLINE
                                    << "State: " << S32(sState)
                                    << " Last policy: " << U32(mLastPolicy)
                                    << LL_ENDL;

                mThread->cancel();
            }
        }
    }

    if (mRequestQueue)
    {
        mRequestQueue->release();
        mRequestQueue = NULL;
    }

    delete mTransport;
    mTransport = NULL;

    delete mPolicy;
    mPolicy = NULL;

    if (mThread)
    {
        mThread->release();
        mThread = NULL;
    }
}


void HttpService::init(HttpRequestQueue * queue)
{
    LL_PROFILE_ZONE_SCOPED_CATEGORY_NETWORK;
    llassert_always(! sInstance);
    llassert_always(NOT_INITIALIZED == sState);
    sInstance = new HttpService();

    queue->addRef();
    sInstance->mRequestQueue = queue;
    sInstance->mPolicy = new HttpPolicy(sInstance);
    sInstance->mTransport = new HttpLibcurl(sInstance);
    sState = INITIALIZED;
}


void HttpService::term()
{
    LL_PROFILE_ZONE_SCOPED_CATEGORY_NETWORK;
    if (sInstance)
    {
        if (RUNNING == sState && sInstance->mThread)
        {
            // Unclean termination.  Thread appears to be running.  We'll
            // try to give the worker thread a chance to cancel using the
            // exit flag...
            sInstance->mExitRequested = 1U;
            sInstance->mRequestQueue->stopQueue();

            // And a little sleep
            for (int i(0); i < 10 && RUNNING == sState; ++i)
            {
                ms_sleep(100);
            }
        }

        delete sInstance;
        sInstance = NULL;
    }
    sState = NOT_INITIALIZED;
}


HttpRequest::policy_t HttpService::createPolicyClass()
{
    mLastPolicy = mPolicy->createPolicyClass();
    return mLastPolicy;
}


bool HttpService::isStopped()
{
    // What is really wanted here is something like:
    //
    //     HttpService * service = instanceOf();
    //     return STOPPED == sState && (! service || ! service->mThread || ! service->mThread->joinable());
    //
    // But boost::thread is not giving me a consistent story on joinability
    // of a thread after it returns.  Debug and non-debug builds are showing
    // different behavior on Linux/Etch so we do a weaker test that may
    // not be globally correct (i.e. thread *is* stopping, may not have
    // stopped but will very soon):

    return STOPPED == sState;
}


/// Threading:  callable by consumer thread *once*.
void HttpService::startThread()
{
    LL_PROFILE_ZONE_SCOPED_CATEGORY_NETWORK;
    llassert_always(! mThread || STOPPED == sState);
    llassert_always(INITIALIZED == sState || STOPPED == sState);

    if (mThread)
    {
        mThread->release();
    }

    // Push current policy definitions, enable policy & transport components
    mPolicy->start();
    mTransport->start(mLastPolicy + 1);

    mThread = new LLCoreInt::HttpThread(boost::bind(&HttpService::threadRun, this, _1));
    sState = RUNNING;
}


/// Threading:  callable by worker thread.
void HttpService::stopRequested()
{
    mExitRequested = 1U;
}


/// Try to find the given request handle on any of the request
/// queues and cancel the operation.
///
/// @return         True if the request was canceled.
///
/// Threading:  callable by worker thread.
bool HttpService::cancel(HttpHandle handle)
{
    LL_PROFILE_ZONE_SCOPED_CATEGORY_NETWORK;
    bool canceled(false);

    // Request can't be on request queue so skip that.

    // Check the policy component's queues first
    canceled = mPolicy->cancel(handle);

    if (! canceled)
    {
        // If that didn't work, check transport's.
        canceled = mTransport->cancel(handle);
    }

    return canceled;
}


/// Threading:  callable by worker thread.
void HttpService::shutdown()
{
    LL_PROFILE_ZONE_SCOPED_CATEGORY_NETWORK;
    // Disallow future enqueue of requests
    mRequestQueue->stopQueue();

    // Cancel requests already on the request queue
    HttpRequestQueue::OpContainer ops;
    mRequestQueue->fetchAll(false, ops);

    for (HttpRequestQueue::OpContainer::iterator it = ops.begin();
        it != ops.end(); ++it)
    {
        (*it)->cancel();
    }
    ops.clear();

    // Shutdown transport canceling requests, freeing resources
    mTransport->shutdown();

    // And now policy
    mPolicy->shutdown();
}


// Working thread loop-forever method.  Gives time to
// each of the request queue, policy layer and transport
// layer pieces and then either sleeps for a small time
// or waits for a request to come in.  Repeats until
// requested to stop.
void HttpService::threadRun(LLCoreInt::HttpThread * thread)
{
    LL_PROFILER_SET_THREAD_NAME("HttpService");

    boost::this_thread::disable_interruption di;

    LLThread::registerThreadID();

    ELoopSpeed loop(REQUEST_SLEEP);
    while (! mExitRequested)
    {
        LL_PROFILE_ZONE_SCOPED_CATEGORY_NETWORK;
        try
        {
            loop = processRequestQueue(loop);

            // Process ready queue issuing new requests as needed
            ELoopSpeed new_loop = mPolicy->processReadyQueue();
            loop = (std::min)(loop, new_loop);

            // Give libcurl some cycles
            new_loop = mTransport->processTransport();
            loop = (std::min)(loop, new_loop);

            // Determine whether to spin, sleep briefly or sleep for next request
            if (REQUEST_SLEEP != loop)
            {
                ms_sleep(HTTP_SERVICE_LOOP_SLEEP_NORMAL_MS);
            }
        }
        catch (const LLContinueError&)
        {
            LOG_UNHANDLED_EXCEPTION("");
        }
        catch (std::bad_alloc&)
        {
            LLMemory::logMemoryInfo(TRUE);

            //output possible call stacks to log file.
            LLError::LLUserWarningMsg::showOutOfMemory();
            LLError::LLCallStacks::print();

            LL_ERRS() << "Bad memory allocation in HttpService::threadRun()!" << LL_ENDL;
        }
        catch (...)
        {
            CRASH_ON_UNHANDLED_EXCEPTION("");
        }
    }

    shutdown();
    sState = STOPPED;
}


HttpService::ELoopSpeed HttpService::processRequestQueue(ELoopSpeed loop)
{
    LL_PROFILE_ZONE_SCOPED_CATEGORY_NETWORK;
    HttpRequestQueue::OpContainer ops;
    const bool wait_for_req(REQUEST_SLEEP == loop);

    mRequestQueue->fetchAll(wait_for_req, ops);
    while (! ops.empty())
    {
        HttpOperation::ptr_t op(ops.front());
        ops.erase(ops.begin());

        // Process operation
        if (! mExitRequested)
        {
            // Setup for subsequent tracing
            long tracing(HTTP_TRACE_OFF);
            mPolicy->getGlobalOptions().get(HttpRequest::PO_TRACE, &tracing);
            op->mTracing = (std::max)(op->mTracing, int(tracing));

            if (op->mTracing > HTTP_TRACE_OFF)
            {
                LL_INFOS(LOG_CORE) << "TRACE, FromRequestQueue, Handle:  "
                                   << op->getHandle()
                                   << LL_ENDL;
            }

            // Stage
            op->stageFromRequest(this);
        }

        // Done with operation
        op.reset();
    }

    // Queue emptied, allow polling loop to sleep
    return REQUEST_SLEEP;
}


HttpStatus HttpService::getPolicyOption(HttpRequest::EPolicyOption opt, HttpRequest::policy_t pclass,
                                        long * ret_value)
{
    LL_PROFILE_ZONE_SCOPED_CATEGORY_NETWORK;
    if (opt < HttpRequest::PO_CONNECTION_LIMIT                                          // option must be in range
        || opt >= HttpRequest::PO_LAST                                                  // ditto
        || (! sOptionDesc[opt].mIsLong)                                                 // datatype is long
        || (pclass != HttpRequest::GLOBAL_POLICY_ID && pclass > mLastPolicy)            // pclass in valid range
        || (pclass == HttpRequest::GLOBAL_POLICY_ID && ! sOptionDesc[opt].mIsGlobal)    // global setting permitted
        || (pclass != HttpRequest::GLOBAL_POLICY_ID && ! sOptionDesc[opt].mIsClass))    // class setting permitted
                                                                                        // can always get, no dynamic check
    {
        return HttpStatus(HttpStatus::LLCORE, LLCore::HE_INVALID_ARG);
    }

    HttpStatus status;
    if (pclass == HttpRequest::GLOBAL_POLICY_ID)
    {
        HttpPolicyGlobal & opts(mPolicy->getGlobalOptions());

        status = opts.get(opt, ret_value);
    }
    else
    {
        HttpPolicyClass & opts(mPolicy->getClassOptions(pclass));

        status = opts.get(opt, ret_value);
    }

    return status;
}


HttpStatus HttpService::getPolicyOption(HttpRequest::EPolicyOption opt, HttpRequest::policy_t pclass,
                                        std::string * ret_value)
{
    LL_PROFILE_ZONE_SCOPED_CATEGORY_NETWORK;
    HttpStatus status(HttpStatus::LLCORE, LLCore::HE_INVALID_ARG);

    if (opt < HttpRequest::PO_CONNECTION_LIMIT                                          // option must be in range
        || opt >= HttpRequest::PO_LAST                                                  // ditto
        || (sOptionDesc[opt].mIsLong)                                                   // datatype is string
        || (pclass != HttpRequest::GLOBAL_POLICY_ID && pclass > mLastPolicy)            // pclass in valid range
        || (pclass == HttpRequest::GLOBAL_POLICY_ID && ! sOptionDesc[opt].mIsGlobal)    // global setting permitted
        || (pclass != HttpRequest::GLOBAL_POLICY_ID && ! sOptionDesc[opt].mIsClass))    // class setting permitted
                                                                                        // can always get, no dynamic check
    {
        return status;
    }

    // Only global has string values
    if (pclass == HttpRequest::GLOBAL_POLICY_ID)
    {
        HttpPolicyGlobal & opts(mPolicy->getGlobalOptions());

        status = opts.get(opt, ret_value);
    }

    return status;
}

HttpStatus HttpService::getPolicyOption(HttpRequest::EPolicyOption opt, HttpRequest::policy_t pclass,
    HttpRequest::policyCallback_t * ret_value)
{
    LL_PROFILE_ZONE_SCOPED_CATEGORY_NETWORK;
    HttpStatus status(HttpStatus::LLCORE, LLCore::HE_INVALID_ARG);

    if (opt < HttpRequest::PO_CONNECTION_LIMIT                                          // option must be in range
        || opt >= HttpRequest::PO_LAST                                                  // ditto
        || (sOptionDesc[opt].mIsLong)                                                   // datatype is string
        || (pclass != HttpRequest::GLOBAL_POLICY_ID && pclass > mLastPolicy)            // pclass in valid range
        || (pclass == HttpRequest::GLOBAL_POLICY_ID && !sOptionDesc[opt].mIsGlobal) // global setting permitted
        || (pclass != HttpRequest::GLOBAL_POLICY_ID && !sOptionDesc[opt].mIsClass)) // class setting permitted
        // can always get, no dynamic check
    {
        return status;
    }

    // Only global has callback values
    if (pclass == HttpRequest::GLOBAL_POLICY_ID)
    {
        HttpPolicyGlobal & opts(mPolicy->getGlobalOptions());

        status = opts.get(opt, ret_value);
    }

    return status;
}



HttpStatus HttpService::setPolicyOption(HttpRequest::EPolicyOption opt, HttpRequest::policy_t pclass,
                                        long value, long * ret_value)
{
    LL_PROFILE_ZONE_SCOPED_CATEGORY_NETWORK;
    HttpStatus status(HttpStatus::LLCORE, LLCore::HE_INVALID_ARG);

    if (opt < HttpRequest::PO_CONNECTION_LIMIT                                          // option must be in range
        || opt >= HttpRequest::PO_LAST                                                  // ditto
        || (! sOptionDesc[opt].mIsLong)                                                 // datatype is long
        || (pclass != HttpRequest::GLOBAL_POLICY_ID && pclass > mLastPolicy)            // pclass in valid range
        || (pclass == HttpRequest::GLOBAL_POLICY_ID && ! sOptionDesc[opt].mIsGlobal)    // global setting permitted
        || (pclass != HttpRequest::GLOBAL_POLICY_ID && ! sOptionDesc[opt].mIsClass)     // class setting permitted
        || (RUNNING == sState && ! sOptionDesc[opt].mIsDynamic))                        // dynamic setting permitted
    {
        return status;
    }

    if (pclass == HttpRequest::GLOBAL_POLICY_ID)
    {
        HttpPolicyGlobal & opts(mPolicy->getGlobalOptions());

        status = opts.set(opt, value);
        if (status && ret_value)
        {
            status = opts.get(opt, ret_value);
        }
    }
    else
    {
        HttpPolicyClass & opts(mPolicy->getClassOptions(pclass));

        status = opts.set(opt, value);
        if (status)
        {
            mTransport->policyUpdated(pclass);
            if (ret_value)
            {
                status = opts.get(opt, ret_value);
            }
        }
    }

    return status;
}


HttpStatus HttpService::setPolicyOption(HttpRequest::EPolicyOption opt, HttpRequest::policy_t pclass,
                                        const std::string & value, std::string * ret_value)
{
    LL_PROFILE_ZONE_SCOPED_CATEGORY_NETWORK;
    HttpStatus status(HttpStatus::LLCORE, LLCore::HE_INVALID_ARG);

    if (opt < HttpRequest::PO_CONNECTION_LIMIT                                          // option must be in range
        || opt >= HttpRequest::PO_LAST                                                  // ditto
        || (sOptionDesc[opt].mIsLong)                                                   // datatype is string
        || (pclass != HttpRequest::GLOBAL_POLICY_ID && pclass > mLastPolicy)            // pclass in valid range
        || (pclass == HttpRequest::GLOBAL_POLICY_ID && ! sOptionDesc[opt].mIsGlobal)    // global setting permitted
        || (pclass != HttpRequest::GLOBAL_POLICY_ID && ! sOptionDesc[opt].mIsClass)     // class setting permitted
        || (RUNNING == sState && ! sOptionDesc[opt].mIsDynamic))                        // dynamic setting permitted
    {
        return status;
    }

    // String values are always global (at this time).
    if (pclass == HttpRequest::GLOBAL_POLICY_ID)
    {
        HttpPolicyGlobal & opts(mPolicy->getGlobalOptions());

        status = opts.set(opt, value);
        if (status && ret_value)
        {
            status = opts.get(opt, ret_value);
        }
    }

    return status;
}

HttpStatus HttpService::setPolicyOption(HttpRequest::EPolicyOption opt, HttpRequest::policy_t pclass,
    HttpRequest::policyCallback_t value, HttpRequest::policyCallback_t * ret_value)
{
    LL_PROFILE_ZONE_SCOPED_CATEGORY_NETWORK;
    HttpStatus status(HttpStatus::LLCORE, LLCore::HE_INVALID_ARG);

    if (opt < HttpRequest::PO_CONNECTION_LIMIT                                          // option must be in range
        || opt >= HttpRequest::PO_LAST                                                  // ditto
        || (sOptionDesc[opt].mIsLong)                                                   // datatype is string
        || (pclass != HttpRequest::GLOBAL_POLICY_ID && pclass > mLastPolicy)            // pclass in valid range
        || (pclass == HttpRequest::GLOBAL_POLICY_ID && !sOptionDesc[opt].mIsGlobal) // global setting permitted
        || (pclass != HttpRequest::GLOBAL_POLICY_ID && !sOptionDesc[opt].mIsClass)      // class setting permitted
        || (RUNNING == sState && !sOptionDesc[opt].mIsDynamic))                     // dynamic setting permitted
    {
        return status;
    }

    // Callbacks values are always global (at this time).
    if (pclass == HttpRequest::GLOBAL_POLICY_ID)
    {
        HttpPolicyGlobal & opts(mPolicy->getGlobalOptions());

        status = opts.set(opt, value);
        if (status && ret_value)
        {
            status = opts.get(opt, ret_value);
        }
    }

    return status;
}


}  // end namespace LLCore