/*
 * © 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.
 */

#if (defined(__APPLE__) && defined(__MACH__))
#include <ctype.h>
#endif
#include <ctype.h>
#include "api.h"
#include "mars.h"


#ifndef NOCURL

typedef struct apidata {
    ecmwf_api* api;
    char* last;
    long long total;
    long long length;
    int tranfer;
    double start;
    char status[80];
    int wmo;
    int content_type_known;
    boolean json;
    char* datasets;
    char* service;
    char* url;
    char* email;
    char* token;
    char service_name[128];
    boolean remote_postproc;
} apidata;

static void apibase_init(void);

static err apibase_open(void* data, request*, request* env, int);
static err apibase_close(void* data);
static err apibase_read(void* data, request* r, void* buffer, long* length);
static err apibase_write(void* data, request* r, void* buffer, long* length);
static err apibase_cntl(void* data, int code, void* param, int size);
static boolean apibase_check(void* data, request* r);
static err apibase_validate(void* data, request*, request*, int);

static option opts[] = {

#if 0
	{	"json", "MARS_JSON_", "-json", "1",
		t_boolean, sizeof(boolean), OFFSET(apidata, json)
	},
#endif

    {"url", NULL, NULL, NULL, t_str,
     sizeof(char*), OFFSET(apidata, url)},

    {"email", "MARS_USER_EMAIL", NULL, NULL, t_str,
     sizeof(char*), OFFSET(apidata, email)},

    {"token", "MARS_USER_TOKEN", NULL, NULL, t_str,
     sizeof(char*), OFFSET(apidata, token)},

    {"datasets", NULL, NULL, NULL, t_str,
     sizeof(char*), OFFSET(apidata, datasets)},


    {"service", "MARS_API_SERVICE", NULL, "services/mars", t_str,
     sizeof(char*), OFFSET(apidata, service)},

    {"remote_postproc", "MARS_API_REMOTE_POSTPROC", NULL, "1",
     t_boolean, sizeof(boolean), OFFSET(apidata, remote_postproc)},
};

static base_class _apibase_base = {

    NULL,      /* parent class */
    "apibase", /* name         */

    false, /* inited       */

    sizeof(apidata), /* private_size  */
    NUMBER(opts),    /* options_count */
    opts,            /* options       */

    apibase_init, /* init          */

    apibase_open,  /* open          */
    apibase_close, /* close         */

    apibase_read,  /* read          */
    apibase_write, /* write         */

    apibase_cntl, /* control       */

    apibase_check, /* check         */

    NULL, /* query        */

    NULL, /* archive      */
    NULL, /* admin        */

    apibase_validate, /* validate */

};

/* the only 'public' variable ... */

base_class* apibase = &_apibase_base;

static FILE* post_open(apidata*, const char*, const char*, const char*);

static void apibase_init(void) {
}

static char* proxies[] = {
    "HTTP_PROXY",
    "FTP_PROXY",
    "HTTPS_PROXY",
    "http_proxy",
    "https_proxy",
    "no_proxy",
    "ftp_proxy",
};


static void typecb(const char* type, void* data) {
    apidata* api = (apidata*)data;

    marslog(LOG_INFO, "Type is %s", type);
    if (strcmp(type, "application/x-grib") == 0 || strcmp(type, "application/x-bufr") == 0) {
        api->wmo = 1;
    }
    else {
        api->wmo = 0;
    }
    api->content_type_known = 1;
}

static char* APIservice(void* data, const char* name) {
    apidata* api = (apidata*)data;
    sprintf(api->service_name, "%s/%s", api->service, name);
    marslog(LOG_DBUG, "Calling service: %s", api->service_name);
    return api->service_name;
}

