/*
 * © 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 <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <strings.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 = 60;
			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
