#include <ctype.h>
#include <stdlib.h>
#include <string.h>

#include "base.h"
#include "log.h"
#include "buffer.h"

#include "plugin.h"

#include "response.h"
#include "stat_cache.h"

#define CONFIG_UPLOAD_PROGRESS_URL "upload-progress.progress-url"
#define CONFIG_UPLOAD_PROGRESS_TIMEOUT "upload-progress.remove-timeout"
#define CONFIG_UPLOAD_PROGRESS_DEBUG "upload-progress.debug"

/**
 * uploadprogress for lighttpd
 *
 * Initial: Jan Kneschke <jan@kneschke.de>
 * Timeout+Status addon: Bjoern Kalkbrenner <terminar@cyberphoria.org> [20070112]
 *
 * the timeout is used to keep in the status information intact even if the parent 
 * connection is gone already
 */

typedef struct {
	buffer     *tracking_id;
	connection *con;

	time_t timeout;
	int status;
} connection_map_entry;

typedef struct {
	connection_map_entry **ptr;

	size_t used;
	size_t size;
} connection_map;

/* plugin config for all request/connections */

typedef struct {
	buffer *progress_url;
	unsigned short debug;
	unsigned short remove_timeout;
} plugin_config;

typedef struct {
	PLUGIN_DATA;

	connection_map *con_map;

	buffer *tmp_buf; /** used as temporary buffer for extracting the tracking id */

	plugin_config **config_storage;

	plugin_config conf;
} plugin_data;

/**
 *
 * connection maps
 *
 */

/* init the plugin data */
static connection_map *connection_map_init() {
	connection_map *cm;

	cm = calloc(1, sizeof(*cm));

	return cm;
}

static void connection_map_free(connection_map *cm) {
	size_t i;
	for (i = 0; i < cm->size; i++) {
		connection_map_entry *cme = cm->ptr[i];

		if (!cme) break;

		if (cme->tracking_id) {
			buffer_free(cme->tracking_id);
		}
		free(cme);
	}

	free(cm);
}

static connection_map_entry *connection_map_insert(connection_map *cm, buffer *tracking_id, connection *con) {
	connection_map_entry *cme;
	size_t i;

	if (cm->size == 0) {
		cm->size = 16;
		cm->ptr = malloc(cm->size * sizeof(*(cm->ptr)));
		for (i = 0; i < cm->size; i++) {
			cm->ptr[i] = NULL;
		}
	} else if (cm->used == cm->size) {
		cm->size += 16;
		cm->ptr = realloc(cm->ptr, cm->size * sizeof(*(cm->ptr)));
		for (i = cm->used; i < cm->size; i++) {
			cm->ptr[i] = NULL;
		}
	}

	if (cm->ptr[cm->used]) {
		/* is already alloced, just reuse it */
		cme = cm->ptr[cm->used];
	} else {
		cme = malloc(sizeof(*cme));
		cme->tracking_id = buffer_init();
	}
	cme->timeout = 0;
	cme->status = 0;
	buffer_copy_string_buffer(cme->tracking_id, tracking_id);
	cme->con = con;

	cm->ptr[cm->used++] = cme;

	return cme;
}

static connection_map_entry *connection_map_get_connection_entry(connection_map *cm, buffer *tracking_id) {
	size_t i;

	for (i = 0; i < cm->used; i++) {
		connection_map_entry *cme = cm->ptr[i];

		if (buffer_is_equal(cme->tracking_id, tracking_id)) {
			/* found connection */
			return cme;
		}
	}
	return NULL;
}

static int connection_map_remove_connection(connection_map *cm, connection_map_entry *entry) {
	size_t i;

	for (i = 0; i < cm->used; i++) {
		connection_map_entry *cme = cm->ptr[i];

		if (cme == entry) {
			/* found connection */
			buffer_reset(cme->tracking_id);
			cme->timeout=0;
			cme->status=0;

			cm->used--;

			/* swap positions with the last entry */
			if (cm->used) {
				cm->ptr[i] = cm->ptr[cm->used];
				cm->ptr[cm->used] = cme;
			}

			return 1;
		}
	}

	return 0;
}

