/**
 * @file slp-light.c
 *
 * @brief Service Location Protocol (SLP) - minimal implemenation.
 *
 * This is a very basic implementation of the Service Location Protocol (SLP).
 * It covers searching for services by expanding-ring broadcasts. It does not
 * include service naming conventions or Directory Agents (DA).
 *
 * Copyright 2007-2008, Freie Universitaet Berlin. All rights reserved.
 * Please refer to the COPYING file for detailed licensing information.
 *
 * @author Georg Wittenburg
 *
 * $Id: slp-light.c 2685 2008-06-25 16:38:56Z wittenbu $
 */

#include <math.h>
#include <stdlib.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 "../rudp-light/rudp-light.h"
#include "../service/service.h"
#include "slp-light.h"
#include "slp-light_types.h"
#include "slp-light_vars.h"

#define SLP_PACKET_TYPE_QUERY	0x0	///< SLP QUERY packet type.
#define SLP_PACKET_TYPE_REPLY	0x1	///< SLP REPLY packet type.

/**
 * @brief Maximal supported number of active queries.
 */
#define SLP_MAX_ACTIVE_QUERIES		C_MAX_SERVICES

/**
 * @brief Number of retries of SLP queries.
 *
 * With each retry of the query the TTL of the query packet is increased, thus
 * implementing an expanding ring search.
 */
#define SLP_QUERY_RETRIES		3

/**
 * @brief Timeout for SLP queries in seconds.
 */
#define SLP_QUERY_TIMEOUT		(C_EXPECTED_MULTI_HOP_RTT + C_ROUTE_ESTABLISHMENT_TIME)



/*
 * Interface to main module.
 */

/**
 * @brief Initialize the SLP module.
 */
void SLP_init() {
	list_alloc(&C_SLP_active_queries);
	C_SLP_total_queries = 0;
	C_SLP_successful_queries = 0;
	C_SLP_failed_queries = 0;
}

/**
 * @brief Start the SLP module.
 */
void SLP_start() {
}

/**
 * @brief Stop the SLP module.
 */
void SLP_stop() {
}

/**
 * @brief Finalize the SLP module.
 */
void SLP_finalize() {
	if(C_check_consistency_on_shutdown) {
		if(C_SLP_total_queries != (C_SLP_successful_queries + C_SLP_failed_queries)) {
			C_DEBUG(C_DEBUG_LEVEL_FATAL, "SLP: Query counters don't add up (%d != (%d + %d)).\n", C_SLP_total_queries, C_SLP_successful_queries, C_SLP_failed_queries);
			abort();
		}
	}
	list_free(&C_SLP_active_queries);
}



/*
 * Packet handling functions.
 */

/**
 * @brief Send a SLP QUERY packet.
 *
 * @param service_id ID of the service to query for.
 * @param retry Retry of this query.
 */
static void send_SLP_QUERY(const SERVICE_id_t* service_id, uint8_t retry) {
	packet_header_t header;
	SLP_QUERY_packet_t query;

	C_DEBUG(C_DEBUG_LEVEL_DEBUG, "SLP: Broadcasting QUERY for service \"%s\"...\n", service_id);
	init_header(&header, C_BROADCAST_ADDR, APP_TYPE_SLP, SLP_PACKET_TYPE_QUERY);
	header.ttl = (ttl_t) pow(2, retry);
	if(header.ttl > C_DEFAULT_TTL)
		header.ttl = C_DEFAULT_TTL;
	header.payload_length = sizeof(SLP_QUERY_packet_t);
	SERVICE_copy_service_id(query.service_id, service_id);
	C_send(&header, &query);
}

/**
 * @brief Send a SLP REPLY packet.
 *
 * @param to Destination address of the reply.
 * @param service_id ID of the service that has been queried for.
 */
static void send_SLP_REPLY(C_addr_t to, const SERVICE_id_t* service_id) {
	SLP_REPLY_packet_t reply;

	C_DEBUG(C_DEBUG_LEVEL_DEBUG, "SLP: Sending REPLY to query for service \"%s\" to node %s...\n", service_id, addr_to_str(to));
	SERVICE_copy_service_id(reply.service_id, service_id);
	RUDP_sendto(to, APP_TYPE_SLP, SLP_PACKET_TYPE_REPLY, &reply, sizeof(SLP_REPLY_packet_t), NULL, NULL);
}

/**
 * @brief Handle an incoming SLP QUERY.
 *
 * Send a reply to this query if the service is hosted on this node, forward
 * the query otherwise.
 *
 * @param header Pointer to header of incoming packet.
 * @param query Pointer to SLP query.
 */
