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

#include <string.h>

#include "../types.h"
#include "../vars.h"
#include "../interface.h"
#include "../interface_internal.h"
#include "../util/debug.h"
#include "../util/list.h"
#include "../util/callback.h"
#include "../util/net.h"
#include "../slp-light/slp-light_types.h"
#include "service.h"
#include "service_types.h"
#include "service_vars.h"
#include "service_util.h"



/**
 * @brief Copy a service ID to another.
 *
 * @param dest_service_id Destination service ID.
 * @param src_service_id Source service ID.
 */
void copy_service_id(SERVICE_id_t* dest_service_id, const SERVICE_id_t* src_service_id) {
#ifdef ARCH_WIN32
	strncpy_s(dest_service_id, SERVICE_ID_LENGTH, src_service_id, SERVICE_ID_LENGTH);
#else
	strncpy(dest_service_id, src_service_id, SERVICE_ID_LENGTH);
#endif	// ARCH_WIN32
}

/**
 * @brief Check whether two service IDs match.
 *
 * @param service_id_1 First service ID.
 * @param service_id_2 Seconds service ID.
 * @return <code>true</code> if the two service IDs are equal,
 * <code>false</code> otherwise.
 */
bool same_service_id(const SERVICE_id_t* service_id_1, const SERVICE_id_t* service_id_2) {
	if(strncmp(service_id_1, service_id_2, SERVICE_ID_LENGTH) == 0)
		return true;
	else
		return false;
}



/**
 * @brief Relative weight of the hop metric when selecting the best server.
 */
#define SERVICE_HOP_MULTIPLIER		0.5f

/**
 * @brief Relative weight of the delay metric when selecting the best server.
 */
#define SERVICE_DELAY_MULTIPLIER	0.5f

/**
 * @brief Select the best server from several found service instances.
 *
 * @param service_instances List of service instances.
 * @return Address of the best server.
 */
C_addr_t selectBestServer(const list_t* service_instances) {
	C_addr_t server = C_BROADCAST_ADDR;
	float best_service_metric = -1;
	unsigned int i;

	if(list_empty(service_instances)) {
		C_DEBUG(C_DEBUG_LEVEL_FATAL, "SERVICE: No service instances available for selection.\n");
		abort();
	}

	for(i = 0; i < list_size(service_instances); i++) {
		SLP_service_instance_t* service_instance = (SLP_service_instance_t*) list_get(service_instances, i);
		float metric = 0;
		if((service_instance->hops > 0) && (service_instance->delay > 0)) {
			metric += SERVICE_HOP_MULTIPLIER * (((float) C_EXPECTED_NETWORK_DIAMETER) / ((float) service_instance->hops));
			metric += SERVICE_DELAY_MULTIPLIER * (((float) C_EXPECTED_MULTI_HOP_RTT) / ((float) service_instance->delay));
		}
		if(metric > best_service_metric) {
			server = service_instance->server;
			best_service_metric = metric;
		}
	}
	return server;
}



/*
 * Client-side cache for results of SLP queries.
 */

/**
 * @brief Size of cache services cache.
 */
#define SERVICE_SERVER_CACHE_SIZE	C_MAX_SERVICES

/**
 * @brief Maximal age of a cache entry.
 *
 * Note that the age of an entry is not a concept of time, but just a counter
 * that may be incremented given the appropriate circumstances.
 */
#define SERVICE_MAX_CACHE_ENTRY_AGE	16

/**
 * @brief Check whether two cache entries have the same service ID.
 *
 * @param item Cache entry to check.
 * @param cmp_item Reference cache entry.
 * @return <code>true</code> if the two cache entries have the same service ID,
 * <code>false</code> otherwise.
 */
static bool matches_entry_service_id(const void* item, const void* cmp_item) {
	SERVICE_service_cache_entry_t* entry = (SERVICE_service_cache_entry_t*) item;
	SERVICE_service_cache_entry_t* cmp_entry = (SERVICE_service_cache_entry_t*) cmp_item;
	return same_service_id(entry->service_id, cmp_entry->service_id);
}

/**
 * @brief Set the entry in the service cache to a server address.
 *
 * @param service_id Service ID of the cached service.
 * @param server Address of the server for this service.
 */