/**
 * remove dead tracking IDs 
 *
 * uploadprogress.remove-timeout sets a grace-period in which the 
 * connection status is still known even of the connection is already 
 * being removed
 *
 */
static void connection_map_clear_timeout_connections(connection_map *cm) {
	size_t i;
	time_t now_t = time(NULL);

	for (i = 0; i < cm->used; i++) {
		connection_map_entry *cme = cm->ptr[i];

		if (cme->timeout != 0 && cme->timeout < now_t) {
			/* found connection */
			connection_map_remove_connection(cm, cme);
		}
	}
}

/** 
 * extract the tracking-id from the parameters 
 *
 * for POST requests it is part of the request headers
 * for GET requests ... too
 */
static buffer *get_tracking_id(plugin_data *p, connection *con) {
	data_string *ds;
	buffer *b = NULL;
	char *qstr=NULL;
	size_t i;

	/* the request has to contain a 32byte ID */
	if (NULL == (ds = (data_string *)array_get_element(con->request.headers, CONST_STR_LEN("X-Progress-ID")))) {
		char *amp = NULL;

		/* perhaps the POST request is using the querystring to pass the X-Progress-ID */
		if (buffer_is_empty(con->uri.query)) {
			/*
			 * con->uri.query will not be parsed out if a 413 error happens
			 */
			if (NULL != (qstr = strchr(con->request.uri->ptr, '?'))) {
				/** extract query string from request.uri */
				buffer_copy_string(con->uri.query, qstr + 1);
			} else {
				return NULL;
			}
		}

		/** split the query-string and extract the X-Progress-ID */
		do {
			char *eq = NULL;
			char *start = amp ? amp + 1 : con->uri.query->ptr;

			amp = strchr(start, '&');

			/* check the string between start and amp for = */

			if (amp) {
				buffer_copy_string_len(p->tmp_buf, start, amp - start);
			} else {
				buffer_copy_string(p->tmp_buf, start);
			}

			eq = strchr(p->tmp_buf->ptr, '=');
			
			if (eq) {
				*eq = '\0';

				if (0 == strcmp(p->tmp_buf->ptr, "X-Progress-ID")) {
					size_t key_len = sizeof("X-Progress-ID") - 1;
					size_t var_len = p->tmp_buf->used - 1;
					/* found */

					buffer_copy_string_len(p->tmp_buf, start + key_len + 1, var_len - key_len - 1);
		
					b = p->tmp_buf;

					break;
				} 
			}
		} while (amp);

		if (!b) return NULL;
	} else {
		/* request header was found, use it */
		b = ds->value;
	}

	if (b->used != 32 + 1) {
		if (p->conf.debug) ERROR("the Progress-ID has to be 32 characters long, got %d characters", b->used - 1);
		return NULL;
	}

	for (i = 0; i < b->used - 1; i++) {
		char c = b->ptr[i];

		if (!light_isxdigit(c)) {
			if (p->conf.debug) ERROR("only hex-digits are allowed (0-9 + a-f): (ascii: %d)", c);
			return NULL;
		}
	}

	return b;
}

/* init the plugin data */
INIT_FUNC(mod_uploadprogress_init) {
	plugin_data *p;

	UNUSED(srv);

	p = calloc(1, sizeof(*p));

	p->con_map = connection_map_init();
	p->tmp_buf = buffer_init();

	return p;
}

/* detroy the plugin data */
FREE_FUNC(mod_uploadprogress_free) {
	plugin_data *p = p_d;

	if (!p) return HANDLER_GO_ON;

	if (p->config_storage) {
		size_t i;
		for (i = 0; i < srv->config_context->used; i++) {
			plugin_config *s = p->config_storage[i];

			buffer_free(s->progress_url);
			s->remove_timeout=0;

			free(s);
		}
		free(p->config_storage);
	}

	connection_map_free(p->con_map);
	buffer_free(p->tmp_buf);

	free(p);

	return HANDLER_GO_ON;
}