static void handle_SLP_QUERY(const packet_header_t* header, const SLP_QUERY_packet_t* query) {
	if(header->from == C_LOCAL_ADDR) {
		C_DEBUG(C_DEBUG_LEVEL_DEBUG, "SLP: Dropping own QUERY...\n");
		return;
	}
	C_DEBUG(C_DEBUG_LEVEL_DEBUG, "SLP: Received QUERY from node %s.\n", addr_to_str(header->from));
	if(SERVICE_get_active_service(query->service_id) != NULL) {
		C_DEBUG(C_DEBUG_LEVEL_DEBUG, "SLP: Service \"%s\" found.\n", query->service_id);
		send_SLP_REPLY(header->from, query->service_id);
	} else {
		// NOTE: One could also check the local service cache here.
		// However, this would get in the way of directory agents.
		C_DEBUG(C_DEBUG_LEVEL_DEBUG, "SLP: Forwarding QUERY...\n");
		forward(header, query);
	}
}

/**
 * @brief Check whether the service ID of two queries match.
 *
 * @param item Query to check.
 * @param cmp_item Reference query.
 * @return <code>true</code> if the service ID of the two queries matches,
 * <code>false</code> otherwise.
 */
static bool matches_query_service_id(const void* item, const void* cmp_item) {
	SLP_query_t* query = (SLP_query_t*) item;
	SLP_query_t* cmp_query = (SLP_query_t*) cmp_item;
	return SERVICE_same_service_id(query->service_id, cmp_query->service_id);
}

/**
 * @brief Check whether the server of two service instances match.
 *
 * @param item Service instance to check.
 * @param cmp_item Reference service instance.
 * @return <code>true</code> if the server of the two service instances
 * matches, <code>false</code> otherwise.
 */
static bool matches_service_instance_server(const void* item, const void* cmp_item) {
	SLP_service_instance_t* service_instance = (SLP_service_instance_t*) item;
	SLP_service_instance_t* cmp_service_instance = (SLP_service_instance_t*) cmp_item;
	return (service_instance->server == cmp_service_instance->server);
}

/**
 * @brief Handle an incoming SLP REPLY.
 *
 * Add or update server in the service instances list in the query results.
 *
 * @param header Pointer to the header of the incoming reply.
 * @param reply Pointer to the SLP reply.
 */
static void handle_SLP_REPLY(const packet_header_t* header, const SLP_REPLY_packet_t* reply) {
	SLP_query_t cmp_query;
	SLP_query_t* query = NULL;
	SLP_service_instance_t cmp_service_instance;
	SLP_service_instance_t* service_instance = NULL;

	C_DEBUG(C_DEBUG_LEVEL_DEBUG, "SLP: Received REPLY to query for service \"%s\" from node %s.\n", reply->service_id, addr_to_str(header->from));

	SERVICE_copy_service_id(cmp_query.service_id, reply->service_id);
	query = (SLP_query_t*) list_find(C_SLP_active_queries, &cmp_query, matches_query_service_id);
	if(query == NULL) {
		C_DEBUG(C_DEBUG_LEVEL_WARN, "SLP: No matching query for service \"%s\" in active query list.\n", reply->service_id);
		return;
	}

	cmp_service_instance.server = header->from;
	service_instance = (SLP_service_instance_t*) list_find(query->result->service_instances, &cmp_service_instance, matches_service_instance_server);
	if(service_instance == NULL) {
		SLP_service_instance_t* service_instance;
		MALLOC(service_instance, SLP_service_instance_t);
		service_instance->server = header->from;
		service_instance->hops = header->hops;
		service_instance->delay = C_time() - query->start_time;
		list_add(query->result->service_instances, service_instance);
		if(list_size(query->result->service_instances) >= query->min_service_instances) {
			C_DEBUG(C_DEBUG_LEVEL_DEBUG, "SLP: Received enough replies to query for service \"%s\", calling callback function...\n", query->service_id);
			query->result->error_code = C_ERROR_OK;
			query->function(query->result);
			query->result = NULL;
			list_remove(C_SLP_active_queries, query);
			callback_cancel(query->callback_id);
			FREE(query);
			C_SLP_successful_queries++;
		}
	} else {
		if(service_instance->hops > header->hops)
			service_instance->hops = header->hops;
	}
}

/**
 * @brief SLP packet classifier.
 *
 * @param header Pointer to header of an incoming packet.
 * @param payload Pointer to payload of an incoming packet.
 */
