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

#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 "../placement/placement.h"
#include "service.h"
#include "service_types.h"
#include "service_vars.h"
#include "service_server.h"
#include "service_management.h"
#include "service_util.h"

/**
 * @brief Maximal number of served streams.
 */
#define SERVICE_MAX_SERVED_STREAMS	64

/*
 * Packet handling functions.
 */

/**
 * @brief Send a SERVICE DATA ITEM packet.
 *
 * Also send redirection information if the service is redirecting.
 *
 * @param to Address of destination node.
 * @param service Pointer to requested service.
 * @param offset Offset in service data.
 * @param size Requested service data size in bytes.
 * @param start_time Start time of this data request in seconds.
 */
static void send_SERVICE_SERVER_DATA_ITEM(C_addr_t to, const SERVICE_service_t* service, uint16_t offset, size_t size, C_time_t start_time) {
	SERVICE_SERVER_DATA_ITEM_packet_t data_item;

	if(size > service->data_size) {
		C_DEBUG(C_DEBUG_LEVEL_WARN, "SERVICE: Requested too much data (%d > %d), request size reduced to %d.\n", size, service->data_size, service->data_size);
		size = service->data_size;
	}

	C_DEBUG(C_DEBUG_LEVEL_DEBUG, "SERVICE: Sending %d byte(s) SERVER DATA ITEM for service \"%s\" (0x%X...) to node %s...\n", size, service->id, (uint8_t) *service->data, addr_to_str(to));
	copy_service_id(data_item.service_id, service->id);
	data_item.start_time = start_time;
	if(service->state == SERVICE_STATE_REDIRECTING) {
		if(service->redirect_addr == C_BROADCAST_ADDR) {
			C_DEBUG(C_DEBUG_LEVEL_FATAL, "SERVICE: Service is redirecting, but address is set to broadcast.\n");
			abort();
		}
		data_item.redirect_addr = service->redirect_addr;
	} else {
		data_item.redirect_addr = C_BROADCAST_ADDR;
	}

	C_sendto2(to, APP_TYPE_SERVICE, SERVICE_PACKET_TYPE_SERVER_DATA_ITEM, &data_item, sizeof(SERVICE_SERVER_DATA_ITEM_packet_t), (service->data + offset), size);
}

/**
 * @brief Send a SERVICE STREAM ITEM packet.
 *
 * Also send redirection information if the service is redirecting.
 *
 * @param to Address of destination node.
 * @param service Pointer to requested service.
 * @param offset Offset in service data.
 * @param size Requested service data size in bytes.
 */
static void send_SERVICE_SERVER_STREAM_ITEM(C_addr_t to, const SERVICE_service_t* service, uint16_t offset, size_t size) {
	SERVICE_SERVER_STREAM_ITEM_packet_t stream_item;

	if(size > service->data_size) {
		C_DEBUG(C_DEBUG_LEVEL_WARN, "SERVICE: Requested too much data (%d > %d), request size reduced to %d.\n", size, service->data_size, service->data_size);
		size = service->data_size;
	}

	C_DEBUG(C_DEBUG_LEVEL_DEBUG, "SERVICE: Sending %d byte(s) SERVER STREAM ITEM for service \"%s\" (0x%X...) to node %s...\n", size, service->id, (uint8_t) *service->data, addr_to_str(to));
	copy_service_id(stream_item.service_id, service->id);
	if(service->state == SERVICE_STATE_REDIRECTING) {
		if(service->redirect_addr == C_BROADCAST_ADDR) {
			C_DEBUG(C_DEBUG_LEVEL_FATAL, "SERVICE: Service is redirecting, but address is set to broadcast.\n");
			abort();
		}
		stream_item.redirect_addr = service->redirect_addr;
	} else {
		stream_item.redirect_addr = C_BROADCAST_ADDR;
	}

	C_sendto2(to, APP_TYPE_SERVICE, SERVICE_PACKET_TYPE_SERVER_STREAM_ITEM, &stream_item, sizeof(SERVICE_SERVER_STREAM_ITEM_packet_t), (service->data + offset), size);
}

/**
 * @brief Send SERVICE NOT AVAILABLE packet.
 *
 * @param to Address of destination node.
 * @param service_id ID of the requested service.
 * @param redirect_addr Address to redirect future service requests to.
 */
