/*
 * © Copyright 1996-2012 ECMWF.
 *
 * This software is licensed under the terms of the Apache Licence Version 2.0
 * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
 * In applying this licence, ECMWF does not waive the privileges and immunities
 * granted to it by virtue of its status as an intergovernmental organisation nor
 * does it submit to any jurisdiction.
 */
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>
#ifndef NOCURL
#include <curl/curl.h>
#include "api.h"
#include "json.h"
#include "mars.h"

#define SKIP_PEER_VERIFICATION

#define INITIAL_BUFFER_SIZE 10240


static CURLM* multi_handle = 0;
static int first           = 1;

struct _ecmwf_api {
    int error;

    char* key;
    char* email;
    char* url;

    char* location;
    size_t location_len;

    char* type;
    size_t type_len;


    int retry_wait;
    int retry_count;
    long code;

    long verbose;

    char* buffer;
    size_t pos;
    size_t len;

    int offset;
    int limit;

    CURL* curl;
    struct curl_slist* chunk;
    FILE* file;

    int active;

    json_value* value;

    CURLcode curl_error;

    typeproc typecb;
    void* typecb_data;

    messageproc msgcb;
    void* msgcb_data;

    char* last_url;
    char* full_url;
};

#define _(a) _call(api, #a, a)

static void _call(ecmwf_api* api, const char* what, CURLcode code) {
    api->curl_error = code;
    if (code != CURLE_OK) {
        marslog(LOG_EROR, "%s failed: %s", what, curl_easy_strerror(code));
        api->error++;
    }
}

void ecmwf_api_set_msg_callback(ecmwf_api* api, messageproc msgcb, void* data) {
    api->msgcb      = msgcb;
    api->msgcb_data = data;
}

static void wait_for_data(ecmwf_api* api, size_t size) {
    fd_set fdr, fdw, fdx;
    struct timeval timeout;

    if ((!api->active) || (api->pos > size))
        return;

    do {
        int maxfd = -1;
        long time = -1;

        FD_ZERO(&fdr);
        FD_ZERO(&fdw);
        FD_ZERO(&fdx);

        _(curl_multi_timeout(multi_handle, &time));
        if (time >= 0) {
            timeout.tv_sec = time / 1000;
            if (timeout.tv_sec > 1)
                timeout.tv_sec = 1;
            else
                timeout.tv_usec = (time % 1000) * 1000;
        }
        else {
            timeout.tv_sec  = 1;
            timeout.tv_usec = 0;
        }

        _(curl_multi_fdset(multi_handle, &fdr, &fdw, &fdx, &maxfd));

        if (select(maxfd + 1, &fdr, &fdw, &fdx, &timeout) == -1) {
            perror("select");
            api->error++;
        }
        else {
            _(curl_multi_perform(multi_handle, &api->active));
        }

    } while (api->active && (api->pos < size));
}

void ecmwf_wait_for_data(ecmwf_api* api, size_t size) {
    wait_for_data(api, size);
}


size_t ecmwf_api_transfer_read(ecmwf_api* api, void* ptr, size_t size) {
    wait_for_data(api, size);

    if (api->pos == 0)
        return 0; /* EOF */

    if (api->pos < size)
        size = api->pos;

    memcpy(ptr, api->buffer, size);

    if (api->pos - size <= 0) {
        api->pos = 0;
    }
    else {
        memmove(api->buffer, api->buffer + size, api->pos - size);
        api->pos -= size;
    }

    return size;
}


static size_t write_callback(void* ptr, size_t size, size_t nmemb, void* userdata) {
    ecmwf_api* api = (ecmwf_api*)userdata;
    size *= nmemb;

    if (api->buffer == NULL) {
        api->len    = INITIAL_BUFFER_SIZE;
        api->buffer = malloc(api->len);
    }

    if (api->pos + size > api->len) {

        while (api->pos + size > api->len) {
            api->len *= 2;
        }

        api->buffer = realloc(api->buffer, api->len);
        if (!api->buffer) {
            api->error++;
            return 0;
        }
    }

    memcpy(api->buffer + api->pos, ptr, size);
    api->pos += size;

    return size;
}

#define MAXLEN 256