void SLP_recv(const packet_header_t* header, const void* payload) {
	if(header->app != APP_TYPE_SLP) {
		C_DEBUG(C_DEBUG_LEVEL_FATAL, "SLP: Received non-SLP packet.\n");
		abort();
	}

	switch(header->type) {
	case SLP_PACKET_TYPE_QUERY:
		if(header->payload_length != sizeof(SLP_QUERY_packet_t)) {
			C_DEBUG(C_DEBUG_LEVEL_FATAL, "SLP: Received QUERY packet with wrong payload length (%d != %d).\n", header->payload_length, sizeof(SLP_QUERY_packet_t));
			abort();
		}
		handle_SLP_QUERY(header, (SLP_QUERY_packet_t*) payload);
		break;
	case SLP_PACKET_TYPE_REPLY:
		if(header->payload_length != sizeof(SLP_REPLY_packet_t)) {
			C_DEBUG(C_DEBUG_LEVEL_FATAL, "SLP: Received REPLY packet with wrong payload length (%d != %d).\n", header->payload_length, sizeof(SLP_REPLY_packet_t));
			abort();
		}
		handle_SLP_REPLY(header, (SLP_REPLY_packet_t*) payload);
		break;
	default:
		C_DEBUG(C_DEBUG_LEVEL_FATAL, "SLP: Received packet of unknown type 0x%X from node %s.\n", header->type, addr_to_str(header->from));
		abort();
	}
}



/*
 * Interface provided by this module.
 */

/**
 * @brief Timeout handler for locating a service.
 *
 * @param parameter Query for which the timeout has expired.
 */
static void SLP_locate_service_timeout(callback_parameter_t parameter) {
	SLP_query_t* query = (SLP_query_t*) parameter;

	// Call callback function if any service instances have been found.
	if(!list_empty(query->result->service_instances)) {
		C_DEBUG(C_DEBUG_LEVEL_DEBUG, "SLP: Timeout for QUERY for service \"%s\" has expired, calling callback function...\n", query->service_id);
		query->result->error_code = C_ERROR_OK;
		query->function(query->result);
		list_remove(C_SLP_active_queries, query);
		FREE(query);

		C_SLP_successful_queries++;

	// Retry if no service instances have been found.
	} else if(query->retries > 0) {
		uint8_t retry = SLP_QUERY_RETRIES - (--query->retries);
		C_DEBUG(C_DEBUG_LEVEL_DEBUG, "SLP: Timeout for QUERY for service \"%s\" has expired (retry #%d).\n", query->service_id, retry);
		query->start_time = C_time();	// Update start time for the next retry.
		send_SLP_QUERY(query->service_id, retry);
		query->callback_id = callback_add(SLP_locate_service_timeout, query, SLP_QUERY_TIMEOUT);

	// Abort and call callback if no retries are left.
	} else {
		C_DEBUG(C_DEBUG_LEVEL_DEBUG, "SLP: Timeout for QUERY for service \"%s\" has expired. No retries left, service was not found.\n", query->service_id);
		query->result->error_code = C_ERROR_TIMEOUT;
		query->function(query->result);
		list_remove(C_SLP_active_queries, query);
		FREE(query);

		C_SLP_failed_queries++;
	}
}

/**
 * @brief Locate a service.
 *
 * @param service_id Service ID of the service to be located.
 * @param min_service_instances Number of service instances found before the query is successful.
 * @param function Callback function.
 * @param result Pointer to query results.
 */
void SLP_locate_service(const SERVICE_id_t* service_id, unsigned int min_service_instances, callback_function_t function, SLP_query_result_t* result) {
	SLP_query_t* query = NULL;

	if((strlen(service_id) + 1) > SERVICE_ID_LENGTH) {
		C_DEBUG(C_DEBUG_LEVEL_ERROR, "SLP: Service ID \"%s\" is too long (%d + 1 > %d).\n", service_id, strlen(service_id), SERVICE_ID_LENGTH);
		result->error_code = C_ERROR_PARAMETER;
		function(result);
		return;
	} else if((function == NULL) || (result == NULL)) {
		C_DEBUG(C_DEBUG_LEVEL_ERROR, "SLP: Callback function or parameter not given (%p, %p).\n", function, result);
		result->error_code = C_ERROR_PARAMETER;
		function(result);
		return;
	} else if(!list_empty(result->service_instances)) {
		C_DEBUG(C_DEBUG_LEVEL_ERROR, "SLP: Query result list for service instances is not empty.\n");
		result->error_code = C_ERROR_PARAMETER;
		function(result);
		return;
	}

	C_SLP_total_queries++;

	if(list_size(C_SLP_active_queries) >= SLP_MAX_ACTIVE_QUERIES) {
		C_DEBUG(C_DEBUG_LEVEL_ERROR, "SLP: No empty slots for query processing.\n");
		result->error_code = C_ERROR_BUSY;
		function(result);
		C_SLP_failed_queries++;
		return;
	} else if(SERVICE_get_active_service(service_id) != NULL) {
		SLP_service_instance_t* service_instance = NULL;
		C_DEBUG(C_DEBUG_LEVEL_INFO, "SLP: Service \"%s\" is hosted on the node itself.\n", service_id);
		MALLOC(service_instance, SLP_service_instance_t);
		service_instance->server = C_LOCAL_ADDR;
		service_instance->hops = 0;
		service_instance->delay = 0.0;
		list_add(result->service_instances, service_instance);
		result->error_code = C_ERROR_OK;
		function(result);
		C_SLP_successful_queries++;
		return;
	}

	MALLOC(query, SLP_query_t);
	SERVICE_copy_service_id(query->service_id, service_id);
	query->min_service_instances = min_service_instances;
	query->retries = SLP_QUERY_RETRIES;
	query->start_time = C_time();
	query->function = function;
	query->result = result;
	SERVICE_copy_service_id(query->result->service_id, service_id);
	query->result->error_code = C_ERROR_NOT_SET;

	if(list_find(C_SLP_active_queries, query, matches_query_service_id) != NULL) {
		C_DEBUG(C_DEBUG_LEVEL_DEBUG, "SLP: Query for service \"%s\" already in progress.\n", service_id);
		result->error_code = C_ERROR_IN_PROGRESS;
		function(result);
		FREE(query);
		C_SLP_total_queries--;
		return;
	}

	C_DEBUG(C_DEBUG_LEVEL_INFO, "SLP: Locating service \"%s\"...\n", service_id);
	list_add(C_SLP_active_queries, query);
	send_SLP_QUERY(service_id, 0);
	query->callback_id = callback_add(SLP_locate_service_timeout, query, SLP_QUERY_TIMEOUT);
}



