/*
 * Copyright 1999-2006 University of Chicago
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#ifndef GLOBUS_DONT_DOCUMENT_INTERNAL

/**
 * @file globus_ftp_client_perf_plugin.c
 * @brief GridFTP Performance Marker Plugin Implementation
 */

#include "globus_ftp_client_perf_plugin.h"
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <string.h>
#include <ctype.h>
#include "version.h"

/* for 'get' mode (in seconds) */
#define MIN_CB_INTERVAL 1

#define GLOBUS_L_FTP_CLIENT_PERF_PLUGIN_NAME "globus_ftp_client_perf_plugin"

static globus_bool_t globus_l_ftp_client_perf_plugin_activate(void);
static globus_bool_t globus_l_ftp_client_perf_plugin_deactivate(void);

globus_module_descriptor_t globus_i_ftp_client_perf_plugin_module =
{
    GLOBUS_L_FTP_CLIENT_PERF_PLUGIN_NAME,
    globus_l_ftp_client_perf_plugin_activate,
    globus_l_ftp_client_perf_plugin_deactivate,
    GLOBUS_NULL,
    GLOBUS_NULL,
    &local_version
};

static
int
globus_l_ftp_client_perf_plugin_activate(void)
{
    int rc;

    rc = globus_module_activate(GLOBUS_FTP_CLIENT_MODULE);
    return rc;
}

static
int
globus_l_ftp_client_perf_plugin_deactivate(void)
{
    return globus_module_deactivate(GLOBUS_FTP_CLIENT_MODULE);
}

typedef struct perf_plugin_info_s
{
    void *                                          user_specific;
    globus_ftp_client_perf_plugin_begin_cb_t        begin_cb;
    globus_ftp_client_perf_plugin_marker_cb_t       marker_cb;
    globus_ftp_client_perf_plugin_complete_cb_t     complete_cb;
    globus_ftp_client_perf_plugin_user_copy_cb_t    copy_cb;
    globus_ftp_client_perf_plugin_user_destroy_cb_t destroy_cb;

    globus_bool_t                                   success;

    /* used for get command or put (when put not in EB mode) only */
    globus_bool_t                                   use_data;
    double                                          last_time;
    globus_off_t                                    nbytes;
    globus_mutex_t                                  lock;

} perf_plugin_info_t;


/**
 * Plugin complete callback
 * @ingroup globus_ftp_client_perf_plugin
 *
 * This callback will be called when one of the transfer commands
 * is completed. Will also call user's 'complete' callback
 */

static
void
perf_plugin_complete_cb(
    globus_ftp_client_plugin_t *                plugin,
    void *                                      plugin_specific,
    globus_ftp_client_handle_t *                handle)
{
    perf_plugin_info_t *                        ps;

    ps = (perf_plugin_info_t *) plugin_specific;

    if(ps->complete_cb)
    {
        ps->complete_cb(
            ps->user_specific,
            handle,
            ps->success);
    }
}

/**
 * Plugin abort callback
 * @ingroup globus_ftp_client_perf_plugin
 *
 * This callback will be called when an abort has been requested
 */

static
void perf_plugin_abort_cb(
    globus_ftp_client_plugin_t *                plugin,
    void *                                      plugin_specific,
    globus_ftp_client_handle_t *                handle)
{
    perf_plugin_info_t *                        ps;

    ps = (perf_plugin_info_t *) plugin_specific;

    ps->success = GLOBUS_FALSE;
}

/**
 * Plugin fault callback
 * @ingroup globus_ftp_client_restart_marker_plugin
 *
 * This callback will be called when one of the transfer commands
 * has failed.
 */

static
void
perf_plugin_fault_cb(
    globus_ftp_client_plugin_t *                plugin,
    void *                                      plugin_specific,
    globus_ftp_client_handle_t *                handle,
    const char *                                url,
    globus_object_t *                           error)
{
    perf_plugin_info_t *                        ps;

    ps = (perf_plugin_info_t *) plugin_specific;

    ps->success = GLOBUS_FALSE;
}

