/**
 * @file icmp-light.c
 *
 * @brief Internet Control Message Protocol (ICMP) - minimal implemenation.
 *
 * A very simple implementation of basic functionality of the Internet Control
 * Message Protocol (ICMP). Supports ECHO and REPLY packets as well as NOP
 * packets that do nothing but causing traffic.
 *
 * Copyright 2007-2008, Freie Universitaet Berlin. All rights reserved.
 * Please refer to the COPYING file for detailed licensing information.
 *
 * @author Georg Wittenburg
 *
 * $Id: icmp-light.c 2685 2008-06-25 16:38:56Z wittenbu $
 */

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

#define ICMP_PACKET_TYPE_NOP		0x0	///< ICMP NOP packet type.
#define ICMP_PACKET_TYPE_ECHO_REQUEST	0x1	///< ICMP ECHO REQUEST packet type.
#define ICMP_PACKET_TYPE_ECHO_REPLY	0x2	///< ICMP ECHO REPLY packet type.



/*
 * Interface to main module.
 */

/**
 * @brief Initialize the ICMP module.
 */
void ICMP_init() {
	list_alloc(&C_ICMP_active_echo_requests);
	C_ICMP_next_echo_id = 0;
	C_ICMP_transmitted_nops = 0;
	C_ICMP_received_nops = 0;
}

/**
 * @brief Start the ICMP module.
 */
void ICMP_start() {
}

/**
 * @brief Stop the ICMP module.
 */
void ICMP_stop() {
}

/**
 * @brief Finalize the ICMP module.
 */
void ICMP_finalize() {
	list_free(&C_ICMP_active_echo_requests);
}



/*
 * Packet handling functions.
 */

/**
 * @brief Send an ICMP NOP packet.
 *
 * @param to Address of the destination node.
 * @param ttl Initial time-to-life of the packet.
 */
static void send_ICMP_NOP(C_addr_t to, ttl_t ttl) {
	packet_header_t header;

	C_DEBUG(C_DEBUG_LEVEL_DEBUG, "ICMP: Sending NOP to node %s with a TTL of %d...\n", addr_to_str(to), ttl);
	init_header(&header, to, APP_TYPE_ICMP, ICMP_PACKET_TYPE_NOP);
	header.ttl = ttl;
	C_send(&header, NULL);
}

/**
 * @brief Send an ICMP ECHO REQUEST packet.
 *
 * @param to Address of the destination node.
 * @param ttl Initial time-to-live of the packet.
 * @param id Unique ID of the echo request/reply.
 */
static void send_ICMP_ECHO_REQUEST(C_addr_t to, ttl_t ttl, uint16_t id) {
	packet_header_t header;
	ICMP_ECHO_packet_t echo;

	C_DEBUG(C_DEBUG_LEVEL_DEBUG, "ICMP: Sending ECHO REQUEST to node %s with a TTL of %d...\n", addr_to_str(to), ttl);
	init_header(&header, to, APP_TYPE_ICMP, ICMP_PACKET_TYPE_ECHO_REQUEST);
	header.ttl = ttl;
	header.payload_length = sizeof(ICMP_ECHO_packet_t);
	echo.id = id;
	C_send(&header, &echo);
}

/**
 * @brief Send an ICMP ECHO REPLY packet.
 *
 * @param to Address of the destination node.
 * @param id Unique ID of the echo request/reply.
 */
static void send_ICMP_ECHO_REPLY(C_addr_t to, uint16_t id) {
	ICMP_ECHO_packet_t echo;

	C_DEBUG(C_DEBUG_LEVEL_DEBUG, "ICMP: Sending ECHO REPLY to node %s...\n", addr_to_str(to));
	echo.id = id;
	C_sendto(to, APP_TYPE_ICMP, ICMP_PACKET_TYPE_ECHO_REPLY, &echo, sizeof(ICMP_ECHO_packet_t));
}

/**
 * @brief Handle an incoming ICMP NOP packet.
 *
 * @param header Pointer to header of the incoming packet.
 */