/*
 * Interface provided by this module for direct commands.
 */

/**
 * @brief Callback of command function to locate a service.
 *
 * @param parameter Result of the query.
 */
static void SLP_command_locate_service_callback(callback_parameter_t parameter) {
	SLP_query_result_t* result = (SLP_query_result_t*) parameter;

	SLP_service_instance_t* service_instance;
	switch(result->error_code) {
	case C_ERROR_OK:
		service_instance = (SLP_service_instance_t*) list_head(result->service_instances);
		C_DEBUG(C_DEBUG_LEVEL_NOTE, "SLP: %d instance(s) of service \"%s\" located on node(s) %s (%d hops)", list_size(result->service_instances), result->service_id, addr_to_str(service_instance->server), service_instance->hops);
		list_remove(result->service_instances, service_instance);
		while(!list_empty(result->service_instances)) {
			service_instance = (SLP_service_instance_t*) list_head(result->service_instances);
			list_remove(result->service_instances, service_instance);
			if(list_size(result->service_instances) >= 1) {
				C_DEBUG(C_DEBUG_LEVEL_NONE, ", ");
			} else {
				C_DEBUG(C_DEBUG_LEVEL_NONE, " and ");
			}
			C_DEBUG(C_DEBUG_LEVEL_NONE, "%s (%d hops)", addr_to_str(service_instance->server), service_instance->hops);
		}
		C_DEBUG(C_DEBUG_LEVEL_NONE, ".\n");
		break;
	case C_ERROR_PARAMETER:
		C_DEBUG(C_DEBUG_LEVEL_ERROR, "SLP: Wrong parameter value.\n");
		break;
	case C_ERROR_TIMEOUT:
		C_DEBUG(C_DEBUG_LEVEL_WARN, "SLP: Service \"%s\" not found.\n", result->service_id);
		break;
	case C_ERROR_BUSY:
		C_DEBUG(C_DEBUG_LEVEL_WARN, "SLP: Too many service queries in progress.\n");
		break;
	case C_ERROR_IN_PROGRESS:
		C_DEBUG(C_DEBUG_LEVEL_WARN, "SLP: Query for service \"%s\" already in progress.\n", result->service_id);
		break;
	default:
		C_DEBUG(C_DEBUG_LEVEL_FATAL, "SLP: Callback with unknown error code 0x%X.\n", result->error_code);
		abort();
	}

	list_free(&result->service_instances);
	FREE(result);
}

/**
 * @brief SLP command function.
 *
 * @param argc Number of command parameters.
 * @param argv Array of strings used as command parameters.
 * @return <code>true</code> if the command was executed successfully,
 * <code>false</code> otherwise.
 */
bool SLP_command(int argc, const char*const* argv) {
	if(argc == 4) {
		if(strcmp(argv[1], "SLP_locate_service") == 0) {
			SLP_query_result_t* result;
			MALLOC(result, SLP_query_result_t);
			list_alloc(&result->service_instances);
			SLP_locate_service(argv[2], atoi(argv[3]), SLP_command_locate_service_callback, result);
			return true;
		}
	}
	return false;
}