/**
 * Plugin response callback
 * @ingroup globus_ftp_client_perf_plugin
 *
 * Parses out performance markers and calls user's 'marker' callback.
 */

static
void
perf_plugin_response_cb(
    globus_ftp_client_plugin_t *                plugin,
    void *                                      plugin_specific,
    globus_ftp_client_handle_t *                handle,
    const char *                                url,
    globus_object_t *                           error,
    const globus_ftp_control_response_t *       ftp_response)
{
    perf_plugin_info_t *                        ps;
    char *                                      buffer;
    char *                                      tmp_ptr;
    int                                         count;
    long                                        time_stamp_int;
    char                                        time_stamp_tenght;
    int                                         stripe_ndx;
    int                                         num_stripes;
    globus_off_t                                nbytes;

    ps = (perf_plugin_info_t *) plugin_specific;

    if(ps->marker_cb &&
        !error &&
        ftp_response &&
        ftp_response->response_buffer &&
        ftp_response->code == 112 &&
        !ps->use_data) /* bad server... sending markers in non-EB mode */
    {
        buffer = (char *) ftp_response->response_buffer;

        /* parse out time stamp */
        tmp_ptr = strstr(buffer, "Timestamp:");
        if(tmp_ptr == NULL)
        {
            return;
        }

        tmp_ptr += sizeof("Timestamp:");
        while(isspace(*tmp_ptr))
        {
            tmp_ptr++;
        }

        time_stamp_int = 0;
        while(isdigit(*tmp_ptr))
        {
            time_stamp_int = (time_stamp_int * 10) + (*tmp_ptr - '0');
            tmp_ptr++;
        }

        time_stamp_tenght = 0;
        if(*tmp_ptr == '.')
        {
            tmp_ptr++;
            time_stamp_tenght = *tmp_ptr - '0';
            tmp_ptr++;
        }

        if(!isspace(*tmp_ptr))
        {
            /* invalid value */
            return;
        }

        /* parse out Stripe Index */
        tmp_ptr = strstr(buffer, "Stripe Index:");
        if(tmp_ptr == NULL)
        {
            return;
        }
        count = sscanf(tmp_ptr + sizeof("Stripe Index:"),
            " %d", &stripe_ndx);
        if(count != 1)
        {
            return;
        }

        /* parse out total stripe count */
        tmp_ptr = strstr(buffer, "Total Stripe Count:");
        if(tmp_ptr == NULL)
        {
            return;
        }
        count = sscanf(tmp_ptr + sizeof("Total Stripe Count:"),
            " %d", &num_stripes);
        if(count != 1)
        {
            return;
        }

        /* parse out bytes transferred */
        tmp_ptr = strstr(buffer, "Stripe Bytes Transferred:");
        if(tmp_ptr == NULL)
        {
            return;
        }
        count = sscanf(tmp_ptr + sizeof("Stripe Bytes Transferred:"),
            " %" GLOBUS_OFF_T_FORMAT, &nbytes);
        if(count != 1)
        {
            return;
        }

        ps->marker_cb(
            ps->user_specific,
            handle,
            time_stamp_int,
            time_stamp_tenght,
            stripe_ndx,
            num_stripes,
            nbytes);
    }
}

/**
 * Plugin data callback
 * @ingroup globus_ftp_client_perf_plugin
 *
 * This plugin is used to create performance marker callbacks in the case
 * of a 'get' command.  (no performance markers are received in the 'get'
 * case)
 */