static void print_max(const char* msg) {
    char buf[MAXLEN];
    int i         = 0;
    const char* p = msg;
    const char* q = msg;
    int flag      = LOG_EROR;

    memset(buf, 0, sizeof(buf));

    while (*p) {
        if (*p == '\n') {
            marslog(flag, "%s", buf);
            memset(buf, 0, sizeof(buf));
            flag = LOG_NONE;
            i    = 0;
        }
        else {
            if (i < MAXLEN - 4) {
                buf[i++] = *p;
            }
            else {
                buf[MAXLEN - 2] = '.';
                buf[MAXLEN - 3] = '.';
                buf[MAXLEN - 4] = '.';
            }
        }
        p++;
    }

    if (i) {
        marslog(flag, "%s", buf);
    }
}

static void print_error(json_value* value) {
    json_value* e = json_object_find(value, "error");
    json_value* r = json_object_find(value, "reason");

    if (e)
        print_max(json_get_string(e));
    if (r)
        print_max(json_get_string(r));
}

static void print_messages(int i, json_value* e, void* userdata) {
    ecmwf_api* api = (ecmwf_api*)userdata;
    if (api->msgcb) {
        (*api->msgcb)(json_get_string(e), api->msgcb_data);
    }
    api->offset = i + 1;
}

static size_t headers_callback(void* ptr, size_t size, size_t nmemb, void* userdata) {
    ecmwf_api* api = (ecmwf_api*)userdata;

    char* p   = (char*)ptr;
    char* q   = (char*)ptr;
    int count = size * nmemb;

    while (count-- > 0) {
        if (*p == ':') {
            if (q == (char*)ptr) {
                char* s = q;
                *p      = 0;
                q       = p;
                if (strcasecmp("location", s) == 0) {
                    while (count-- > 0) {
                        if (*p == '\r') {
                            *p = 0;
                            if (api->location_len < p - q) {
                                free(api->location);
                                api->location = NULL;
                            }
                            if (!api->location) {
                                api->location_len = p - q;
                                api->location     = malloc(api->location_len);
                                if (!api->location) {
                                    api->error++;
                                    return 0;
                                }
                            }
                            strcpy(api->location, q + 2);
                            *p = '\n';
                        }
                        p++;
                    }
                }
                if (strcasecmp("content-type", s) == 0) {
                    while (count-- > 0) {
                        if (*p == '\r') {
                            *p = 0;
                            if (api->type_len < p - q) {
                                free(api->type);
                                api->type = NULL;
                            }
                            if (!api->type) {
                                api->type_len = p - q;
                                api->type     = malloc(api->type_len);
                                if (!api->type) {
                                    api->error++;
                                    return 0;
                                }
                            }
                            strcpy(api->type, q + 2);
                            *p = '\n';
                        }
                        p++;
                    }

                    if (api->typecb) {
                        api->typecb(api->type, api->typecb_data);
                    }
                }
                if (strcasecmp("retry-after", s) == 0) {
                    while (count-- > 0) {
                        if (*p == '\r') {
                            *p              = 0;
                            api->retry_wait = atol(q + 2);
                            *p              = '\r';
                        }
                        p++;
                    }
                }
                *q = ':';
            }
        }
        p++;
    }

    return size * nmemb;
}

static void init(ecmwf_api* api, const char* method, const char* url) {
    if (api->last_url) {
        free(api->last_url);
        api->last_url = strdup(url);
    }

    api->curl  = curl_easy_init();
    api->error = 0;

    if (!api->curl) {
        api->error++;
        return;
    }

    _(curl_easy_setopt(api->curl, CURLOPT_VERBOSE, api->verbose));

    _(curl_easy_setopt(api->curl, CURLOPT_URL, url));

    api->chunk = curl_slist_append(api->chunk, "Accept: application/json");
    api->chunk = curl_slist_append(api->chunk, "Content-Type: application/json");
    api->chunk = curl_slist_append(api->chunk, "charsets: utf-8");
    api->chunk = curl_slist_append(api->chunk, api->key);
    api->chunk = curl_slist_append(api->chunk, api->email);

#ifdef SKIP_PEER_VERIFICATION
    _(curl_easy_setopt(api->curl, CURLOPT_SSL_VERIFYPEER, 0L));
#endif

#ifdef SKIP_HOSTNAME_VERIFICATION
    _(curl_easy_setopt(api->curl, CURLOPT_SSL_VERIFYHOST, 0L));
#endif

    _(curl_easy_setopt(api->curl, CURLOPT_HTTPHEADER, api->chunk));
    _(curl_easy_setopt(api->curl, CURLOPT_CUSTOMREQUEST, method));
    _(curl_easy_setopt(api->curl, CURLOPT_USERAGENT, "mars/1.0"));
}

