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

};

#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];

	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 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 = api->location ? api->location : api->last_url;


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

		return where;
	}

	return NULL;

}

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;
	api->retry_count = 0;
	const char *where = buffer;

	if(api->url && url[0] == '/') {
		sprintf( buffer, "%s%s" , api->url, url);
	}
	else
	{
		strcpy(buffer, url);
	}

	do {
		v = _ecmwf_api_call(api, method, where, json);
	} while((where = 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);
	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 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