static void send_SERVICE_SERVER_NOT_AVAILABLE(C_addr_t to, const SERVICE_id_t* service_id, C_addr_t redirect_addr) {
	SERVICE_SERVER_NOT_AVAILABLE_packet_t not_available;

	C_DEBUG(C_DEBUG_LEVEL_DEBUG, "SERVICE: Sending SERVER NOT AVAILABLE for service \"%s\" to node %s...\n", service_id, addr_to_str(to));
	copy_service_id(not_available.service_id, service_id);
	not_available.redirect_addr = redirect_addr;
	RUDP_sendto(to, APP_TYPE_SERVICE, SERVICE_PACKET_TYPE_SERVER_NOT_AVAILABLE, &not_available, sizeof(SERVICE_SERVER_NOT_AVAILABLE_packet_t), NULL, NULL);
}

/**
 * @brief Handle incoming SERVICE DATA REQUEST packet.
 *
 * Send a SERVICE DATA ITEM packet if the service is available or a SERVICE NOT
 * AVAILABLE packet otherwise.
 *
 * @param header Header of incoming packet.
 * @param request Data request information.
 */
void handle_SERVICE_CLIENT_DATA_REQUEST(const packet_header_t* header, const SERVICE_CLIENT_DATA_REQUEST_packet_t* request) {
	SERVICE_service_t* service = get_service(request->service_id);
	if(service == NULL) {
		C_DEBUG(C_DEBUG_LEVEL_WARN, "SERVICE: Received CLIENT DATA REQUEST from node %s, but service \"%s\" is not available.\n", addr_to_str(header->from), request->service_id);
		send_SERVICE_SERVER_NOT_AVAILABLE(header->from, request->service_id, C_BROADCAST_ADDR);
		return;
	} else if(
		(service->state != SERVICE_STATE_RUNNING)
		&& (service->state != SERVICE_STATE_REPLICATING)
		&& (service->state != SERVICE_STATE_REDIRECTING)
	) {
		C_DEBUG(C_DEBUG_LEVEL_WARN, "SERVICE: Received CLIENT DATA REQUEST from node %s, but service \"%s\" is not active.\n", addr_to_str(header->from), request->service_id);
		if(service->state == SERVICE_STATE_STOPPED)
			send_SERVICE_SERVER_NOT_AVAILABLE(header->from, request->service_id, service->redirect_addr);
		return;
	} else if(service->data_size < (request->offset + request->size)) {
		C_DEBUG(C_DEBUG_LEVEL_WARN, "SERVICE: Received CLIENT DATA REQUEST for %d byte(s) at 0x%X of service \"%s\" from node %s, but request size exceeds service data (%d < (%d + %d)).\n", request->size, request->offset, request->service_id, addr_to_str(header->from), service->data_size, request->offset, request->size);
		return;
	}

	C_DEBUG(C_DEBUG_LEVEL_DEBUG, "SERVICE: Received CLIENT DATA REQUEST for %d byte(s) at 0x%X of service \"%s\" from node %s.\n", request->size, request->offset, request->service_id, addr_to_str(header->from));
	send_SERVICE_SERVER_DATA_ITEM(header->from, service, request->offset, request->size, request->start_time);
	PLACEMENT_stat_service_request(service, header->from, header->last_hop, header->hops, request->size);
}

/**
 * @brief Handle incoming SERVICE DATA UPDATE packet.
 *
 * Update the service data if the srevice is available or send a SERVICE NOT
 * AVAILABLE packet otherwise.
 *
 * @param header Header of incoming packet.
 * @param update CLIENT DATA UPDATE header.
 * @param data Data to update.
 * @param data_size Size of data.
 */
