/*
 * nebclient.c - nebulous client
 *
 * Copyright (C) 2005  Joshua Hoblitt
 *
 * $Id: nebclient.c,v 1.48 2008-06-09 20:51:47 jhoblitt Exp $
 */

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <regex.h>
#include <unistd.h>
#include <stdbool.h>
#include <stdarg.h>
#include <time.h>
#include "xmalloc.h"
#include "soapH.h"
#include "SOAP.nsmap"
#include "nebclient.h"

#define ERRBUF_SIZE 2048
#define LOCK_TIMEOUT 10
#define LOCK_INTERVAL 1

#define nebSetErr(server,...) \
    p_nebSetErr(__FILE__, __LINE__, __func__, server,__VA_ARGS__)

// this is a slightly more useful error message than returned by assert()
#define REQUIRE_SERVER \
    if (!server) { \
        fprintf(stderr, "parameter 'server' may not be NULL"); \
        abort(); \
    }

#define NEBCOPYFILE 0

static void p_nebSetErr(const char * filename, unsigned long lineno, const char *function, nebServer *server, const char *format, ...);
static void nebSetServerErr(nebServer *server);
#if NEBCOPYFILE
static off_t nebCopyFile(nebServer *server, const char *source, const char *dest);
#endif
static off_t nebCopyFilehandle(nebServer *server, int sourceFH, int destFH);

static size_t nebParseURI(nebServer *server, const char *URI, char **filename);
static bool nebNukeFile(nebServer *server, const char *filename);
static char *nullstrncpy(char *dest, const char *src, size_t n);

nebServer *nebServerAlloc(const char *endpoint)
{
    nebServer       *server;

    server          = xmalloc(sizeof(nebServer));

    if (endpoint) {
        server->endpoint = xmalloc(strlen(endpoint) + 1);
        strcpy(server->endpoint, endpoint);
    } else {
        server->endpoint = NULL;
    }

    server->soap    = xmalloc(sizeof(struct soap));
    server->err_buf = xmalloc(ERRBUF_SIZE);
    server->err_buf_size = ERRBUF_SIZE;

    soap_init(server->soap);

    return server;
}

void nebServerFree(nebServer *server)
{
    if (!server) {
        return;
    }

    soap_end(server->soap); // remove deserialized data and clean up
    soap_done(server->soap); // detach the gSOAP environment

    nebFree(server->endpoint);
    nebFree(server->soap);
    nebFree(server->err_buf);
    nebFree(server);
}

char *nebCreate(nebServer *server, const char *key, const char *volume, char **URI)
{
    struct ns1__create_USCOREobjectResponse response;
    char            *filename;

    REQUIRE_SERVER;

    if (!key) {
        nebSetErr(server, "parameter 'key' may not be NULL");

        return NULL;
    }

    if (soap_call_ns1__create_USCOREobject(server->soap, server->endpoint, NULL,
            (char *)key, (char *)volume, (char **)&response) != SOAP_OK) {
        nebSetServerErr(server);

        return NULL;
    }

    if (!(char *)response.result) {
        nebSetErr(server, "server returned no result");
        return NULL;
    }

    if (!nebParseURI(server, (char *)response.result, &filename)) {
        nebSetErr(server, "can not parse URI");
        return NULL;
    }

    // if URI is not NULL
    if (URI) {
        *URI = xmalloc(strlen((char *)response.result));
        strcpy(*URI, (char *)response.result);
    }

    return filename;
}

int nebOpenCreate(nebServer *server, const char *key, const char *volume, char **URI)
{
    char            *filename;
    int             file;

    REQUIRE_SERVER;

    if (!key) {
        nebSetErr(server, "parameter 'key' may not be NULL");

        return -1;
    }

    filename = nebCreate(server, key, volume, URI);

    file = open(filename, O_RDWR|O_TRUNC, 0660);
    if (file == -1) {
        nebSetErr(server, "can not open %s: %s", filename, strerror(errno));
        nebFree(filename);

        return -1;
    }

    nebFree(filename);

    return file;
}