static
void
perf_plugin_data_cb(
    globus_ftp_client_plugin_t *                plugin,
    void *                                      plugin_specific,
    globus_ftp_client_handle_t *                handle,
    globus_object_t *                           error,
    const globus_byte_t *                       buffer,
    globus_size_t                               length,
    globus_off_t                                offset,
    globus_bool_t                               eof)
{
    perf_plugin_info_t *                        ps;
    globus_abstime_t                            timebuf;
    long                                        secs;
    long                                        usecs;
    double                                      time_now;

    ps = (perf_plugin_info_t *) plugin_specific;

    if(ps->use_data && !error)
    {
        globus_mutex_lock(&ps->lock);
        
        GlobusTimeAbstimeGetCurrent(timebuf);
        GlobusTimeAbstimeGet(timebuf, secs, usecs);
        time_now = secs + (usecs / 1000000.0);
        
        ps->nbytes += length;

        if(ps->marker_cb && (time_now - ps->last_time) > MIN_CB_INTERVAL)
        {
            ps->last_time = time_now;

            ps->marker_cb(
                ps->user_specific,
                handle,
                secs,
                usecs / 100000,
                0,
                1,
                ps->nbytes);
        }

        globus_mutex_unlock(&ps->lock);
    }
}

/**
 * Plugin get command callback
 * @ingroup globus_ftp_client_perf_plugin
 *
 * This callback signifies the start of a 'get' command. It will
 * call the user's 'begin' callback
 */

static
void
perf_plugin_get_cb(
    globus_ftp_client_plugin_t *                plugin,
    void *                                      plugin_specific,
    globus_ftp_client_handle_t *                handle,
    const char *                                url,
    const globus_ftp_client_operationattr_t *   attr,
    globus_bool_t                               restart)
{
    perf_plugin_info_t *                        ps;

    ps = (perf_plugin_info_t *) plugin_specific;

    ps->success = GLOBUS_TRUE;

    ps->use_data = GLOBUS_TRUE;
    ps->nbytes = 0;
    ps->last_time = 0;

    if(ps->begin_cb)
    {
        ps->begin_cb(
            ps->user_specific,
            handle,
            url,
            GLOBUS_NULL,
            restart);
    }
}


/**
 * Plugin put command callback
 * @ingroup globus_ftp_client_perf_plugin
 *
 * This callback signifies the start of a 'put' command. It will
 * call the user's 'begin' callback
 */

static
void
perf_plugin_put_cb(
    globus_ftp_client_plugin_t *                plugin,
    void *                                      plugin_specific,
    globus_ftp_client_handle_t *                handle,
    const char *                                url,
    const globus_ftp_client_operationattr_t *   attr,
    globus_bool_t                               restart)
{
    perf_plugin_info_t *                        ps;
    globus_ftp_control_mode_t                   mode;
    globus_result_t                             result;

    ps = (perf_plugin_info_t *) plugin_specific;

    ps->success = GLOBUS_TRUE;

    result = globus_ftp_client_operationattr_get_mode(attr, &mode);
    if(result == GLOBUS_SUCCESS &&
        mode == GLOBUS_FTP_CONTROL_MODE_EXTENDED_BLOCK)
    {
        ps->use_data = GLOBUS_FALSE;
    }
    else
    {
        ps->use_data = GLOBUS_TRUE;
        ps->nbytes = 0;
        ps->last_time = 0;
    }

    if(ps->begin_cb)
    {
        ps->begin_cb(
            ps->user_specific,
            handle,
            GLOBUS_NULL,
            url,
            restart);
    }
}


/**
 * Plugin thrid party transfer command callback
 * @ingroup globus_ftp_client_perf_plugin
 *
 * This callback signifies the start of a 'transfer' command. It will
 * call the user's 'begin' callback
 */

static
void
perf_plugin_transfer_cb(
    globus_ftp_client_plugin_t *                plugin,
    void *                                      plugin_specific,
    globus_ftp_client_handle_t *                handle,
    const char *                                source_url,
    const globus_ftp_client_operationattr_t *   source_attr,
    const char *                                dest_url,
    const globus_ftp_client_operationattr_t *   dest_attr,
    globus_bool_t                               restart)
{
    perf_plugin_info_t *                        ps;

    ps = (perf_plugin_info_t *) plugin_specific;

    ps->success = GLOBUS_TRUE;
    ps->use_data = GLOBUS_FALSE;

    if(ps->begin_cb)
    {
        ps->begin_cb(
            ps->user_specific,
            handle,
            source_url,
            dest_url,
            restart);
    }
}