void handle_SERVICE_CLIENT_DATA_UPDATE(const packet_header_t* header, const SERVICE_CLIENT_DATA_UPDATE_packet_t* update, const char* data, size_t data_size) {
	SERVICE_service_t* service = get_service(update->service_id);
	if(service == NULL) {
		C_DEBUG(C_DEBUG_LEVEL_WARN, "SERVICE: Received CLIENT DATA UPDATE from node %s, but service \"%s\" is not available.\n", addr_to_str(header->from), update->service_id);
		send_SERVICE_SERVER_NOT_AVAILABLE(header->from, update->service_id, C_BROADCAST_ADDR);
		return;
	} else if(service->state != SERVICE_STATE_RUNNING) {
		C_DEBUG(C_DEBUG_LEVEL_WARN, "SERVICE: Received CLIENT DATA UPDATE from node %s, but service \"%s\" is not active.\n", addr_to_str(header->from), update->service_id);
		if(
			(service->state == SERVICE_STATE_REDIRECTING)
			|| (service->state == SERVICE_STATE_STOPPED)
		) {
			send_SERVICE_SERVER_NOT_AVAILABLE(header->from, update->service_id, service->redirect_addr);
		}
		return;

	// NOTE: We silently ignore DATA UPDATES that arrive while a service is
	// replicating. This may cause data inconsistencies.

	} else if(service->data_size < (update->offset + data_size)) {
		C_DEBUG(C_DEBUG_LEVEL_WARN, "SERVICE: Received CLIENT DATA UPDATE for %d byte(s) at 0x%X of service \"%s\" from node %s, but update size exceeds service data (%d < (%d + %d)).\n", data_size, update->offset, update->service_id, addr_to_str(header->from), service->data_size, update->offset, data_size);
		return;
	}

	C_DEBUG(C_DEBUG_LEVEL_DEBUG, "SERVICE: Received CLIENT DATA UPDATE for %d byte(s) at 0x%X of service \"%s\" (0x%X...) from node %s.\n", data_size, update->offset, update->service_id, (uint8_t) *data, addr_to_str(header->from));
	update_service_data(service, update->offset, data_size, data);
	PLACEMENT_stat_service_request(service, header->from, header->last_hop, header->hops, data_size);

	// NOTE: A this point we would have to take care of synchronizing with
	// other service instances.
}

/**
 * @brief Process an active stream.
 *
 * Send a SERVICE STREAM ITEM if the service is still active, possibly
 * reschedule until the next packet is due.
 *
 * @param parameter Pointer to stream to be processed.
 */
static void process_SERVICE_CLIENT_STREAM_REQUEST(callback_parameter_t parameter) {
	SERVICE_stream_t* stream = (SERVICE_stream_t*) parameter;
	SERVICE_service_t* service = get_service(stream->service_id);

	if(service == NULL) {
		C_DEBUG(C_DEBUG_LEVEL_ERROR, "SERVICE: Trying to stream service \"%s\" to node %s, but service is not available.\n", stream->service_id, addr_to_str(stream->to));
		send_SERVICE_SERVER_NOT_AVAILABLE(stream->to, stream->service_id, C_BROADCAST_ADDR);
		list_remove(C_SERVICE_served_streams, stream);
		FREE(stream);
		return;
	} else if(
		(service->state != SERVICE_STATE_RUNNING)
		&& (service->state != SERVICE_STATE_REPLICATING)
		&& (service->state != SERVICE_STATE_REDIRECTING)
	) {
		C_DEBUG(C_DEBUG_LEVEL_ERROR, "SERVICE: Trying to stream service \"%s\" to node %s, but service is not active.\n", stream->service_id, addr_to_str(stream->to));
		if(service->state == SERVICE_STATE_STOPPED)
			send_SERVICE_SERVER_NOT_AVAILABLE(stream->to, stream->service_id, service->redirect_addr);
		list_remove(C_SERVICE_served_streams, stream);
		FREE(stream);
		return;
	}

	C_DEBUG(C_DEBUG_LEVEL_DEBUG, "SERVICE: Processing stream of service \"%s\" to node %s...\n", stream->service_id, addr_to_str(stream->to));
	send_SERVICE_SERVER_STREAM_ITEM(stream->to, service, stream->offset, stream->size);
	if((C_time() + stream->interval) <= stream->end_time) {
		callback_add(process_SERVICE_CLIENT_STREAM_REQUEST, stream, stream->interval);
	} else {
		C_DEBUG(C_DEBUG_LEVEL_INFO, "SERVICE: Stream of service \"%s\" to node %s has ended.\n", stream->service_id, addr_to_str(stream->to));
		list_remove(C_SERVICE_served_streams, stream);
		FREE(stream);
	}
}

/**
 * @brief Check whether a two streams match.
 *
 * @param item Stream to check.
 * @param cmp_item Reference stream.
 * @return <code>true</code> if the two streams match, <code>false</code>
 * otherwise.
 */