static void cleanup(ecmwf_api* api) {
    /* always cleanup */
    if (api->curl)
        curl_easy_cleanup(api->curl);

    if (api->chunk)
        curl_slist_free_all(api->chunk);

    api->curl  = NULL;
    api->chunk = NULL;
    api->pos   = 0;
}

const json_value* _ecmwf_api_call(ecmwf_api* api, const char* method, const char* url, const char* json) {

    char buf[10240];

    marslog(LOG_DBUG, "_ecmwf_api_call [%s] [%s] [%s]", method, url, json);

    json_free(api->value);
    api->value = NULL;

    api->error = 0;

    if (strcmp(method, "GET") == 0) {
        init(api, method, url);
    }
    else {
        sprintf(buf, "%s?offset=%d&limit=%d", url, api->offset, api->limit);
        init(api, method, buf);
    }


    if (json) {
        _(curl_easy_setopt(api->curl, CURLOPT_POSTFIELDS, json));
    }

    api->pos = 0;
    _(curl_easy_setopt(api->curl, CURLOPT_HEADERFUNCTION, &headers_callback));
    _(curl_easy_setopt(api->curl, CURLOPT_HEADERDATA, api));

    _(curl_easy_setopt(api->curl, CURLOPT_WRITEFUNCTION, &write_callback));
    _(curl_easy_setopt(api->curl, CURLOPT_WRITEDATA, api));

    /* Perform the request, res will get the return code */
    _(curl_easy_perform(api->curl));
    _(curl_easy_getinfo(api->curl, CURLINFO_RESPONSE_CODE, &api->code));


    api->value = json_parse_string(api->buffer, api->pos);

    if (api->verbose) {
        json_println(api->value);
    }

    print_error(api->value);

    cleanup(api);

    json_array_each(json_object_find(api->value, "messages"), print_messages, api);

    if (api->code >= 200 && api->code <= 400) {
        return api->value;
    }

    marslog(LOG_EROR, "HTTP error code: %ld", api->code);

    return NULL;
}

const char* ecmwf_api_must_retry(ecmwf_api* api) {
    int retry = 0;

    /* Failed to connect */
    if (api->code == 0) {
        retry = 1;
    }

    if (api->code >= 500 && api->code <= 599) {
        retry = 1;
    }

    if (api->code == 301 || api->code == 302) {
        return ecmwf_api_full(api, api->location);
    }

    if (retry) {
        if (api->code >= 100 && api->code <= 499) {

            if (api->location_len < 10240) {
                free(api->location);
                api->location = NULL;
            }
            if (!api->location) {
                api->location_len = 10240;
                api->location     = malloc(api->location_len);
                if (!api->location) {
                    api->error++;
                    return 0;
                }
            }

            _(curl_easy_getinfo(api->curl, CURLINFO_EFFECTIVE_URL, api->location));
        }

        const char* where = ecmwf_api_full(api, api->location ? api->location : api->last_url);


        marslog(LOG_EROR, "ecmwf_api: error %ld, retrying... %s in 2 minutes", api->code, where ? where : "?");
        sleep(120);
        api->retry_count++;

        return where;
    }

    return NULL;
}

const char* ecmwf_api_full(ecmwf_api* api, const char* url) {
    char buffer[10240];

    if (!url) {
        return api->url;
    }


    marslog(LOG_DBUG, "ecmwf_api_full => [%s] [%s]", api->url, url);

    if (strncmp(url, "http://", 7) == 0 || strncmp(url, "https://", 8) == 0) {
        marslog(LOG_DBUG, "ecmwf_api_full <= [%s]", url);
        return url;
    }

    if (url[0] == '/') {
        const char* p = api->url;
        int i         = 0;
        int n         = 0; /* http://.../ */

        while (*p) {
            if (*p == '/') {
                n++;
                if (n == 3) {
                    break;
                }
            }
            buffer[i] = *p;
            p++;
            i++;
        }

        p = url;
        while (*p) {
            buffer[i++] = *p++;
        }
        buffer[i] = 0;
    }
    else {
        if (api->url[strlen(api->url) - 1] == '/') {
            sprintf(buffer, "%s%s", api->url, url);
        }
        else {
            sprintf(buffer, "%s/%s", api->url, url);
        }
    }


    if (api->full_url) {
        free(api->full_url);
        api->full_url = NULL;
    }
    api->full_url = strdup(buffer);
    marslog(LOG_DBUG, "ecmwf_api_full <= [%s]", api->full_url);
    return api->full_url;
}