void cache_set(const SERVICE_id_t* service_id, C_addr_t server) {
	SERVICE_service_cache_entry_t cmp_entry;
	SERVICE_service_cache_entry_t* entry = NULL;

	if(list_size(C_SERVICE_service_cache) >= SERVICE_SERVER_CACHE_SIZE) {
		C_DEBUG(C_DEBUG_LEVEL_ERROR, "SERVICE: Server cache is full.\n");
		return;
	}

	C_DEBUG(C_DEBUG_LEVEL_DEBUG, "SERVICE: Setting cache entry for service \"%s\" to node %s...\n", service_id, addr_to_str(server));
	copy_service_id(cmp_entry.service_id, service_id);
	entry = (SERVICE_service_cache_entry_t*) list_find(C_SERVICE_service_cache, &cmp_entry, matches_entry_service_id);
	if(entry == NULL) {
		MALLOC(entry, SERVICE_service_cache_entry_t);
		copy_service_id(entry->service_id, service_id);
		list_add(C_SERVICE_service_cache, entry);
		entry->reachable = true;
	}
	entry->server = server;
	entry->age = 0;

	// If the cache entry was previously marked as not reachable, cancel
	// the callback that would remove it from the cache.
	if(!entry->reachable) {
		callback_cancel(entry->callback_id);
		entry->reachable = true;
	}
	entry->callback_id = 0;
}

/**
 * @brief Remove a service from the cache.
 *
 * @param service_id ID of the service that is to be removed.
 */
void cache_remove(const SERVICE_id_t* service_id) {
	SERVICE_service_cache_entry_t cmp_entry;
	SERVICE_service_cache_entry_t* entry = NULL;

	copy_service_id(cmp_entry.service_id, service_id);
	entry = (SERVICE_service_cache_entry_t*) list_find(C_SERVICE_service_cache, &cmp_entry, matches_entry_service_id);

	if((entry != NULL) && (!entry->reachable)) {
		C_DEBUG(C_DEBUG_LEVEL_ERROR, "SERVICE: Trying to remove cache entry for service \"%s\" that is not reachable...\n", service_id);
		return;
	}

	C_DEBUG(C_DEBUG_LEVEL_DEBUG, "SERVICE: Removing cache entry for service \"%s\"...\n", service_id);
	// We need to check whether the entry was already removed from the
	// list, e.g. by a DATA REQUEST overlapping with a SERVER NOT AVAILABLE.
	if(entry != NULL) {
		list_remove(C_SERVICE_service_cache, entry);
		FREE(entry);
	}
}

/**
 * @brief Lookup a service in the cache.
 *
 * @param service_id ID of the service to lookup.
 * @param server Pointer to the server address if C_ERROR_OK is returned,
 * Pointer to C_BROADCAST_ADDR otherwise.
 * @return C_ERROR_NOT_FOUND if the service in not in the cache,
 * C_ERROR_TIMEOUT if the service is currently not reachable, or C_ERROR_OK
 * if a valid cache entry has been found.
 */
uint8_t cache_lookup(const SERVICE_id_t* service_id, C_addr_t* server) {
	SERVICE_service_cache_entry_t cmp_entry;
	SERVICE_service_cache_entry_t* entry = NULL;

	C_DEBUG(C_DEBUG_LEVEL_DEBUG, "SERVICE: Looking up cache entry for service \"%s\"...\n", service_id);
	copy_service_id(cmp_entry.service_id, service_id);
	entry = (SERVICE_service_cache_entry_t*) list_find(C_SERVICE_service_cache, &cmp_entry, matches_entry_service_id);
	if(entry == NULL) {
		*server = C_BROADCAST_ADDR;
		return C_ERROR_NOT_FOUND;
	} else if(entry->reachable) {
		*server = entry->server;
		return C_ERROR_OK;
	} else {
		*server = C_BROADCAST_ADDR;
		return C_ERROR_TIMEOUT;
	}
}

/**
 * @brief Increment the age of a cache entry.
 *
 * If this causes the age of the entry to exceed a limit, the entry is removed
 * from the cache.
 *
 * @param service_id ID of the service whose cache entry is to be incremented.
 */
