/*
**
**      Copyright (c) 2000-2005 ChoiceStream, Inc.  All Rights Reserved
**
**      ChoiceStream
**      210 Broadway
**      Cambridge, MA 02139
**
**  $Id: httpconnection.cpp,v 1.15 2005/03/23 02:03:27 cweikart Exp $
**
**/


#include "csm_first.h"

#include "curl.h"
extern "C" {
#include "ares.h"
}
#include "easy.h"
#include "httpconnection.h"
#include "strutil.h"
#include "csutil.h"
#include "urn.h"
#include "lock.h"
#include "counted_ptr.h"

#include <iostream>
#include "funtimer.h"

using namespace csm;
using namespace std;


namespace {

    Lockable curlInitLock;
    static bool curlInitialized = false;

    size_t WriteMemoryCallbackStdString(void *ptr, size_t size, size_t nmemb, void *data)
    {
        register int realsize = size * nmemb;
        string & contents = *(string *)(data);
        contents.append((char *)ptr,realsize);
        return realsize;
    }

    size_t HeaderCallbackMap(void *ptr, size_t size, size_t nmemb, void *data)
    {
        register int realsize = size * nmemb;
        HttpHeaders & headers = *(HttpHeaders *)(data);
        string temp = (char *)ptr;
        //contents.push_back(temp);
        string::size_type pos = temp.find(":");
        if(pos == string::npos)
            return realsize;
        string name  = temp.substr(0, temp.length()- (pos-2));
        string value = temp.substr(pos+2);
        trim(&value);
        headers[temp].push_back(value);
        return realsize;
    }

    // something of a dirty trick. we want curl_global_init to be called 
    // when we first need a curl connection, and we want curl_global cleanup
    // to be called when we shut down. So we create a singleton object 
    // which calls curl_global_init in its CTor, and curl_global_cleanup
    // in its DTor, and we sneakily name our instance method initialize
    class CurlInitializer;
    typedef counted_ptr<CurlInitializer> RefCurlInitializer;

    class CurlInitializer
    {
      public:
        static RefCurlInitializer initialize();
        ~CurlInitializer()
        {
            curl_global_cleanup();
        }

      private:
        CurlInitializer()
        {
            curl_global_init(CURL_GLOBAL_ALL);      
        };

        static RefCurlInitializer instance_;
    };

    RefCurlInitializer CurlInitializer::initialize() {
        if (!curlInitialized) {
            Gatekeeper g(&curlInitLock);
            if (!curlInitialized) {
                instance_ = RefCurlInitializer(new CurlInitializer);
                curlInitialized = true;
            }
        }
        return instance_;
    }
    RefCurlInitializer CurlInitializer::instance_;
}


//LIMITING TIME SPENT ON ONE CONNECTION
//If you want to limit the amount of time allowed on the underlying
//socket connection, set a value in milliseconds > 0 in this Ctor. Internally,
//this object will then destroy and recreate its underlying curlhandle (which
//will destroy the socket connection that had been kept alive by the curlhandle)
//after that much elapsed time. If you would like potentially unlimited time
//on the socket, pass -1
HttpConnection::HttpConnection(long maxtime,
                               const string & dns1,
                               const string & dns2):maxTime_(maxtime), 
                                                         postBody_(NULL),
                                                         dns1_(dns1),
                                                         dns2_(dns2)
{
    init();
}

HttpConnection::~HttpConnection()
{
    cleanup();
}

void HttpConnection::init()
{
    handleTime_ = tickCount();
    CurlInitializer::initialize();

    int aresCode = 0;
    CURLcode res;
    if(dns1_.empty() && dns2_.empty())
    {
        res = curl_easy_init_e(&curlHandle_, &aresCode);
    }
    else
    {
        res = curl_easy_init_dns(&curlHandle_, 
                                 &aresCode, 
                                 dns1_.c_str(),
                                 dns2_.c_str());
    }
       
    if (CURLE_OK != res)
    {
        CSM_LOG_ERROR(logger(), "init failed, reason: " << curl_easy_strerror(res));
        if(aresCode != ARES_SUCCESS)
        {
            CSM_LOG_ERROR(logger(), "init failed in ares, reason: " << ares_strerror(aresCode));
        }
    }
    if (NULL == curlHandle_)
    {
        CSM_LOG_ERROR(logger(), "init failed: curlHandle_ == NULL");
    }
}