bool nebReplicate(nebServer *server, const char *key, const char *volume, char **URI)
{
    struct ns1__replicate_USCOREobjectResponse response;
    char            *filename;
    int             sourceFH, destFH;

    REQUIRE_SERVER;

    if (!key) {
        nebSetErr(server, "parameter 'key' may not be NULL");

        return false;
    }


    // We have to open the instance that we're going to copy from first.  If
    // we don't do this, it's possible that open will find & open the new
    // instance that we're in the process of creating
    sourceFH = nebOpen(server, (char *)key, NEB_READ);
    if (sourceFH == -1) {
        nebSetErr(server, "can not open key %s", key);
        nebFree(filename);

        return false;
    }

    // ask the server for a new instance attached to our key
    if (soap_call_ns1__replicate_USCOREobject(server->soap, server->endpoint,
            NULL, (char *)key, (char *)volume, (char **)&response) != SOAP_OK) {
        nebSetServerErr(server);

        return false;
    }

    if (!nebParseURI(server, (char *)response.result, &filename)) {
        nebSetErr(server, "can not parse URI");

        return false;
    }

    destFH = open(filename, O_RDWR|O_TRUNC, 0660);
    if (destFH == -1) {
        nebSetErr(server, "can not open %s: %s\n", filename, strerror(errno));
        nebFree(filename);

        return false;
    }

    nebCopyFilehandle(server, sourceFH, destFH);

    if (close(sourceFH) == -1) {
        nebSetErr(server, "can not close file: %s", strerror(errno));
        nebFree(filename);

        return false;
    }

    if (close(destFH) == -1) {
        nebSetErr(server, "can not close %s: %s", filename, strerror(errno));
        nebFree(filename);

        return false;
    }

    nebFree(filename);

    // if URI is not NULL
    if (URI) {
        *URI = xmalloc(strlen((char *)response.result));
        strcpy(*URI, (char *)response.result);
    }

    return true;
}

bool nebCull(nebServer *server, const char *key)
{
    nebObjectInstances *locations;

    REQUIRE_SERVER;

    if (!key) {
        nebSetErr(server, "parameter 'key' may not be NULL");

        return false;
    }

    locations = nebFindInstances(server, key, NULL);
    if (!locations) {
        nebSetErr(server, "can not cull - not enough instances");

        return false;
    }

    if (!nebDeleteInstance(server, locations->URI[0])) {
        nebObjectInstancesFree(locations);

        return false;
    }

    nebObjectInstancesFree(locations);

    return true;
}

bool nebLock(nebServer *server, const char *key, nebRW flag)
{
    time_t          endtime;
    char            *type;
    int             response;

    REQUIRE_SERVER;

    if (!key) {
        nebSetErr(server, "parameter 'key' may not be NULL");

        return false;
    }

    type = flag == NEB_READ ? "read" : "write";

    endtime = time(NULL) + LOCK_TIMEOUT;

    while (time(NULL) < endtime) {
        if (soap_call_ns1__lock_USCOREobject(server->soap, server->endpoint,
            NULL, (char *)key, type, &response) != SOAP_OK) {
            nebSetServerErr(server);
            return false;
        }

        if (response) {
            return true;
        }

        sleep(LOCK_INTERVAL);
    }

    // else, we never got a lock
    nebSetErr(server, "can not get a %s lock", type);

    return false;
}

bool nebUnlock(nebServer *server, const char *key, nebRW flag)
{
    char            *type;
    int             response;

    REQUIRE_SERVER;

    if (!key) {
        nebSetErr(server, "parameter 'key' may not be NULL");

        return false;
    }

    type = flag == NEB_READ ? "read" : "write";

    if (soap_call_ns1__unlock_USCOREobject(server->soap, server->endpoint,
        NULL, (char *)key, type, &response) != SOAP_OK) {
        nebSetServerErr(server);
        return false;
    }

    if (response) {
        return true;
    }

    // else, we never got a lock
    nebSetErr(server, "can not release %s lock", type);

    return false;
}

nebObjectInstances *nebFindInstances(nebServer *server, const char *key, const char *volume)
{
    struct ns1__find_USCOREinstancesResponse response;
    int             resultElements = 0;
    char            **resultArray;
    nebObjectInstances *locations = NULL;

    REQUIRE_SERVER;

    if (!key) {
        nebSetErr(server, "parameter 'key' may not be NULL");

        return NULL;
    }

    // FIXME is this leaking memory when response goes out of scope?  the gsoap
    // manual seems to 'suggest' that this is temporary data that gets cleaed
    // up on the next soap function call
    if (soap_call_ns1__find_USCOREinstances(server->soap, server->endpoint,
            NULL, (char *)key, (char *)volume, &response) != SOAP_OK) {
        nebSetServerErr(server);
        return NULL;
    }

    resultElements = response.result->__size;

    if (resultElements) {
        locations = xmalloc(sizeof(nebObjectInstances));
        locations->n = resultElements;
        
        resultArray    = response.result->__ptr;

        locations->URI = xmalloc(sizeof(char*) * resultElements);

        for (int i = 0; i < resultElements; i++) {
            locations->URI[i] = xmalloc(strlen(resultArray[i]) + 1);
            strcpy(locations->URI[i], resultArray[i]);
        }

    }

    return locations;
}