static char* json2string(const json_value* j) {
    static char* buf = NULL;
    off_t n;

    char* tmp = marstmp();
    FILE* f   = fopen(tmp, "w");

    if (f == NULL) {
        marslog(LOG_EROR | LOG_PERR, "%s", tmp);
        return NULL;
    }
    json_save(j, f);
    fclose(f);

    f = fopen(tmp, "r");
    if (f == NULL) {
        marslog(LOG_EROR | LOG_PERR, "%s", tmp);
        return NULL;
    }

    if (buf)
        free(buf);

    fseek(f, 0, 2);
    n = ftell(f);
    rewind(f);
    buf = (char*)MALLOC(n + 2);

    n      = fread(buf, 1, n, f);
    buf[n] = 0;

    fclose(f);

    unlink(tmp);

    return buf;
}


json_value* request2json(const request* r) {
    json_value* o = json_new_object();
    parameter* p  = r->params;
    while (p) {
        if (*p->name != '_') {
            json_value* a = json_new_array();
            value* v      = p->values;
            while (v) {
                json_array_push_item(a, json_new_string(v->name));
                v = v->next;
            }
            json_object_set_item(o, p->name, a);
        }
        p = p->next;
    }
    return o;
}


static void messages(const char* msg, void* data) {
    apidata* api = (apidata*)data;


    // if (mars.debug)
    // {
    // 	marslog(LOG_DBUG, " MESSAGE ==> %s", msg);
    // }


    if (strncmp(msg, "mars - ", 7) == 0) {
        int state = 0;
        int mode  = -1;

        while (*msg) {

            switch (*msg) {
                case '-':
                    state++;
                    if (state == 3 && mode != -1) {
                        msg++;
                        while (*msg && isspace(*msg))
                            msg++;
                        if (*msg) {
                            marslog(mode, "%s [%s]", msg, database_name(api));
                        }
                    }
                    break;

                case 'W':
                    if (state == 1 && mode == -1)
                        mode = LOG_WARN;
                    break;

                case 'I':
                    if (state == 1 && mode == -1)
                        mode = LOG_INFO;
                    break;

                case 'E':
                    if (state == 1 && mode == -1)
                        mode = LOG_EROR;
                    break;

                case 'D':
                    if (state == 1 && mode == -1)
                        mode = LOG_DBUG;
                    break;

                case 'F':
                    if (state == 1 && mode == -1)
                        mode = LOG_EXIT;
                    break;
            }
            if (*msg)
                msg++;
        }
    }
}