/* handle plugin config and check values */

SETDEFAULTS_FUNC(mod_uploadprogress_set_defaults) {
	plugin_data *p = p_d;
	size_t i = 0;

	config_values_t cv[] = {
		{ CONFIG_UPLOAD_PROGRESS_URL, NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION },       /* 0 */
		{ CONFIG_UPLOAD_PROGRESS_TIMEOUT, NULL, T_CONFIG_SHORT, T_CONFIG_SCOPE_CONNECTION },    /* 1 */
		{ CONFIG_UPLOAD_PROGRESS_DEBUG, NULL, T_CONFIG_BOOLEAN, T_CONFIG_SCOPE_CONNECTION },    /* 2 */
		{ NULL,                         NULL, T_CONFIG_UNSET, T_CONFIG_SCOPE_UNSET }
	};

	if (!p) return HANDLER_ERROR;

	p->config_storage = calloc(1, srv->config_context->used * sizeof(specific_config *));

	for (i = 0; i < srv->config_context->used; i++) {
		plugin_config *s;

		s = calloc(1, sizeof(plugin_config));
		s->progress_url    = buffer_init();
		s->remove_timeout  = 60;
		s->debug  = 0;

		cv[0].destination = s->progress_url;
		cv[1].destination = &(s->remove_timeout);
		cv[2].destination = &(s->debug);

		p->config_storage[i] = s;

		if (0 != config_insert_values_global(srv, ((data_config *)srv->config_context->data[i])->value, cv)) {
			return HANDLER_ERROR;
		}
	}

	return HANDLER_GO_ON;
}

static int mod_uploadprogress_patch_connection(server *srv, connection *con, plugin_data *p) {
	size_t i, j;
	plugin_config *s = p->config_storage[0];

	PATCH_OPTION(progress_url);
	PATCH_OPTION(remove_timeout);
	PATCH_OPTION(debug);

	/* skip the first, the global context */
	for (i = 1; i < srv->config_context->used; i++) {
		data_config *dc = (data_config *)srv->config_context->data[i];
		s = p->config_storage[i];

		/* condition didn't match */
		if (!config_check_cond(srv, con, dc)) continue;

		/* merge config */
		for (j = 0; j < dc->value->used; j++) {
			data_unset *du = dc->value->data[j];

			if (buffer_is_equal_string(du->key, CONST_STR_LEN(CONFIG_UPLOAD_PROGRESS_URL))) {
				PATCH_OPTION(progress_url);
			} else if (buffer_is_equal_string(du->key, CONST_STR_LEN(CONFIG_UPLOAD_PROGRESS_TIMEOUT))) {
				PATCH_OPTION(remove_timeout);
			} else if (buffer_is_equal_string(du->key, CONST_STR_LEN(CONFIG_UPLOAD_PROGRESS_DEBUG))) {
				PATCH_OPTION(debug);
			}
		}
	}

	return 0;
}

/**
 *
 * the idea:
 *
 * for the first request we check if it is a post-request
 *
 * if no, move out, don't care about them
 *
 * if yes, take the connection structure and register it locally
 * in the progress-struct together with an session-id (md5 ... )
 *
 * if the connections closes, cleanup the entry in the progress-struct
 *
 * a second request can now get the info about the size of the upload,
 * the received bytes
 *
 */