void nebObjectInstancesFree(nebObjectInstances *locations)
{
    if (!locations) {
        return;
    }

    for (int i = 0; i < locations->n; i++) {
        nebFree(locations->URI[i]);
    }

    nebFree(locations);
}

char *nebFind(nebServer *server, const char *key)
{
    nebObjectInstances *locations;
    char            *filename;

    REQUIRE_SERVER;

    if (!key) {
        nebSetErr(server, "parameter 'key' may not be NULL");

        return NULL;
    }
    
    locations = nebFindInstances(server, key, NULL);
    if (!locations) {
        if (!strstr(nebErr(server), "no instances on storage volume")) {
            nebSetErr(server, "no instances found");
            return NULL;
        }

        locations = nebFindInstances(server, key, ":any");
        if (!locations) {
            nebSetErr(server, "no instances found");
            return NULL;
        }
    }

    if (!nebParseURI(server, locations->URI[0], &filename)) {
        nebSetErr(server, "can not parse URI");
        nebObjectInstancesFree(locations);

        return NULL;
    }

    nebObjectInstancesFree(locations);

    return filename;
}

int nebOpen(nebServer *server, const char *key, nebRW flag)
{
    nebObjectInstances *locations;
    char            *filename;
    int             fh;

    REQUIRE_SERVER;

    if (!key) {
        nebSetErr(server, "parameter 'key' may not be NULL");

        return -1;
    }

    locations = nebFindInstances(server, key, NULL);
    if (!locations) {
        nebSetErr(server, "no instances found");

        return -1;
    }

    if (!nebParseURI(server, locations->URI[0], &filename)) {
        nebSetErr(server, "can not parse URI");
        nebObjectInstancesFree(locations);

        return -1;
    }

    if (flag == NEB_WRITE) {
        if (locations->n > 1) {
            nebSetErr(server, "write not allowed with multiple instances");
            nebFree(filename);
            nebObjectInstancesFree(locations);

            return -1;
        }
        fh = open(filename, O_RDWR);
    } else {
        fh = open(filename, O_RDONLY);
    }

    nebFree(filename);
    nebObjectInstancesFree(locations);

    if (fh < 0) {
        nebSetErr(server, "open: %s", strerror(errno));

        return -1;
    }

    return fh;
}

bool nebDelete(nebServer *server, const char *key)
{
    nebObjectInstances *locations;

    REQUIRE_SERVER;

    if (!key) {
        nebSetErr(server, "parameter 'key' may not be NULL");

        return false;
    }

    locations = nebFindInstances(server, key, NULL);
    if (!locations) {
        nebSetErr(server, "no instances found");

        return false;
    }

    for (int i = 0; i < locations->n; i++) {
        if (!nebDeleteInstance(server, locations->URI[i])) {
            nebSetErr(server, "can not delete instance");
            nebObjectInstancesFree(locations);

            return false;
        }
    }

    nebObjectInstancesFree(locations);

    return true;
}

bool nebCopy(nebServer *server, const char *key, const char *newKey)
{
    int             sourceFH;
    int             destFH;

    REQUIRE_SERVER;

    if (!key) {
        nebSetErr(server, "parameter 'key' may not be NULL");

        return false;
    }

    if (!newKey) {
        nebSetErr(server, "parameter 'newKey' may not be NULL");

        return false;
    }

    sourceFH = nebOpen(server, (char *)key, NEB_READ);
    if (sourceFH < 0) {
        // propigate nebOpen() error

        return false;
    }

    destFH = nebOpenCreate(server, newKey, NULL, NULL);
    if (destFH < 0) {
        // propigate nebCreate() error

        return false;
    }

    nebCopyFilehandle(server, sourceFH, destFH);

    if (close(sourceFH) == -1) {
        nebSetErr(server, "can not close file: %s", strerror(errno));

        return false;
    }

    if (close(destFH) == -1) {
        nebSetErr(server, "can not close file: %s", strerror(errno));

        return false;
    }

    return true;
}