/**
 * Plugin copy callback
 * @ingroup globus_ftp_client_perf_plugin
 *
 * This callback is called to create a new copy of this plugin. It will also
 * call the user's 'copy' callback
 */

static
globus_ftp_client_plugin_t *
perf_plugin_copy_cb(
    globus_ftp_client_plugin_t *                plugin_template,
    void *                                      plugin_specific)
{
    perf_plugin_info_t *                        old_ps;
    globus_ftp_client_plugin_t *                new_plugin;
    void *                                      new_user_specific;
    globus_result_t                             result;

    old_ps = (perf_plugin_info_t *) plugin_specific;

    new_plugin = (globus_ftp_client_plugin_t *)
        globus_malloc(sizeof(globus_ftp_client_plugin_t));

    if(new_plugin == GLOBUS_NULL)
    {
        return GLOBUS_NULL;
    }

    if(old_ps->copy_cb)
    {
        new_user_specific = old_ps->copy_cb(old_ps->user_specific);
    }
    else
    {
        new_user_specific = old_ps->user_specific;
    }

    result = globus_ftp_client_perf_plugin_init(
        new_plugin,
        old_ps->begin_cb,
        old_ps->marker_cb,
        old_ps->complete_cb,
        new_user_specific);

    if(result != GLOBUS_SUCCESS)
    {
        globus_free(new_plugin);
        if(old_ps->destroy_cb)
        {
            old_ps->destroy_cb(new_user_specific);
        }

        return GLOBUS_NULL;
    }

    globus_ftp_client_perf_plugin_set_copy_destroy(
        new_plugin,
        old_ps->copy_cb,
        old_ps->destroy_cb);

    return new_plugin;
}

/**
 * Plugin destroy callback
 * @ingroup globus_ftp_client_perf_plugin
 *
 * This callback is called to destroy a copy of a plugin made with the
 * copy callback above.  It will also call the user's 'destroy' callback
 */

static
void
perf_plugin_destroy_cb(
    globus_ftp_client_plugin_t *                plugin,
    void *                                      plugin_specific)
{
    perf_plugin_info_t *                        ps;

    ps = (perf_plugin_info_t *) plugin_specific;

    if(ps->destroy_cb)
    {
        ps->destroy_cb(ps->user_specific);
    }

    globus_ftp_client_perf_plugin_destroy(plugin);
    globus_free(plugin);
}

#endif  /* GLOBUS_DONT_DOCUMENT_INTERNAL */

/**
 * Initialize a perf plugin
 * @ingroup globus_ftp_client_perf_plugin
 *
 * This function initializes a performance marker plugin. Any params
 * except for the plugin may be GLOBUS_NULL
 *
 * @param plugin
 *        a pointer to a plugin type to be initialized
 *
 * @param user_specific
 *        a pointer to some user specific data that will be provided to
 *        all callbacks
 *
 * @param begin_cb
 *        the callback to be called upon the start of a transfer
 *
 * @param marker_cb
 *        the callback to be called with every performance marker received
 *
 * @param complete_cb
 *        the callback to be called to indicate transfer completion
 *
 * @return
 *        - GLOBUS_SUCCESS
 *        - Error on NULL plugin
 *        - Error on init internal plugin
 */

