/**
 * @file callback.c
 *
 * @brief Callback functions.
 *
 * Copyright 2007-2008, Freie Universitaet Berlin. All rights reserved.
 * Please refer to the COPYING file for detailed licensing information.
 *
 * @author Georg Wittenburg
 *
 * $Id: callback.c 2685 2008-06-25 16:38:56Z wittenbu $
 */

#include "../types.h"
#include "../vars.h"
#include "../interface.h"
#include "../interface_internal.h"
#include "debug.h"
#include "list.h"
#include "callback.h"

/**
 * @brief Maximal supported number of simultaneous callbacks.
 */
#define MAX_CALLBACKS	1024

/**
 * @brief Check whether a callback expires later than another one.
 *
 * @param item Pointer to callback to check.
 * @param cmp_item Pointer to reference callback
 * @return <code>true</code> if the callback to check expires later than the
 * reference callback, <code>false</code> otherwise.
 */
static bool is_later(const void* item, const void* cmp_item) {
	callback_t* callback = (callback_t*) item;
	callback_t* cmp_callback = (callback_t*) cmp_item;
	return (callback->time > cmp_callback->time);
}

/**
 * @brief Add a new callback to be executed later on.
 *
 * @param function Pointer to the callback function.
 * @param parameter Pointer to the parameter passed to the callback function.
 * @param delay Delay before the callback expires in seconds.
 * @return Unique ID of the callback.
 */
uint16_t callback_add(callback_function_t function, callback_parameter_t parameter, C_time_t delay) {
	callback_t* callback = NULL;

	if(function == NULL) {
		C_DEBUG(C_DEBUG_LEVEL_FATAL, "Trying to register callback with NULL function...\n");
		abort();
	}
	if(delay < 0) {
		C_DEBUG(C_DEBUG_LEVEL_FATAL, "Trying to register callback to function at %p with negative delay (%f)...\n", delay);
		abort();
	}
	if(list_size(C_callbacks) >= MAX_CALLBACKS) {
		C_DEBUG(C_DEBUG_LEVEL_FATAL, "Trying to register callback to function at %p, but %d callbacks are already running...\n", function, MAX_CALLBACKS);
		abort();
	}

	if(delay == 0) {
		C_DEBUG(C_DEBUG_LEVEL_DEBUG, "Registering callback #%d to function at %p and parameter at %p without delay...\n", C_next_callback_id, function, parameter);
	} else {
		C_DEBUG(C_DEBUG_LEVEL_VERBOSE, "Registering callback #%d to function at %p and parameter at %p with a delay of %.2f seconds...\n", C_next_callback_id, function, parameter, delay);
	}
	MALLOC(callback, callback_t);
	callback->id = C_next_callback_id++;
	callback->function = function;
	callback->parameter = parameter;
	callback->time = C_time() + delay;
	list_insert_where(C_callbacks, callback, is_later);
	C_activate_callback(((callback_t*) list_head(C_callbacks))->time);
	C_registered_callbacks++;
	return callback->id;
}

/**
 * @brief Check whether two callback have the same unique ID.
 *
 * @param item Callback to check.
 * @param cmp_item Reference callback.
 * @return <code>true</code> if the callbacks have the same unique ID,
 * <code>false</code> otherwise.
 */
static bool cmp_callback_id(const void* item, const void* cmp_item) {
	callback_t* callback = (callback_t*) item;
	callback_t* cmp_callback = (callback_t*) cmp_item;
	return (callback->id == cmp_callback->id);
}

/**
 * @brief Cancel callback, thus preventing it from ever being executed.
 *
 * @param id Unique ID of the callback that is to be cancelled.
 */
void callback_cancel(uint16_t id) {
	callback_t* callback = NULL;
	callback_t cmp_callback;
	bool was_active_callback = false;

	cmp_callback.id = id;
	callback = (callback_t*) list_find(C_callbacks, &cmp_callback, cmp_callback_id);
	if(callback == NULL) {
		C_DEBUG(C_DEBUG_LEVEL_FATAL, "Trying to cancel callback #%d, but no callback with that ID is scheduled for execution.\n", id);
		abort();
	}

	C_DEBUG(C_DEBUG_LEVEL_VERBOSE, "Cancelling callback #%d to function at %p and parameter at %p...\n", callback->id, callback->function, callback->parameter);
	was_active_callback = (callback == (callback_t*) list_head(C_callbacks));
	list_remove(C_callbacks, callback);
	FREE(callback);
	C_executed_callbacks++;

	if(was_active_callback && !list_empty(C_callbacks))
		C_activate_callback(((callback_t*) list_head(C_callbacks))->time);
}

/**
 * @brief This function is called when a callback has expired.
 */
void callback_expire() {
	C_time_t time;
	callback_t* callback = NULL;

	if(list_empty(C_callbacks)) {
		C_DEBUG(C_DEBUG_LEVEL_VERBOSE, "Ignoring call to cancelled callback...\n");
		return;
	}

	time = C_time();
	callback = (callback_t*) list_head(C_callbacks);
	if((callback->time - time) > 0.1) {		// Allow callbacks to occur slightly to early due to rounding errors.
		C_DEBUG(C_DEBUG_LEVEL_FATAL, "Trying to call expired callback, but next callback is only due in %.2f seconds.\n", callback->time - time);
		abort();
	}

	C_DEBUG(C_DEBUG_LEVEL_VERBOSE, "Calling expired callback function at %p with parameter at %p (%f seconds overdue)...\n", callback->function, callback->parameter, time - callback->time);
	if((callback->time - time) <= -1) {
		C_DEBUG(C_DEBUG_LEVEL_WARN, "Callback is overdue by %f seconds.\n", callback->time - time);
	}
	list_remove(C_callbacks, callback);
	callback->function(callback->parameter);
	FREE(callback);
	C_executed_callbacks++;

	// Schedule next callback.
	if(!list_empty(C_callbacks))
		C_activate_callback(((callback_t*) list_head(C_callbacks))->time);
}