static void handle_ICMP_PACKET_TYPE_NOP(const packet_header_t* header) {
	C_DEBUG(C_DEBUG_LEVEL_DEBUG, "ICMP: Received NOP from node %s.\n", addr_to_str(header->from));
	if((header->to == C_BROADCAST_ADDR) && (header->ttl > 1)) {
		C_DEBUG(C_DEBUG_LEVEL_DEBUG, "ICMP: Forwarding NOP...\n");
		forward(header, NULL);
	}

	C_ICMP_received_nops++;
}

/**
 * @brief Handle an incoming ICMP ECHO REQUEST packet.
 *
 * @param header Pointer to header of the incoming packet.
 * @param echo Pointer to the ICMP ECHO header.
 */
static void handle_ICMP_ECHO_REQUEST(const packet_header_t* header, const ICMP_ECHO_packet_t* echo) {
	if(header->from == C_LOCAL_ADDR) {
		C_DEBUG(C_DEBUG_LEVEL_DEBUG, "ICMP: Dropping own ECHO REQUEST...\n");
		return;
	}
	C_DEBUG(C_DEBUG_LEVEL_DEBUG, "ICMP: Received ECHO REQUEST from node %s via node %s.\n", addr_to_str(header->from), addr_to_str(header->last_hop));
	send_ICMP_ECHO_REPLY(header->from, echo->id);
	if((header->to == C_BROADCAST_ADDR) && (header->ttl > 0)) {
		C_DEBUG(C_DEBUG_LEVEL_DEBUG, "ICMP: Forwarding ECHO REQUEST...\n");
		forward(header, echo);
	}
}

/**
 * @brief Check whether the ID of two echo requests matches.
 *
 * @param item Echo request to check.
 * @param cmp_item Reference echo request.
 * @return <code>true</code> if ID of the two echo requests are equal,
 * <code>false</code> otherwise.
 */
static bool matches_echo_request_id(const void* item, const void* cmp_item) {
	ICMP_echo_request_t* echo_request = (ICMP_echo_request_t*) item;
	ICMP_echo_request_t* cmp_echo_request = (ICMP_echo_request_t*) cmp_item;
	return (echo_request->id == cmp_echo_request->id);
}

/**
 * @brief Handle an incoming ICMP ECHO REPLY packet.
 *
 * @param header Pointer to header of the incoming packet.
 * @param echo Pointer to the ICMP ECHO header.
 */