URIHANDLER_FUNC(mod_uploadprogress_uri_handler) {
	plugin_data *p = p_d;
	buffer *tracking_id;
	buffer *b;
	connection_map_entry *post_con_entry = NULL;
	connection_map_entry *map_con_entry = NULL;

	if (buffer_is_empty(con->uri.path)) return HANDLER_GO_ON;

	mod_uploadprogress_patch_connection(srv, con, p);

	/* no progress URL set, ignore request */	
	if (buffer_is_empty(p->conf.progress_url)) return HANDLER_GO_ON;

	switch(con->request.http_method) {
	case HTTP_METHOD_POST:
		/**
		 * a POST request is the UPLOAD itself
		 *
		 * get the unique tracker id 
		 */
		if (NULL == (tracking_id = get_tracking_id(p, con))) {
			return HANDLER_GO_ON;
		}

		if (NULL == (map_con_entry = connection_map_get_connection_entry(p->con_map, tracking_id))) {
			connection_map_insert(p->con_map, tracking_id, con);
		
			if (p->conf.debug) TRACE("POST: connection is new, registered: %s", BUF_STR(tracking_id));
		} else {
			map_con_entry->timeout = 0;
			map_con_entry->status = 0;
			
			if (p->conf.debug) TRACE("POST: connection is known, id: %s", BUF_STR(tracking_id));
		}

		return HANDLER_GO_ON;
	case HTTP_METHOD_GET:
		/**
		 * the status request for the current connection
		 */
		if (p->conf.debug) TRACE("(uploadprogress) urls %s == %s", BUF_STR(con->uri.path), BUF_STR(p->conf.progress_url));

		if (!buffer_is_equal(con->uri.path, p->conf.progress_url)) {
			return HANDLER_GO_ON;
		}

		/* get the tracker id */
		if (NULL == (tracking_id = get_tracking_id(p, con))) {
			return HANDLER_GO_ON;
		}

		buffer_reset(con->physical.path);

		con->file_started = 1;
		con->http_status = 200;
		con->send->is_closed = 1;

		/* send JSON content */

		response_header_overwrite(srv, con, CONST_STR_LEN("Content-Type"), CONST_STR_LEN("text/javascript"));

		/* just an attempt the force the IE/proxies to NOT cache the request */
		response_header_overwrite(srv, con, CONST_STR_LEN("Pragma"), CONST_STR_LEN("no-cache"));
		response_header_overwrite(srv, con, CONST_STR_LEN("Expires"), CONST_STR_LEN("Thu, 19 Nov 1981 08:52:00 GMT"));
		response_header_overwrite(srv, con, CONST_STR_LEN("Cache-Control"),
				CONST_STR_LEN("no-store, no-cache, must-revalidate, post-check=0, pre-check=0"));

		b = chunkqueue_get_append_buffer(con->send);

		/* get the connection */
		if (NULL == (post_con_entry = connection_map_get_connection_entry(p->con_map, tracking_id))) {
			/**
			 * looks like we don't know the tracking id yet, GET and POST out of sync ? */
			BUFFER_APPEND_STRING_CONST(b, "new Object({ 'state' : 'starting' })\r\n");
			
			if (p->conf.debug) TRACE("connection unknown: %s, sending: %s", BUF_STR(tracking_id), BUF_STR(b));

			return HANDLER_FINISHED;
		}

		BUFFER_COPY_STRING_CONST(b, "new Object({ 'state' : ");
	
		if (post_con_entry->status == 413) {
			/* the upload was too large */	
			BUFFER_APPEND_STRING_CONST(b, "'error', 'status' : 413");
		} else if (post_con_entry->con == NULL) {
			/* the connection is already gone */
			buffer_append_string(b, "'done'");
		} else {
			/* the upload is already done, but the connection might be still open */
			buffer_append_string(b, post_con_entry->con->recv->is_closed ? "'done'" : "'uploading'");
			BUFFER_APPEND_STRING_CONST(b, ", 'received' : ");
			buffer_append_off_t(b, post_con_entry->con->recv->bytes_in);
			BUFFER_APPEND_STRING_CONST(b, ", 'size' : ");
			buffer_append_off_t(b, post_con_entry->con->request.content_length == -1 ? 0 : post_con_entry->con->request.content_length);
		}
		BUFFER_APPEND_STRING_CONST(b, "})\r\n");

		if (p->conf.debug) TRACE("connection is known: %s, sending: %s", BUF_STR(tracking_id), BUF_STR(b));

		return HANDLER_FINISHED;
	default:
		break;
	}

	return HANDLER_GO_ON;
}