bool nebMove(nebServer *server, const char *key, const char *newKey)
{
    REQUIRE_SERVER;

    if (!key) {
        nebSetErr(server, "parameter 'key' may not be NULL");

        return false;
    }

    if (!newKey) {
        nebSetErr(server, "parameter 'newKey' may not be NULL");

        return false;
    }

    if (!nebCopy(server, key, newKey)) {
        return false;
    }

    if (!nebDelete(server, key)) {
        return false;
    }

    return true;
}

bool nebDeleteInstance(nebServer *server, const char *URI)
{
    int             response;
    char            *filename;
    bool            status;

    REQUIRE_SERVER;

    if (!URI) {
        nebSetErr(server, "parameter 'URI' may not be NULL");

        return false;
    }

    if (!nebParseURI(server, URI, &filename)) {
        nebSetErr(server, "can not parse URI");

        return false;
    }

    if (soap_call_ns1__delete_USCOREinstance(server->soap, server->endpoint,
            NULL, (char *)URI, &response) != SOAP_OK) {
        nebFree(filename);

        return false;
    }

    if (response > 0) {
        status = nebNukeFile(server, filename);
    } else {
        status = false;
    }

    nebFree(filename);

    return status;
}

nebObjectStat *nebStat(nebServer *server, const char *key)
{
    struct ns1__stat_USCOREobjectResponse response;
    nebObjectStat   *stat;
    int             resultElements = 0;
    char            **resultArray = NULL;

    REQUIRE_SERVER;

    if (!key) {
        nebSetErr(server, "parameter 'key' may not be NULL");

        return NULL;
    }

    stat = xmalloc(sizeof(nebObjectStat));

    // FIXME is this leaking memory when response goes out of scope?  the gsoap
    // manual seems to 'suggest' that this is temporary data that gets cleaed
    // up on the next soap function call
    if (soap_call_ns1__stat_USCOREobject(server->soap, server->endpoint,
            NULL, (char *)key, &response) != SOAP_OK) {
        nebSetServerErr(server);
        return NULL;
    }

    resultElements = response.result->__size;
    resultArray    = response.result->__ptr;

    if (resultElements != 7) {
        nebSetErr(server, "server didn't return the proper number of stat elements");
        return NULL;
    }

    nullstrncpy(stat->so_id, resultArray[0], 256);
    nullstrncpy(stat->ext_id, resultArray[1], 256);
    nullstrncpy(stat->read_lock, resultArray[2], 256);
    nullstrncpy(stat->write_lock, resultArray[3], 256);
    nullstrncpy(stat->epoch, resultArray[4], 256);
    nullstrncpy(stat->mtime, resultArray[5], 256);
    stat->instances = atoi(resultArray[6]);

    return stat;
}

char *nebErr(nebServer *server)
{
    return server->err_buf;
}

void nebFree(void *ptr) {
    xfree(ptr);
}


static void p_nebSetErr(const char * filename, unsigned long lineno, const char *function, nebServer *server, const char *format, ...)
{
    va_list         args;
    size_t          err_size;
    char            err_location[ERRBUF_SIZE];
    size_t          err_location_size; 

    err_location_size = snprintf(err_location, ERRBUF_SIZE, "%s:%lu %s() - ",
            filename, lineno, function);

    if (err_location_size < 0) {
        fprintf(stderr, "failed to set error info");

        abort();
    }

    // copy error location info to start of error string
    strncpy(server->err_buf, err_location, ERRBUF_SIZE);

    while (1) {
        va_start(args, format);
        // append error string after error location info
        err_size = vsnprintf(
                server->err_buf + err_location_size,
                server->err_buf_size - err_location_size,
                format, args);
        va_end(args);

        if (err_size < 0) {
            fprintf(stderr, "failed to set error message");

            abort();
        }

        if (err_size < server->err_buf_size) {
            break;
        }

        // else: expand err_buf
        // account for '\0'
        server->err_buf_size = err_size + 1;
        xrealloc(server->err_buf, server->err_buf_size);
    }

}

static void nebSetServerErr(nebServer *server)
{
    const char      **code;
    const char      **string;

    REQUIRE_SERVER;

    code   = soap_faultcode(server->soap);
    string = soap_faultstring(server->soap);

    nebSetErr(server, "%s - %s", *code, *string);
}