void cache_increment_age(const SERVICE_id_t* service_id) {
	SERVICE_service_cache_entry_t cmp_entry;
	SERVICE_service_cache_entry_t* entry = NULL;

	copy_service_id(cmp_entry.service_id, service_id);
	entry = (SERVICE_service_cache_entry_t*) list_find(C_SERVICE_service_cache, &cmp_entry, matches_entry_service_id);
	if(entry == NULL) {
		C_DEBUG(C_DEBUG_LEVEL_ERROR, "SERVICE: Trying to increment age of cache entry for service \"%s\", but entry doesn't exists...\n", service_id);
		return;
	} else if(!entry->reachable) {
		C_DEBUG(C_DEBUG_LEVEL_ERROR, "SERVICE: Trying to increment age of cache entry for service \"%s\", but service is not reachable...\n", service_id);
		return;
	}

	C_DEBUG(C_DEBUG_LEVEL_DEBUG, "SERVICE: Incrementing age of cache entry for service \"%s\" pointing to node %s...\n", service_id, addr_to_str(entry->server));
	entry->age++;
	if(entry->age > SERVICE_MAX_CACHE_ENTRY_AGE) {
		C_DEBUG(C_DEBUG_LEVEL_WARN, "SERVICE: Cache entry for service \"%s\" pointing to node %s exceeds age limit, removing...\n", service_id, addr_to_str(entry->server));
		list_remove(C_SERVICE_service_cache, entry);
		FREE(entry);
	}
}

/**
 * @brief Reset the age of a cache entry.
 *
 * @param service_id ID of service whose cache entry's age is to be reset.
 */
void cache_reset_age(const SERVICE_id_t* service_id) {
	SERVICE_service_cache_entry_t cmp_entry;
	SERVICE_service_cache_entry_t* entry = NULL;

	copy_service_id(cmp_entry.service_id, service_id);
	entry = (SERVICE_service_cache_entry_t*) list_find(C_SERVICE_service_cache, &cmp_entry, matches_entry_service_id);
	if(entry == NULL) {
		C_DEBUG(C_DEBUG_LEVEL_ERROR, "SERVICE: Trying to reset age of cache entry for service \"%s\", but not entry exists...\n", service_id);
		return;
	} else if(!entry->reachable) {
		C_DEBUG(C_DEBUG_LEVEL_ERROR, "SERVICE: Trying to reset age of cache entry for service \"%s\", but service is not reachable...\n", service_id);
		return;
	}

	C_DEBUG(C_DEBUG_LEVEL_DEBUG, "SERVICE: Resetting age of cache entry for service \"%s\"...\n", service_id);
	entry->age = 0;
}

/**
 * @brief Callback that removes entries of unreachable services from the cache
 * after a certain time.
 *
 * @param parameter Cache entry that is to be removed.
 */
void cache_tag_not_reachable_callback(callback_parameter_t parameter) {
	SERVICE_service_cache_entry_t* entry = (SERVICE_service_cache_entry_t*) parameter;
	list_remove(C_SERVICE_service_cache, entry);
	FREE(entry);
}

/**
 * @brief Tag a service as not reachable.
 *
 * This is to avoid excessive queries for unreachable services. The tag is
 * removed after a certain time and query requests are processed again.
 *
 * @param service_id ID of service whose cache entry is to be tagged as not
 * reachable.
 */
void cache_tag_not_reachable(const SERVICE_id_t* service_id) {
	SERVICE_service_cache_entry_t cmp_entry;
	SERVICE_service_cache_entry_t* entry = NULL;

	copy_service_id(cmp_entry.service_id, service_id);
	entry = (SERVICE_service_cache_entry_t*) list_find(C_SERVICE_service_cache, &cmp_entry, matches_entry_service_id);
	if((entry != NULL) && (!entry->reachable)) {
		C_DEBUG(C_DEBUG_LEVEL_ERROR, "SERVICE: Trying to tag cache entry for service \"%s\" not reachable, but entry is already tagged...\n", service_id);
		return;
	}

	C_DEBUG(C_DEBUG_LEVEL_WARN, "SERVICE: Tagging cache entry for service \"%s\" not reachable and scheduling removal in %.2fs...\n", service_id, C_TIMEOUT);
	if(entry == NULL) {
		MALLOC(entry, SERVICE_service_cache_entry_t);
		copy_service_id(entry->service_id, service_id);
		list_add(C_SERVICE_service_cache, entry);
		entry->server = C_BROADCAST_ADDR;
		entry->age = 0;
	}
	entry->reachable = false;
	entry->callback_id = callback_add(cache_tag_not_reachable_callback, entry, C_TIMEOUT);
}