static void handle_ICMP_ECHO_REPLY(const packet_header_t* header, const ICMP_ECHO_packet_t* echo) {
	ICMP_echo_request_t* echo_request = NULL;
	ICMP_echo_request_t cmp_echo_request;
	cmp_echo_request.id = echo->id;

	echo_request = (ICMP_echo_request_t*) list_find(C_ICMP_active_echo_requests, &cmp_echo_request, matches_echo_request_id);
	if(echo_request == NULL) {
		C_DEBUG(C_DEBUG_LEVEL_WARN, "ICMP: Received ECHO REPLY from node %s, but no request with ID %d is pending.\n", addr_to_str(header->from), echo->id);
		return;
	}

	C_DEBUG(C_DEBUG_LEVEL_DEBUG, "ICMP: Received ECHO REPLY from node %s.\n", addr_to_str(header->from));
	echo_request->replies++;
	if(echo_request->echo_request_result->nodes != NULL) {
		node_t* node = NULL;
		MALLOC(node, node_t);
		init_node(node, header->from);
		node->hops = header->hops;
		node->last_hop = header->last_hop;
		node->packets = 1;
		list_add(echo_request->echo_request_result->nodes, node);
	}

	if(echo_request->address != C_BROADCAST_ADDR) {
		C_DEBUG(C_DEBUG_LEVEL_DEBUG, "ICMP: Echo request was unicast, calling callback function and cancelling timeout...\n");
		echo_request->echo_request_result->error_code = C_ERROR_OK;
		echo_request->function(echo_request->echo_request_result);
		callback_cancel(echo_request->callback_id);
		list_remove(C_ICMP_active_echo_requests, echo_request);
		FREE(echo_request);
	}
}

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

	switch(header->type) {
	case ICMP_PACKET_TYPE_NOP:
		handle_ICMP_PACKET_TYPE_NOP(header);
		break;
	case ICMP_PACKET_TYPE_ECHO_REQUEST:
		if(header->payload_length != sizeof(ICMP_ECHO_packet_t)) {
			C_DEBUG(C_DEBUG_LEVEL_FATAL, "ICMP: Received ECHO REQUEST packet with wrong payload length (%d != %d).\n", header->payload_length, sizeof(ICMP_ECHO_packet_t));
			abort();
		}
		handle_ICMP_ECHO_REQUEST(header, (ICMP_ECHO_packet_t*) payload);
		break;
	case ICMP_PACKET_TYPE_ECHO_REPLY:
		if(header->payload_length != sizeof(ICMP_ECHO_packet_t)) {
			C_DEBUG(C_DEBUG_LEVEL_FATAL, "ICMP: Received ECHO REPLY packet with wrong payload length (%d != %d).\n", header->payload_length, sizeof(ICMP_ECHO_packet_t));
			abort();
		}
		handle_ICMP_ECHO_REPLY(header, (ICMP_ECHO_packet_t*) payload);
		break;
	default:
		C_DEBUG(C_DEBUG_LEVEL_FATAL, "ICMP: 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 Send an ICMP NOP packet.
 *
 * @param to Address of the destination node.
 * @param ttl Initial time-to-life of the packet.
 */
void ICMP_send_nop(C_addr_t to, ttl_t ttl) {
	C_DEBUG(C_DEBUG_LEVEL_INFO, "ICMP: Sending NOP to node %s with a TTL of %d...\n", addr_to_str(to), ttl);
	send_ICMP_NOP(to, ttl);

	C_ICMP_transmitted_nops++;
}

/**
 * @brief Timeout callback for ICMP ECHO REQUESTs.
 *
 * @param parameter Echo request data structure.
 */
void ICMP_send_echo_request_timeout(callback_parameter_t parameter) {
	ICMP_echo_request_t* echo_request = (ICMP_echo_request_t*) parameter;

	if(echo_request->replies > 0) {
		C_DEBUG(C_DEBUG_LEVEL_DEBUG, "ICMP: Timeout for ECHO REQUEST has expired, calling callback function...\n");
		echo_request->echo_request_result->error_code = C_ERROR_OK;
	} else {
		C_DEBUG(C_DEBUG_LEVEL_WARN, "ICMP: Timeout for ECHO REQUEST has expired without any replies.\n");
		echo_request->echo_request_result->error_code = C_ERROR_TIMEOUT;
	}
	echo_request->function(echo_request->echo_request_result);
	list_remove(C_ICMP_active_echo_requests, echo_request);
	FREE(echo_request);
}

/**
 * @brief Send an ICMP ECHO REQUEST packet.
 *
 * @param to Address of the destination node.
 * @param ttl Initial time-to-life of the packet.
 * @param function Pointer to function to call upon completion.
 * @param echo_request_result Data structure for the results of the echo request.
 */
void ICMP_send_echo_request(C_addr_t to, ttl_t ttl, callback_function_t function, ICMP_echo_request_result_t* echo_request_result) {
	ICMP_echo_request_t* echo_request = NULL;

	if((function == NULL) || (echo_request_result == NULL)) {
		C_DEBUG(C_DEBUG_LEVEL_ERROR, "ICMP: Callback function or parameter not given (%p, %p).\n", function, echo_request_result);
		return;
	}

	C_DEBUG(C_DEBUG_LEVEL_INFO, "ICMP: Sending echo request to node %s with a TTL of %d...\n", addr_to_str(to), ttl);
	MALLOC(echo_request, ICMP_echo_request_t);
	echo_request->id = C_ICMP_next_echo_id++;
	echo_request->address = to;
	echo_request->replies = 0;
	echo_request->function = function;
	echo_request->echo_request_result = echo_request_result;
	list_add(C_ICMP_active_echo_requests, echo_request);

	send_ICMP_ECHO_REQUEST(to, ttl, echo_request->id);
	echo_request->callback_id = callback_add(ICMP_send_echo_request_timeout, echo_request, (4.0 * ttl * C_EXPECTED_SINGLE_HOP_RTT));
}



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

/**
 * @brief Print a node structure.
 *
 * @param item Node to print.
 */
static void print_node_addr_and_hops(const void* item) {
	node_t* node = (node_t*) item;
	C_DEBUG(C_DEBUG_LEVEL_NONE, "%s @ %d hops", addr_to_str(node->address), node->hops);
}

/**
 * @brief Callback for ECHO REQUESTs of the ICMP command function.
 *
 * @param parameter The results of the echo request.
 */
static void ICMP_command_send_echo_request_callback(callback_parameter_t parameter) {
	ICMP_echo_request_result_t* echo_request_result = (ICMP_echo_request_result_t*) parameter;

	switch(echo_request_result->error_code) {
	case C_ERROR_OK:
		C_DEBUG(C_DEBUG_LEVEL_NOTE, "ICMP: %d node(s) replied to echo request: ", list_size(echo_request_result->nodes));
		list_print(echo_request_result->nodes, print_node_addr_and_hops);
		C_DEBUG(C_DEBUG_LEVEL_NONE, ".\n");
		break;
	case C_ERROR_TIMEOUT:
		C_DEBUG(C_DEBUG_LEVEL_WARN, "ICMP: No replies to echo request.\n");
		break;
	default:
		C_DEBUG(C_DEBUG_LEVEL_FATAL, "ICMP: Callback with unknown error code 0x%X.\n", echo_request_result->error_code);
		abort();
	}

	list_free(&echo_request_result->nodes);
	FREE(echo_request_result);
}

/**
 * @brief ICMP 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 ICMP_command(int argc, const char*const* argv) {
	if(argc == 2) {
		if(strcmp(argv[1], "ICMP_send_nop") == 0) {
			ICMP_send_nop(C_BROADCAST_ADDR, C_DEFAULT_TTL);
			return true;
		} else if(strcmp(argv[1], "ICMP_send_echo_request") == 0) {
			ICMP_echo_request_result_t* echo_request_result;
			MALLOC(echo_request_result, ICMP_echo_request_result_t);
			list_alloc(&echo_request_result->nodes);
			ICMP_send_echo_request(C_BROADCAST_ADDR, C_DEFAULT_TTL, ICMP_command_send_echo_request_callback, echo_request_result);
			return true;
		} else if(strcmp(argv[1], "ICMP_send_local_echo_request") == 0) {
			ICMP_echo_request_result_t* echo_request_result;
			MALLOC(echo_request_result, ICMP_echo_request_result_t);
			list_alloc(&echo_request_result->nodes);
			ICMP_send_echo_request(C_BROADCAST_ADDR, 1, ICMP_command_send_echo_request_callback, echo_request_result);
			return true;
		}
	} else if(argc == 3) {
		if(strcmp(argv[1], "ICMP_send_nop") == 0) {
			ICMP_send_nop((C_addr_t) atoi(argv[2]), C_DEFAULT_TTL);
			return true;
		} else if(strcmp(argv[1], "ICMP_send_echo_request") == 0) {
			ICMP_echo_request_result_t* echo_request_result;
			MALLOC(echo_request_result, ICMP_echo_request_result_t);
			list_alloc(&echo_request_result->nodes);
			ICMP_send_echo_request((C_addr_t) atoi(argv[2]), C_DEFAULT_TTL, ICMP_command_send_echo_request_callback, echo_request_result);
			return true;
		} else if(strcmp(argv[1], "ICMP_send_local_echo_request") == 0) {
			ICMP_echo_request_result_t* echo_request_result;
			MALLOC(echo_request_result, ICMP_echo_request_result_t);
			list_alloc(&echo_request_result->nodes);
			ICMP_send_echo_request((C_addr_t) atoi(argv[2]), 1, ICMP_command_send_echo_request_callback, echo_request_result);
			return true;
		}
	} else if(argc == 4) {
		if(strcmp(argv[1], "ICMP_send_nop") == 0) {
			ICMP_send_nop((C_addr_t) atoi(argv[2]), (ttl_t) atoi(argv[3]));
			return true;
		} else if(strcmp(argv[1], "ICMP_send_echo_request") == 0) {
			ICMP_echo_request_result_t* echo_request_result;
			MALLOC(echo_request_result, ICMP_echo_request_result_t);
			list_alloc(&echo_request_result->nodes);
			ICMP_send_echo_request((C_addr_t) atoi(argv[2]), (ttl_t) atoi(argv[3]), ICMP_command_send_echo_request_callback, echo_request_result);
			return true;
		}
	}
	return false;
}