#if NEBCOPYFILE
static off_t nebCopyFile(nebServer *server, const char *source, const char *dest)
{
    int             sourceFH;
    int             destFH; 
    off_t           bytesCopied;

    REQUIRE_SERVER;

    sourceFH = open(source, O_RDONLY);
    if (sourceFH == -1) {
        nebSetErr(server, "can not open %s: %s", source, strerror(errno));

        return -1;
    }

    destFH = open(dest, O_WRONLY|O_CREAT|O_CREAT, 0600);
    if (destFH == -1) {
        nebSetErr(server, "can not open %s: %s", dest, strerror(errno));

        return -1;
    }

    bytesCopied = nebCopyFilehandle(server, sourceFH, destFH);
    if (bytesCopied < 0) {
        return -1;
    }

    if (close(sourceFH) == -1) {
        nebSetErr(server, "can not close %s: %s", source, strerror(errno));

        return -1;
    }

    if (close(destFH) == -1) {
        nebSetErr(server, "can not close %s: %s", dest, strerror(errno));

        return -1;
    }

    return bytesCopied;
}
#endif

static off_t nebCopyFilehandle(nebServer *server, int sourceFH, int destFH)
{
    off_t           bytesTotal;
    off_t           bytesRemaining;
    off_t           writeSize;
    char            writeBuf[64 * 1024];
    struct stat     sourceStat;

    REQUIRE_SERVER;

    if(fstat(sourceFH, &sourceStat)) {
        nebSetErr(server, "can not stat filehandles: %s", strerror(errno));

        return -1;
    }

    // the size of the file to copied
    bytesTotal = sourceStat.st_size;

    /*
     * If the file size is <= the buffer size call write(2) with the
     * file size.  If the file size is > the buffer call write with the
     * buffer size and loop until file size <= buffer size is true.
    */

    bytesRemaining = bytesTotal;

    while (bytesRemaining) {
        if (bytesRemaining <= sizeof(writeBuf)) {
            writeSize = bytesRemaining;
        } else {
            writeSize = sizeof(writeBuf);
        }

        if (read(sourceFH, writeBuf, writeSize) != writeSize) {
            nebSetErr(server, "can not read filehandle: %s", strerror(errno));

            return -1;
        }

        if (write(destFH, writeBuf, writeSize) != writeSize) {
            nebSetErr(server, "can not write filehandles: %s", strerror(errno));

            return -1;
        }

        bytesRemaining -= writeSize;
    }

    return bytesTotal;
}

static size_t nebParseURI(nebServer *server, const char *URI, char **filename)
{
    regex_t         myregex;
    regmatch_t      mymatch[2];
    char            errbuf[ERRBUF_SIZE];
    char            *pattern = "^file://(.*)$";
    int             status;
    size_t          matchStart, matchEnd;
    size_t          matchLength;
    size_t          filename_size;

    REQUIRE_SERVER;

    if (!URI) {
        nebSetErr(server, "parameter 'URI' may not be NULL");

        return 0;
    }

    if (!filename) {
        nebSetErr(server, "parameter 'filename' may not be NULL");

        return 0;
    }

    status = regcomp(&myregex, pattern, REG_EXTENDED);
    if (status != 0) {
        regerror(status, &myregex, errbuf, ERRBUF_SIZE);
        nebSetErr(server, "regcomp error: %s", errbuf);

        return 0;
    }

    status = regexec(&myregex, URI, 2, mymatch, 0);
    if (status != 0) {
        regerror(status, &myregex, errbuf, ERRBUF_SIZE);
        nebSetErr(server, "regexec error: %s", errbuf);

        return 0;
    }

    regfree(&myregex);

    matchStart = (size_t)mymatch[1].rm_so;
    matchEnd   = (size_t)mymatch[1].rm_eo;
    matchLength = matchEnd - matchStart;

    if (matchStart == -1) {
        return 0;
    }

    filename_size = matchLength + 1;
    *filename = xmalloc(filename_size);

    snprintf(*filename, filename_size, "%.*s", (int)matchLength, URI + matchStart);

    return strlen(*filename);
}

static bool nebNukeFile(nebServer *server, const char *filename)
{
    REQUIRE_SERVER;

    if (!filename) {
        nebSetErr(server, "parameter 'filename' may not be NULL");

        return false;
    }

    if (unlink(filename) < 0) {
        nebSetErr(server, "unlink: %s", strerror(errno));

        return false;
    }

    return true;
}

static char *nullstrncpy(char *dest, const char *src, size_t n) {
    if (!src) {
        dest = NULL;
        return dest;
    }

    return strncpy(dest, src, n);
}
