/** **************************************************************************
 * request_context.c
 * 
 * Copyright 2008 Bryan Ischo <bryan@ischo.com>
 * 
 * This file is part of libs3.
 * 
 * libs3 is free software: you can redistribute it and/or modify it under the
 * terms of the GNU General Public License as published by the Free Software
 * Foundation, version 3 of the License.
 *
 * In addition, as a special exception, the copyright holders give
 * permission to link the code of this library and its programs with the
 * OpenSSL library, and distribute linked combinations including the two.
 *
 * libs3 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 General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License version 3
 * along with libs3, in a file named COPYING.  If not, see
 * <http://www.gnu.org/licenses/>.
 *
 ************************************************************************** **/

#include <curl/curl.h>
#include <stdlib.h>
#include <sys/select.h>
#include "request.h"
#include "request_context.h"


S3Status S3_create_request_context(S3RequestContext **requestContextReturn)
{
    *requestContextReturn = 
        (S3RequestContext *) malloc(sizeof(S3RequestContext));
    
    if (!*requestContextReturn) {
        return S3StatusOutOfMemory;
    }
    
    if (!((*requestContextReturn)->curlm = curl_multi_init())) {
        free(*requestContextReturn);
        return S3StatusOutOfMemory;
    }

    (*requestContextReturn)->requests = 0;

    return S3StatusOK;
}


void S3_destroy_request_context(S3RequestContext *requestContext)
{
    curl_multi_cleanup(requestContext->curlm);

    // For each request in the context, call back its done method with
    // 'interrupted' status
    Request *r = requestContext->requests, *rFirst = r;
    
    if (r) do {
        r->status = S3StatusInterrupted;
        Request *rNext = r->next;
        request_finish(r);
        r = rNext;
    } while (r != rFirst);

    free(requestContext);
}


S3Status S3_runall_request_context(S3RequestContext *requestContext)
{
    int requestsRemaining;
    do {
        fd_set readfds, writefds, exceptfds;
        FD_ZERO(&readfds);
        FD_ZERO(&writefds);
        FD_ZERO(&exceptfds);
        int maxfd;
        S3Status status = S3_get_request_context_fdsets
            (requestContext, &readfds, &writefds, &exceptfds, &maxfd);
        if (status != S3StatusOK) {
            return status;
        }
        // curl will return -1 if it hasn't even created any fds yet because
        // none of the connections have started yet.  In this case, don't
        // do the select at all, because it will wait forever; instead, just
        // skip it and go straight to running the underlying CURL handles
        if (maxfd != -1) {
            int64_t timeout = S3_get_request_context_timeout(requestContext);
            struct timeval tv = { timeout / 1000, (timeout % 1000) * 1000 };
            select(maxfd + 1, &readfds, &writefds, &exceptfds,
                   (timeout == -1) ? 0 : &tv);
        }
        status = S3_runonce_request_context(requestContext,
                                            &requestsRemaining);
        if (status != S3StatusOK) {
            return status;
        }
    } while (requestsRemaining);
    
    return S3StatusOK;
}


S3Status S3_runonce_request_context(S3RequestContext *requestContext, 
                                    int *requestsRemainingReturn)
{
    CURLMcode status;

    do {
        status = curl_multi_perform(requestContext->curlm,
                                    requestsRemainingReturn);

        switch (status) {
        case CURLM_OK:
        case CURLM_CALL_MULTI_PERFORM:
            break;
        case CURLM_OUT_OF_MEMORY:
            return S3StatusOutOfMemory;
        default:
            return S3StatusInternalError;
        }

        CURLMsg *msg;
        int junk;
        while ((msg = curl_multi_info_read(requestContext->curlm, &junk))) {
            if (msg->msg != CURLMSG_DONE) {
                return S3StatusInternalError;
            }
            Request *request;
            if (curl_easy_getinfo(msg->easy_handle, CURLINFO_PRIVATE, 
                                  (char **) (char *) &request) != CURLE_OK) {
                return S3StatusInternalError;
            }
            // Remove the request from the list of requests
            if (request->prev == request->next) {
                // It was the only one on the list
                requestContext->requests = 0;
            }
            else {
                // It doesn't matter what the order of them are, so just in
                // case request was at the head of the list, put the one after
                // request to the head of the list
                requestContext->requests = request->next;
                request->prev->next = request->next;
                request->next->prev = request->prev;
            }
            if ((msg->data.result != CURLE_OK) &&
                (request->status == S3StatusOK)) {
                request->status = request_curl_code_to_status
                    (msg->data.result);
            }
            if (curl_multi_remove_handle(requestContext->curlm, 
                                         msg->easy_handle) != CURLM_OK) {
                return S3StatusInternalError;
            }
            // Finish the request, ensuring that all callbacks have been made,
            // and also releases the request
            request_finish(request);
            // Now, since a callback was made, there may be new requests 
            // queued up to be performed immediately, so do so
            status = CURLM_CALL_MULTI_PERFORM;
        }
    } while (status == CURLM_CALL_MULTI_PERFORM);

    return S3StatusOK;
}

S3Status S3_get_request_context_fdsets(S3RequestContext *requestContext,
                                       fd_set *readFdSet, fd_set *writeFdSet,
                                       fd_set *exceptFdSet, int *maxFd)
{
    return ((curl_multi_fdset(requestContext->curlm, readFdSet, writeFdSet,
                              exceptFdSet, maxFd) == CURLM_OK) ?
            S3StatusOK : S3StatusInternalError);
}

int64_t S3_get_request_context_timeout(S3RequestContext *requestContext)
{
    long timeout;

    if (curl_multi_timeout(requestContext->curlm, &timeout) != CURLM_OK) {
        timeout = 0;
    }
    
    return timeout;
}