const json_value* ecmwf_api_call(ecmwf_api* api, const char* method, const char* url, const char* json) {

    // char buffer[10240];
    const json_value* v;
    const char* full = ecmwf_api_full(api, url);

    api->retry_count = 0;


    do {
        v = _ecmwf_api_call(api, method, full, json);
    } while ((full = ecmwf_api_must_retry(api)) != NULL);

    return v;
}

ecmwf_api* ecmwf_api_create(const char* url, const char* key, const char* email) {
    ecmwf_api* api = calloc(1, sizeof(ecmwf_api));
    if (first) {
        curl_global_init(CURL_GLOBAL_DEFAULT);
        first = 0;
    }
    api->limit = 500;

    if (key) {
        api->key = malloc(strlen(key) + strlen("X-ECMWF-KEY: ") + 1);
        sprintf(api->key, "X-ECMWF-KEY: %s", key);
    }

    if (email) {
        api->email = malloc(strlen(email) + strlen("From: ") + 1);
        sprintf(api->email, "From: %s", email);
    }

    if (url)
        api->url = strdup(url);

    return api;
}

void ecmwf_api_add_message_callback(ecmwf_api* api, void (*cb)(const char*)) {
}

void ecmwf_api_destroy(ecmwf_api* api) {
    cleanup(api);
    json_free(api->value);
    api->value = NULL;
    if (api->buffer)
        free(api->buffer);
    if (api->location)
        free(api->location);
    if (api->type)
        free(api->type);
    if (api->key)
        free(api->key);
    if (api->email)
        free(api->email);
    if (api->url)
        free(api->url);
    if (api->last_url)
        free(api->last_url);
    if (api->full_url)
        free(api->full_url);
    free(api);
    /* curl_global_cleanup(); */
}

int ecmwf_api_in_progress(ecmwf_api* api) {
    return api->code == 202;
}

int ecmwf_api_transfer_ready(ecmwf_api* api) {
    return api->code == 303;
}

void ecmwf_api_in_wait(ecmwf_api* api) {
    if (api->retry_wait)
        sleep(api->retry_wait);
}

const char* ecmwf_api_location(ecmwf_api* api) {
    return ecmwf_api_full(api, api->location);
}

const char* ecmwf_api_content_type(ecmwf_api* api) {
    return api->type;
}

int ecmwf_api_error(ecmwf_api* api) {
    return api->error;
}

void ecmwf_api_verbose(ecmwf_api* api, int v) {
    api->verbose = v;
}

long long ecmwf_api_transfer_start(ecmwf_api* api, const char* url, typeproc typecb, void* typecb_data) {

    init(api, "GET", url);

    if (!multi_handle)
        multi_handle = curl_multi_init();

    api->typecb      = typecb;
    api->typecb_data = typecb_data;

    api->pos = 0;
    _(curl_easy_setopt(api->curl, CURLOPT_HEADERFUNCTION, &headers_callback));
    _(curl_easy_setopt(api->curl, CURLOPT_HEADERDATA, api));

    _(curl_easy_setopt(api->curl, CURLOPT_WRITEFUNCTION, &write_callback));
    _(curl_easy_setopt(api->curl, CURLOPT_WRITEDATA, api));

    _(curl_multi_add_handle(multi_handle, api->curl));

    _(curl_multi_perform(multi_handle, &api->active));

    if ((api->pos == 0) && (!api->active)) {
        curl_multi_remove_handle(multi_handle, api->curl);
        api->error++;
    }

    return json_get_integer(json_object_find(api->value, "size"));
}

int ecmwf_api_transfer_end(ecmwf_api* api) {
    curl_easy_getinfo(api->curl, CURLINFO_RESPONSE_CODE, &api->code);
    cleanup(api);
    if (api->code != 200)
        marslog(LOG_EROR, "Transfer return code is %d", api->code);
    return api->code == 200;
}

#endif