static bool matches_stream(const void* item, const void* cmp_item) {
	SERVICE_stream_t* stream = (SERVICE_stream_t*) item;
	SERVICE_stream_t* cmp_stream = (SERVICE_stream_t*) cmp_item;
	return (
		(stream->to == cmp_stream->to)
		&& (strcmp(stream->service_id, cmp_stream->service_id) == 0)
		&& (stream->offset == cmp_stream->offset)
		&& (stream->size == cmp_stream->size)
		&& (stream->interval == cmp_stream->interval)
	);
}

/**
 * @brief Handle incoming SERVICE STREAM REQUEST packet.
 *
 * Schedule a stream for processing if the service is available and additional
 * streams can be supported, send a SERVICE NOT AVAILABLE packet otherwise.
 *
 * @param header Header of incoming packet.
 * @param request Stream request information.
 */
void handle_SERVICE_CLIENT_STREAM_REQUEST(const packet_header_t* header, const SERVICE_CLIENT_STREAM_REQUEST_packet_t* request) {
	SERVICE_stream_t cmp_stream;
	SERVICE_stream_t* stream = NULL;
	unsigned int expected_stream_size;

	SERVICE_service_t* service = get_service(request->service_id);
	if(service == NULL) {
		C_DEBUG(C_DEBUG_LEVEL_WARN, "SERVICE: Received CLIENT STREAM REQUEST from node %s, but service \"%s\"is not available.\n", addr_to_str(header->from), request->service_id);
		send_SERVICE_SERVER_NOT_AVAILABLE(header->from, request->service_id, C_BROADCAST_ADDR);
		return;
	} else if((service->state != SERVICE_STATE_RUNNING) && (service->state != SERVICE_STATE_REPLICATING)) {
		C_DEBUG(C_DEBUG_LEVEL_WARN, "SERVICE: Received CLIENT STREAM REQUEST from node %s, but service \"%s\"is not active.\n", addr_to_str(header->from), request->service_id);
		if(service->state == SERVICE_STATE_STOPPED)
			send_SERVICE_SERVER_NOT_AVAILABLE(header->from, request->service_id, service->redirect_addr);
		return;
	} else if(list_size(C_SERVICE_served_streams) >= SERVICE_MAX_SERVED_STREAMS) {
		C_DEBUG(C_DEBUG_LEVEL_ERROR, "SERVICE: No empty slots for stream processing.\n");
		send_SERVICE_SERVER_NOT_AVAILABLE(header->from, request->service_id, C_BROADCAST_ADDR);
		return;
	} else if(service->data_size < (request->offset + request->size)) {
		C_DEBUG(C_DEBUG_LEVEL_WARN, "SERVICE: Received CLIENT STREAM REQUEST for %d byte(s) at 0x%X of service \"%s\" from node %s, but request size exceeds service data (%d < (%d + %d)).\n", request->size, request->offset, request->service_id, addr_to_str(header->from), service->data_size, request->offset, request->size);
		return;
	}

	C_DEBUG(C_DEBUG_LEVEL_DEBUG, "SERVICE: Received CLIENT STREAM REQUEST for %d byte(s) at 0x%X of service \"%s\" at interval %.2fs for a duration of %.3fs from node %s.\n", request->size, request->offset, request->service_id, request->interval, request->duration, addr_to_str(header->from));
	cmp_stream.to = header->from;
	copy_service_id(cmp_stream.service_id, request->service_id);
	cmp_stream.offset = request->offset;
	cmp_stream.size = request->size;
	cmp_stream.interval = request->interval;
	if(list_find(C_SERVICE_served_streams, &cmp_stream, matches_stream)) {
		C_DEBUG(C_DEBUG_LEVEL_WARN, "SERVICE: A matching CLIENT STREAM REQUEST is already being served, ignoring request.\n");
		return;
	}

	// Initialize stream.
	MALLOC(stream, SERVICE_stream_t);
	stream->to = header->from;
	copy_service_id(stream->service_id, request->service_id);
	stream->offset = request->offset;
	stream->size = request->size;
	stream->interval = request->interval;
	stream->end_time = request->duration + C_time();
	list_add(C_SERVICE_served_streams, stream);
	callback_add(process_SERVICE_CLIENT_STREAM_REQUEST, stream, 0);

	expected_stream_size = request->size * ((unsigned int) (request->duration / request->interval)) + 1;
	PLACEMENT_stat_service_request(service, header->from, header->last_hop, header->hops, expected_stream_size);
}