static err apibase_open(void* data, request* _r, request* env, int mode) {
    apidata* api = (apidata*)data;
    char path[PATH_MAX];
    const json_value* v;
    const char *key = NULL, *email = NULL, *url = api->url;
    char* tmp;
    char* rs;
    const char* s;
    int i;
    request* r = empty_request(_r->name);

    reqcpy(r, _r);
    if (!api->remote_postproc) {
        // We use the local interpolation
        unset_value(r, "GRID");
        unset_value(r, "AREA");
        unset_value(r, "RESOL");
        unset_value(r, "FRAME");
        unset_value(r, "ROTATION");
        unset_value(r, "BITMAP");
        unset_value(r, "INTERPOLATION");
        unset_value(r, "FORMAT");
        unset_value(r, "PACKING");
        unset_value(r, "TRUNCATION");
        unset_value(r, "INTGRID");
        unset_value(r, "LSM");
    }

    if (api->url) {
        /* Access via a configured database */
        url       = api->url;
        key       = api->token;
        email     = api->email;
        api->json = true;
    }
    else {
        sprintf(path, "%s/.ecmwfapirc", getenv("HOME"));
        if (access(path, F_OK) == 0) {
            v = json_read_file(path);

            key   = json_get_string(json_object_find(v, "key"));
            email = json_get_string(json_object_find(v, "email"));
            url   = json_get_string(json_object_find(v, "url"));
        }
        else {
            marslog(LOG_EROR | LOG_PERR, "Cannot open %s", path);
        }

        if (getenv("ECMWF_API_KEY"))
            key = getenv("ECMWF_API_KEY");
        if (getenv("ECMWF_API_URL"))
            url = getenv("ECMWF_API_URL");
        if (getenv("ECMWF_API_EMAIL"))
            email = getenv("ECMWF_API_EMAIL");
    }

    if (!key) {
        marslog(LOG_EROR, "API KEY is missing");
        return -1;
    }

    if (!url) {
        marslog(LOG_EROR, "API URL is missing");
        return -1;
    }

    if (!email) {
        marslog(LOG_EROR, "API EMAIL is missing");
        return -1;
    }

    /* The service should be configured with the database */
    if (api->datasets) {
        const char* dataset = get_value(r, "DATASET", 0);
        char s[1024];
        if (!dataset) {
            marslog(LOG_EROR, "Please provide a value for DATASET");
            return -1;
        }
        sprintf(s, "%s/%s", api->datasets, dataset);
        api->service = strcache(s);
        api->json    = 1;
    }

    api->api = ecmwf_api_create(url, key, email);
    ecmwf_api_set_msg_callback(api->api, messages, api);

    marslog(LOG_INFO, "ECMWF API is at %s", url);
    marslog(LOG_INFO, "Using MARS service at %s", APIservice(api, ""));

    for (i = 0; i < NUMBER(proxies); i++) {
        char* p = getenv(proxies[i]);
        if (p) {
            marslog(LOG_INFO, "Proxy settings: %s=%s", proxies[i], p);
            if (*p && p[strlen(p) - 1] == '/') {
                marslog(LOG_WARN, "Libcurl does not use proxy settings that finish with a '/'");
            }
        }
    }

    ecmwf_api_verbose(api->api, mars.debug);
    if (getenv("ECMWF_API_DEBUG"))
        ecmwf_api_verbose(api->api, atol(getenv("ECMWF_API_DEBUG")));

    if (!api->url) {
        v = ecmwf_api_call(api->api, "GET", "who-am-i", NULL);
        if (!v) {
            marslog(LOG_EROR, "API: Failed to get user information");
            return -1;
        }

        marslog(LOG_INFO, "ECMWF user id is '%s'", json_get_string(json_object_find(v, "uid")));


        v = ecmwf_api_call(api->api, "GET", APIservice(api, "news"), NULL);
        if (!v) {
            marslog(LOG_EROR, "API: Failed to get service information");
            return -1;
        }

        if ((v != NULL) && ((v = json_object_find(v, "news")) != NULL)) {
            char* p   = strdup(json_get_string(v));
            char* q   = p;
            char* tok = "\n";
            while (strtok(p, tok)) {
                marslog(LOG_INFO, "%s", p);
                p = NULL;
            }
            free(q);
        }
    }

    if (api->json) {
        if (api->url) {
            /* Here, we assume that the request is already expanded by the client */
            json_value* j = json_new_object();
            json_object_set_item(j, lowcase(r->name), request2json(r));
            json_object_set_item(j, "environ", request2json(env));
            v = ecmwf_api_call(api->api, "POST", APIservice(api, "requests"), json2string(j));
            json_free(j);
        }
        else {
            json_value* j = request2json(r);
            v             = ecmwf_api_call(api->api, "POST", APIservice(api, "requests"), json2string(j));
            json_free(j);
        }
    }
    else {
        rs  = request2string(r);
        tmp = MALLOC(strlen(rs) * 3);
        v   = ecmwf_api_call(api->api, "POST", APIservice(api, "requests"), json_encode_string(request2string(r), tmp));
        FREE(tmp);
    }

    if (!v) {
        marslog(LOG_EROR, "API: Failed to send request");
        return -1;
    }

    marslog(LOG_INFO, "Request ID is %s", json_get_string(json_object_find(v, "name")));
    s = json_get_string(json_object_find(v, "status"));
    if (s && strcmp(s, api->status) != 0) {
        strcpy(api->status, s);
        marslog(LOG_INFO, "Request is %s", s);
    }

    api->last = strcache(ecmwf_api_location(api->api));

    while (ecmwf_api_in_progress(api->api)) {
        ecmwf_api_in_wait(api->api);
        strfree(api->last);
        api->last = strcache(ecmwf_api_location(api->api));
        v         = ecmwf_api_call(api->api, "GET", ecmwf_api_location(api->api), NULL);
        if (ecmwf_api_transfer_ready(api->api)) {
            break;
        }
        if (!v) {
            marslog(LOG_EROR, "API: Failed to get request status");
            return -1;
        }
        s = json_get_string(json_object_find(v, "status"));
        if (s && strcmp(s, api->status) != 0) {
            strcpy(api->status, s);
            marslog(LOG_INFO, "Request is %s", s);
        }
    }

    if (!ecmwf_api_transfer_ready(api->api)) {
        marslog(LOG_EROR, "API: Failed to initiate transfer");
        return -1;
    }

    api->tranfer = 1;

    url = ecmwf_api_location(api->api);

    api->total  = 0;
    api->length = ecmwf_api_transfer_start(api->api, url, typecb, api);

    marslog(LOG_INFO, "Transfering %s from %s", bytename(api->length), url);
    timer_start(get_timer("Transfer", NULL, 0));
    api->start = timer_value(get_timer("Transfer", NULL, 0));

    return 0;
}