globus_result_t
globus_ftp_client_perf_plugin_init(
    globus_ftp_client_plugin_t *                    plugin,
    globus_ftp_client_perf_plugin_begin_cb_t        begin_cb,
    globus_ftp_client_perf_plugin_marker_cb_t       marker_cb,
    globus_ftp_client_perf_plugin_complete_cb_t     complete_cb,
    void *                                          user_specific)
{
    perf_plugin_info_t *                            ps;
    globus_result_t                                 result;
    GlobusFuncName(globus_ftp_client_perf_plugin_init);

    if(plugin == GLOBUS_NULL)
    {
        return globus_error_put(globus_error_construct_string(
            GLOBUS_FTP_CLIENT_MODULE,
            GLOBUS_NULL,
            "[%s] NULL plugin at %s\n",
            GLOBUS_FTP_CLIENT_MODULE->module_name,
            _globus_func_name));
    }

    ps = (perf_plugin_info_t *)
        globus_malloc(sizeof(perf_plugin_info_t));

    if(ps == GLOBUS_NULL)
    {
        return globus_error_put(globus_error_construct_string(
            GLOBUS_FTP_CLIENT_MODULE,
            GLOBUS_NULL,
            "[%s] Out of memory at %s\n",
             GLOBUS_FTP_CLIENT_MODULE->module_name,
             _globus_func_name));
    }

    /*
     *  initialize plugin specific structure.
     */
    ps->user_specific       = user_specific;
    ps->begin_cb            = begin_cb;
    ps->marker_cb           = marker_cb;
    ps->complete_cb         = complete_cb;
    ps->copy_cb             = GLOBUS_NULL;
    ps->destroy_cb          = GLOBUS_NULL;

    globus_mutex_init(&ps->lock, GLOBUS_NULL);

    result = globus_ftp_client_plugin_init(
              plugin,
              GLOBUS_L_FTP_CLIENT_PERF_PLUGIN_NAME,
              GLOBUS_FTP_CLIENT_CMD_MASK_FILE_ACTIONS,
              ps);

    if(result != GLOBUS_SUCCESS)
    {
        globus_mutex_destroy(&ps->lock);
        globus_free(ps);

        return result;
    }

    globus_ftp_client_plugin_set_destroy_func(plugin,
        perf_plugin_destroy_cb);
    globus_ftp_client_plugin_set_copy_func(plugin,
        perf_plugin_copy_cb);
    globus_ftp_client_plugin_set_get_func(plugin,
        perf_plugin_get_cb);
    globus_ftp_client_plugin_set_data_func(plugin,
        perf_plugin_data_cb);
    globus_ftp_client_plugin_set_put_func(plugin,
        perf_plugin_put_cb);
    globus_ftp_client_plugin_set_third_party_transfer_func(plugin,
        perf_plugin_transfer_cb);
    globus_ftp_client_plugin_set_response_func(plugin,
        perf_plugin_response_cb);
    globus_ftp_client_plugin_set_complete_func(plugin,
        perf_plugin_complete_cb);
    globus_ftp_client_plugin_set_fault_func(plugin,
        perf_plugin_fault_cb);
    globus_ftp_client_plugin_set_abort_func(plugin,
        perf_plugin_abort_cb);

    return GLOBUS_SUCCESS;
}

/**
 * Set user copy and destroy callbacks
 * @ingroup globus_ftp_client_perf_plugin
 *
 * Use this to have the plugin make callbacks any time a copy of this
 * plugin is being made.  This will allow the user to keep state for
 * different handles.
 *
 * @param plugin
 *        plugin previously initialized with init (above)
 *
 * @param copy_cb
 *        func to be called when a copy is needed
 *
 * @param destroy_cb
 *        func to be called when a copy is to be destroyed
 *
 * @return
 *        - Error on NULL arguments
 *        - GLOBUS_SUCCESS
 */