/**
 * check if request parser sent 413 for our POST request
 */
URIHANDLER_FUNC(mod_uploadprogress_response_header) {
	plugin_data *p = p_d;

	buffer *tracking_id;
	connection_map_entry *map_con_entry = NULL;

	UNUSED(srv);

	/*
	 * we only want to process an 413 (Bad length) error for the upload (POST request)
	 */
	if (con->request.http_method != HTTP_METHOD_POST || con->http_status != 413) {
		return HANDLER_GO_ON;
	}

	if (p->conf.debug) {
		TRACE("response_header: con=%p, http_method=%d, http_status=%d",con,
			con->request.http_method, con->http_status);
	}

	/* get the tracker id */
	if (NULL == (tracking_id = get_tracking_id(p, con))) {
		return HANDLER_GO_ON;
	}

	if (NULL == (map_con_entry = connection_map_get_connection_entry(p->con_map, tracking_id))) {
		/**
		 * in case the request parser meant the request was too large the URI handler won't 
		 * get called. Insert the connection mapping here
		 */
		if (NULL == (map_con_entry = connection_map_insert(p->con_map, tracking_id, con))) {
			return HANDLER_GO_ON;
		}
	}

	/* ok, found our entries, setting 413 here for status */
	map_con_entry->status = 413;
	
	return HANDLER_GO_ON;
}

/**
 * remove the parent connection from the connection mapping
 * when it got closed
 *
 * keep the mapping active for a while to send a valid final status
 */
REQUESTDONE_FUNC(mod_uploadprogress_request_done) {
	plugin_data *p = p_d;
	buffer *tracking_id;
	connection_map_entry *cm = NULL;

	UNUSED(srv);

	if (buffer_is_empty(con->uri.path)) return HANDLER_GO_ON;
	
	/*
	 * only need to handle the upload request.
	 */
	if (con->request.http_method != HTTP_METHOD_POST) {
		return HANDLER_GO_ON;
	}

	if (NULL == (tracking_id = get_tracking_id(p, con))) {
		return HANDLER_GO_ON;
	}

	if (p->conf.debug) {
		TRACE("upload is done, moving tracking-id to backlog: tracking-id=%s, http_status=%d",
				BUF_STR(tracking_id),
				con->http_status);
	}

	/*
	 * set timeout on the upload's connection_map_entry.
	 */
	if (NULL == (cm = connection_map_get_connection_entry(p->con_map, tracking_id))) {
		if (p->conf.debug) {
			TRACE("tracking ID %s not found, can't set timeout", BUF_STR(tracking_id));
		}
		return HANDLER_GO_ON;
	}

	cm->timeout = time(NULL) + p->conf.remove_timeout;
	cm->con     = NULL; /* con becomes invalid very soon */

	return HANDLER_GO_ON;
}

/**
 * remove dead connections once in while 
 */
TRIGGER_FUNC(mod_uploadprogress_trigger) {
	plugin_data *p = p_d;

	if ((srv->cur_ts % 10) != 0) return HANDLER_GO_ON;

	connection_map_clear_timeout_connections(p->con_map);

	return HANDLER_GO_ON;
}


/* this function is called at dlopen() time and inits the callbacks */

LI_EXPORT int mod_uploadprogress_plugin_init(plugin *p) {
	p->version     = LIGHTTPD_VERSION_ID;
	p->name        = buffer_init_string("uploadprogress");

	p->init        = mod_uploadprogress_init;
	p->handle_uri_clean = mod_uploadprogress_uri_handler;
	p->handle_response_done  = mod_uploadprogress_request_done;
	p->set_defaults  = mod_uploadprogress_set_defaults;
	p->cleanup     = mod_uploadprogress_free;
	p->handle_trigger = mod_uploadprogress_trigger;
	p->handle_response_header = mod_uploadprogress_response_header;
	
	p->data        = NULL;

	return 0;
}