static err apibase_close(void* data) {
    apidata* api = (apidata*)data;
    int e        = 0;
    if (api->tranfer) {
        timer_stop(get_timer("Transfer", NULL, 0), api->length);
        timer_partial_rate(get_timer("Transfer", NULL, 0), api->start, api->length);
        if (!ecmwf_api_transfer_end(api->api)) {
            marslog(LOG_EROR, "API: Transfer failed");
            e = -1;
        }
    }
    if (api->last) {
        ecmwf_api_call(api->api, "DELETE", api->last, NULL);
        strfree(api->last);
    }
    if (api->api)
        ecmwf_api_destroy(api->api);

    return e;
}

static long readcb(void* data, void* buffer, long len) {
    apidata* api = (apidata*)data;

    long size = ecmwf_api_transfer_read(api->api, buffer, len);
    if (size > 0)
        api->total += size;
    return size ? size : EOF;
}

static err apibase_read(void* data, request* r, void* buffer, long* length) {
    apidata* api = (apidata*)data;
    size_t len   = *length;

    if (!api->content_type_known) {
        ecmwf_wait_for_data(api->api, 1024);
        if (!api->content_type_known) {
            marslog(LOG_WARN, "apibase_read: Did not receive the header information");
            return -12;
        }
    }


    if (api->wmo) {
        err ret = wmo_read_any_from_stream(data, &readcb, buffer, &len);
        *length = len;
        if (ret != 0 && ret != -1) {
            marslog(LOG_EROR, "apibase_read:  wmo_read_any_from_stream returns %ld", (long)ret);
            if (ret != BUF_TO_SMALL)
                return RETRY_ERR;
        }
        return ret;
    }
    else {
        long size = ecmwf_api_transfer_read(api->api, buffer, len);
        if (size > 0) {
            *length = size;
            api->total += size;
            return 0;
        }
        return -1;
    }
}

static err apibase_write(void* data, request* r, void* buffer, long* length) {
    return -1;
}

static err apibase_list(void* data, request* r) {
    return -1;
}

static err apibase_cntl(void* data, int code, void* param, int size) {
    switch (code) {
        case CNTL_LIST:
            return -1;
            /*NOTREACHED*/
            break;

        default:
            return -1;
            /*NOTREACHED*/
            break;
    }
    return -1;
}

static boolean apibase_check(void* data, request* r) {
    return true;
}

static err apibase_validate(void* data, request* r, request* e, int mode) {
    err ret = NOERR;

    /*	if(net->validate)*/
    /*		ret = validate_request(r,e,net->validate);*/

    return ret;
}

#else
extern base_class _nullbase;
base_class* apibase = &_nullbase;
#endif