globus_result_t
globus_ftp_client_perf_plugin_set_copy_destroy(
    globus_ftp_client_plugin_t *                    plugin,
    globus_ftp_client_perf_plugin_user_copy_cb_t    copy_cb,
    globus_ftp_client_perf_plugin_user_destroy_cb_t destroy_cb)
{
    globus_result_t                             result;
    perf_plugin_info_t *                        ps;
    GlobusFuncName(globus_ftp_client_perf_plugin_set_copy_destroy);

    if(plugin == GLOBUS_NULL ||
        copy_cb == GLOBUS_NULL ||
        destroy_cb == GLOBUS_NULL)
    {
        return globus_error_put(globus_error_construct_string(
                GLOBUS_FTP_CLIENT_MODULE,
                GLOBUS_NULL,
                "[%s] NULL arg at %s\n",
                GLOBUS_FTP_CLIENT_MODULE->module_name,
                _globus_func_name));
    }

    result = globus_ftp_client_plugin_get_plugin_specific(
              plugin,
              (void **) (void *) &ps);

    if(result != GLOBUS_SUCCESS)
    {
        return result;
    }

    ps->copy_cb = copy_cb;
    ps->destroy_cb = destroy_cb;

    return GLOBUS_SUCCESS;
}

/**
 * Destroy performance marker plugin
 * @ingroup globus_ftp_client_perf_plugin
 *
 * Frees up memory associated with plugin
 *
 * @param plugin
 *        plugin previously initialized with init (above)
 *
 * @return
 *        - GLOBUS_SUCCESS
 *        - Error on NULL plugin
 */

globus_result_t
globus_ftp_client_perf_plugin_destroy(
    globus_ftp_client_plugin_t *                    plugin)
{
    globus_result_t                                 result;
    perf_plugin_info_t *                            ps;
    GlobusFuncName(globus_ftp_client_perf_plugin_destroy);

    if(plugin == GLOBUS_NULL)
    {
        return globus_error_put(globus_error_construct_string(
            GLOBUS_FTP_CLIENT_MODULE,
            GLOBUS_NULL,
            "[%s] NULL plugin at %s\n",
            GLOBUS_FTP_CLIENT_MODULE->module_name,
            _globus_func_name));
    }

    result = globus_ftp_client_plugin_get_plugin_specific(
        plugin,
        (void **) (void *) &ps);

    if(result != GLOBUS_SUCCESS)
    {
        return result;
    }

    globus_mutex_destroy(&ps->lock);
    globus_free(ps);

    return globus_ftp_client_plugin_destroy(plugin);
}

/**
 * Retrieve user specific pointer
 * @ingroup globus_ftp_client_perf_plugin
 *
 * @param plugin
 *        plugin previously initialized with init (above)
 *
 * @param user_specific
 *        pointer to storage for user_specific pointer
 *
 * @return
 *        - GLOBUS_SUCCESS
 *        - Error on NULL plugin
 *        - Error on NULL user_specific
 */

globus_result_t
globus_ftp_client_perf_plugin_get_user_specific(
    globus_ftp_client_plugin_t *                    plugin,
    void **                                         user_specific)
{
    globus_result_t                                 result;
    perf_plugin_info_t *                            ps;
    GlobusFuncName(globus_ftp_client_perf_plugin_get_user_specific);

    if(plugin == GLOBUS_NULL)
    {
        return globus_error_put(globus_error_construct_string(
            GLOBUS_FTP_CLIENT_MODULE,
            GLOBUS_NULL,
            "[%s] NULL plugin at %s\n",
            GLOBUS_FTP_CLIENT_MODULE->module_name,
            _globus_func_name));
    }

    if(user_specific == GLOBUS_NULL)
    {
        return globus_error_put(globus_error_construct_string(
            GLOBUS_FTP_CLIENT_MODULE,
            GLOBUS_NULL,
            "[%s] NULL user_specific at %s\n",
            GLOBUS_FTP_CLIENT_MODULE->module_name,
            _globus_func_name));
    }

    result = globus_ftp_client_plugin_get_plugin_specific(
        plugin,
        (void **) (void *) &ps);

    if(result != GLOBUS_SUCCESS)
    {
        return result;
    }

    *user_specific = ps->user_specific;

    return GLOBUS_SUCCESS;
}