void HttpConnection::recreateHandle()
{
    cleanup();
    init();
}

void HttpConnection::cleanup()
{
    if (NULL != curlHandle_)
    {
        curl_easy_cleanup(curlHandle_);
        curlHandle_ = NULL;
    }
    if (NULL != postBody_)
    {
        curl_formfree(postBody_);
        postBody_ = NULL;
    }
}

void HttpConnection::sendRequest(const HttpRequest& request, HttpResponse& response)
{
    long elapsed = tickCount() - handleTime_;
    if(maxTime_ != -1 && elapsed >= maxTime_)
        recreateHandle();

    switch (request.method)
    {
      case HTTP_GET:
        sendGetRequest(request, response);
        break;
      case HTTP_POST:
        if (request.multipart)
        {
            sendPostRequest(request, response);
        }
        else
        {
            sendSinglePartPost(request, response);
        }
        break;
      default:
        CSM_LOG_ERROR(logger(), "sendRequest: request.method " << request.method << " is unknown");
        break;
    }
}

bool HttpConnection::initializeRequest(const csm::HttpRequest& request)
{
    if (NULL == curlHandle_) {
        return false;
    }
    curl_easy_setopt(curlHandle_, CURLOPT_NOSIGNAL, 1);
    curl_easy_setopt(curlHandle_, CURLOPT_NOPROGRESS, 1);
    curl_easy_setopt(curlHandle_, CURLOPT_WRITEFUNCTION, WriteMemoryCallbackStdString);

    curl_easy_setopt(curlHandle_, CURLOPT_USERAGENT, request.userAgent.c_str());
    curl_easy_setopt(curlHandle_, CURLOPT_CONNECTTIMEOUT, request.connectTimeout);
    curl_easy_setopt(curlHandle_, CURLOPT_TIMEOUT, request.timeout); // read timeout
    if(request.useCookies)
        curl_easy_setopt(curlHandle_, CURLOPT_COOKIEJAR, request.cookieFile.c_str());
    
    curl_easy_setopt(curlHandle_, CURLOPT_FOLLOWLOCATION, request.followRedirect);
    curl_easy_setopt(curlHandle_, CURLOPT_MAXREDIRS, request.maxRedirectFollow);
    return true;
}

void HttpConnection::addFormInfo(const HttpFormList & forms)
{
    struct curl_httppost* post = NULL;
    struct curl_httppost* last = NULL;
    for(HttpFormList::const_iterator f = forms.begin();
        f != forms.end();
        ++f)
    {
        curl_formadd(&post, 
                     &last, 
                     CURLFORM_COPYNAME, 
                     (*f).name.c_str(),
                     CURLFORM_COPYCONTENTS, 
                     (*f).contents.c_str(),
                     CURLFORM_CONTENTTYPE, 
                     (*f).type.c_str(),
                     CURLFORM_END);
    }
    postBody_  = post;
}

void HttpConnection::sendGetRequest(const HttpRequest& request, 
                                    HttpResponse& response)
{
    if (!initializeRequest(request)) 
    {
        response.status = HTTP_NULL_STATUS;
        return;
    }

    curl_easy_setopt(curlHandle_, CURLOPT_HEADERFUNCTION, HeaderCallbackMap);
    curl_easy_setopt(curlHandle_, CURLOPT_HEADERDATA,     &response.headers);
    curl_easy_setopt(curlHandle_, CURLOPT_WRITEDATA, (void *)&response.body);
    curl_easy_setopt(curlHandle_, CURLOPT_URL, request.url.c_str());

    CURLcode code = curl_easy_perform(curlHandle_);
    if (CURLE_OK != code) 
    {
        CSM_LOG_ERROR(logger(), "sendGetRequest: " << curl_easy_strerror(code) << " for url: " << request.url);
    }

    curl_easy_getinfo(curlHandle_, CURLINFO_REDIRECT_COUNT, &response.redirectsFollowed); 
    curl_easy_getinfo(curlHandle_, CURLINFO_RESPONSE_CODE, &response.status);
    char *newurl = NULL;
    curl_easy_getinfo(curlHandle_, CURLINFO_EFFECTIVE_URL, &newurl);
    if (NULL != newurl) {
        response.effectiveUrl = newurl;
    }
}

void HttpConnection::sendPostRequest(const HttpRequest& request, 
                                     HttpResponse& response)
{
    if (!initializeRequest(request)) {
        response.status = HTTP_NULL_STATUS;
        return;
    }
    curl_easy_setopt(curlHandle_, CURLOPT_HEADERFUNCTION, HeaderCallbackMap);
    curl_easy_setopt(curlHandle_, CURLOPT_HEADERDATA,     &response.headers);
    curl_easy_setopt(curlHandle_, CURLOPT_WRITEDATA, (void *)&response.body);
    curl_easy_setopt(curlHandle_, CURLOPT_URL, request.url.c_str());
    addFormInfo(request.body);
    curl_easy_setopt(curlHandle_, CURLOPT_HTTPPOST, postBody_);

    CURLcode code = curl_easy_perform(curlHandle_);
    if (CURLE_OK != code)
    {
        CSM_LOG_ERROR(logger(), "sendPostRequest: " << curl_easy_strerror(code) << " for url: " << request.url);
    }
    curl_easy_getinfo(curlHandle_, CURLINFO_REDIRECT_COUNT, &response.redirectsFollowed); 
    curl_easy_getinfo(curlHandle_, CURLINFO_RESPONSE_CODE, &response.status); 
    char *newurl = NULL;
    curl_easy_getinfo(curlHandle_, CURLINFO_EFFECTIVE_URL, &newurl); 
    if (NULL != newurl) 
    {
        response.effectiveUrl = newurl;
    }
}

void HttpConnection::sendSinglePartPost(const HttpRequest& request, HttpResponse& response)
{
    if (!initializeRequest(request)) {
        response.status = HTTP_NULL_STATUS;
        return;
    }
    curl_easy_setopt(curlHandle_, CURLOPT_HEADERFUNCTION, HeaderCallbackMap);
    curl_easy_setopt(curlHandle_, CURLOPT_HEADERDATA,     &response.headers);
    curl_easy_setopt(curlHandle_, CURLOPT_WRITEDATA, (void *)&response.body);
    curl_easy_setopt(curlHandle_, CURLOPT_URL, request.url.c_str());

    curl_slist *slist=NULL; 

    if (!request.body.empty())
    {
        curl_easy_setopt(curlHandle_, CURLOPT_POST, 1);
        slist = curl_slist_append(slist, ("Content-Type: " + request.body[0].type).c_str());
        curl_easy_setopt(curlHandle_, CURLOPT_HTTPHEADER, slist);

        curl_easy_setopt(curlHandle_, CURLOPT_POSTFIELDS, request.body[0].contents.data());
        curl_easy_setopt(curlHandle_, CURLOPT_POSTFIELDSIZE, request.body[0].contents.length());
    }

    CURLcode code = curl_easy_perform(curlHandle_);
    if (CURLE_OK != code) {
        CSM_LOG_ERROR(logger(), "sendSinglePartPost: " << curl_easy_strerror(code) << " for url: " << request.url);
    }

    curl_easy_getinfo(curlHandle_, CURLINFO_REDIRECT_COUNT, &response.redirectsFollowed); 
    curl_easy_getinfo(curlHandle_, CURLINFO_RESPONSE_CODE, &response.status);
    char *newurl = NULL;
    curl_easy_getinfo(curlHandle_, CURLINFO_EFFECTIVE_URL, &newurl); 
    if (NULL != newurl) {
        response.effectiveUrl = newurl;
    }
    if (NULL != slist) {
        curl_slist_free_all(slist);
    }
}
