#include <base/system.h>
#include <string.h>
#include "e_config.h"
#include "e_network_internal.h"

static void conn_reset_stats(NETCONNECTION *conn)
{
	mem_zero(&conn->stats, sizeof(conn->stats));
}

static void conn_reset(NETCONNECTION *conn)
{
	conn->seq = 0;
	conn->ack = 0;
	conn->remote_closed = 0;
	
	conn->state = NET_CONNSTATE_OFFLINE;
	conn->last_send_time = 0;
	conn->last_recv_time = 0;
	conn->last_update_time = 0;
	conn->token = -1;
	mem_zero(&conn->peeraddr, sizeof(conn->peeraddr));
	
	conn->buffer = ringbuf_init(conn->buffer_memory, sizeof(conn->buffer_memory), 0);
	
	mem_zero(&conn->construct, sizeof(conn->construct));
}


const char *conn_error(NETCONNECTION *conn)
{
	return conn->error_string;
}

static void conn_set_error(NETCONNECTION *conn, const char *str)
{
	str_copy(conn->error_string, str, sizeof(conn->error_string));
}

void conn_init(NETCONNECTION *conn, NETSOCKET socket)
{
	conn_reset(conn);
	conn_reset_stats(conn);
	conn->socket = socket;
	mem_zero(conn->error_string, sizeof(conn->error_string));
}


static void conn_ack(NETCONNECTION *conn, int ack)
{
	
	while(1)
	{
		NETCHUNKDATA *resend = (NETCHUNKDATA *)ringbuf_first(conn->buffer);
		if(!resend)
			break;
		
		if(seq_in_backroom(resend->sequence, ack))
			ringbuf_popfirst(conn->buffer);
		else
			break;
	}
}

void conn_want_resend(NETCONNECTION *conn)
{
	conn->construct.flags |= NET_PACKETFLAG_RESEND;
}

int conn_flush(NETCONNECTION *conn)
{
	int num_chunks = conn->construct.num_chunks;
	if(!num_chunks && !conn->construct.flags)
		return 0;

	/* send of the packets */	
	conn->construct.ack = conn->ack;
	send_packet(conn->socket, &conn->peeraddr, &conn->construct);
	
	/* update send times */
	conn->last_send_time = time_get();
	
	/* clear construct so we can start building a new package */
	mem_zero(&conn->construct, sizeof(conn->construct));
	return num_chunks;
}

static void conn_queue_chunk_ex(NETCONNECTION *conn, int flags, int data_size, const void *data, int sequence)
{
	unsigned char *chunk_data;
	
	/* check if we have space for it, if not, flush the connection */
	if(conn->construct.data_size + data_size + NET_MAX_CHUNKHEADERSIZE > sizeof(conn->construct.chunk_data))
		conn_flush(conn);

	/* pack all the data */
	chunk_data = &conn->construct.chunk_data[conn->construct.data_size];
	chunk_data = pack_chunk_header(chunk_data, flags, data_size, sequence);
	mem_copy(chunk_data, data, data_size);
	chunk_data += data_size;

	/* */
	conn->construct.num_chunks++;
	conn->construct.data_size = (int)(chunk_data-conn->construct.chunk_data);
	
	/* set packet flags aswell */
	
	if(flags&NET_CHUNKFLAG_VITAL && !(flags&NET_CHUNKFLAG_RESEND))
	{
		/* save packet if we need to resend */
		NETCHUNKDATA *resend = (NETCHUNKDATA *)ringbuf_allocate(conn->buffer, sizeof(NETCHUNKDATA)+data_size);
		if(resend)
		{
			resend->sequence = sequence;
			resend->flags = flags;
			resend->data_size = data_size;
			resend->data = (unsigned char *)(resend+1);
			resend->first_send_time = time_get();
			resend->last_send_time = resend->first_send_time;
			mem_copy(resend->data, data, data_size);
		}
		else
		{
			/* out of buffer */
			conn_disconnect(conn, "too weak connection (out of buffer)");
		}
	}
}

void conn_queue_chunk(NETCONNECTION *conn, int flags, int data_size, const void *data)
{
	if(flags&NET_CHUNKFLAG_VITAL)
		conn->seq = (conn->seq+1)%NET_MAX_SEQUENCE;
	conn_queue_chunk_ex(conn, flags, data_size, data, conn->seq);
}


static void conn_send_control(NETCONNECTION *conn, int controlmsg, const void *extra, int extra_size)
{
	/* send the control message */
	conn->last_send_time = time_get();
	send_controlmsg(conn->socket, &conn->peeraddr, conn->ack, controlmsg, extra, extra_size);
}

static void conn_resend_chunk(NETCONNECTION *conn, NETCHUNKDATA *resend)
{
	conn_queue_chunk_ex(conn, resend->flags|NET_CHUNKFLAG_RESEND, resend->data_size, resend->data, resend->sequence);
	resend->last_send_time = time_get();
}

static void conn_resend(NETCONNECTION *conn)
{
	int resend_count = 0;
	int first = 0, last = 0;
	void *item = ringbuf_first(conn->buffer);
	
	while(item)
	{
		NETCHUNKDATA *resend = item;
		
		if(resend_count == 0)
			first = resend->sequence;
		last = resend->sequence;
			
		conn_resend_chunk(conn, resend);
		item = ringbuf_next(conn->buffer, item);
		resend_count++;
	}
	
	if(config.debug)
		dbg_msg("conn", "resent %d packets (%d to %d)", resend_count, first, last);
}

int conn_connect(NETCONNECTION *conn, NETADDR *addr)
{
	if(conn->state != NET_CONNSTATE_OFFLINE)
		return -1;
	
	/* init connection */
	conn_reset(conn);
	conn->peeraddr = *addr;
	mem_zero(conn->error_string, sizeof(conn->error_string));
	conn->state = NET_CONNSTATE_CONNECT;
	conn_send_control(conn, NET_CTRLMSG_CONNECT, 0, 0);
	return 0;
}

void conn_disconnect(NETCONNECTION *conn, const char *reason)
{
	if(conn->state == NET_CONNSTATE_OFFLINE)
		return;

	if(conn->remote_closed == 0)
	{
		if(reason)
			conn_send_control(conn, NET_CTRLMSG_CLOSE, reason, strlen(reason)+1);
		else
			conn_send_control(conn, NET_CTRLMSG_CLOSE, 0, 0);

		conn->error_string[0] = 0;
		if(reason)
			str_copy(conn->error_string, reason, sizeof(conn->error_string));
	}
	
	conn_reset(conn);
}

int conn_feed(NETCONNECTION *conn, NETPACKETCONSTRUCT *packet, NETADDR *addr)
{
	int64 now = time_get();
	conn->last_recv_time = now;
	
	/* check if resend is requested */
	if(packet->flags&NET_PACKETFLAG_RESEND)
		conn_resend(conn);

	/* */									
	if(packet->flags&NET_PACKETFLAG_CONTROL)
	{
		int ctrlmsg = packet->chunk_data[0];
		
		if(ctrlmsg == NET_CTRLMSG_CLOSE)
		{
			conn->state = NET_CONNSTATE_ERROR;
			conn->remote_closed = 1;
			
			if(packet->data_size)
			{
				/* make sure to sanitize the error string form the other party*/
				char str[128];
				if(packet->data_size < 128)
					str_copy(str, (char *)packet->chunk_data, packet->data_size);
				else
					str_copy(str, (char *)packet->chunk_data, 128);
				str_sanitize_strong(str);
				
				/* set the error string */
				conn_set_error(conn, str);
			}
			else
				conn_set_error(conn, "no reason given");
				
			if(config.debug)
				dbg_msg("conn", "closed reason='%s'", conn_error(conn));
			return 0;			
		}
		else
		{
			if(conn->state == NET_CONNSTATE_OFFLINE)
			{
				if(ctrlmsg == NET_CTRLMSG_CONNECT)
				{
					/* send response and init connection */
					conn_reset(conn);
					conn->state = NET_CONNSTATE_PENDING;
					conn->peeraddr = *addr;
					conn->last_send_time = now;
					conn->last_recv_time = now;
					conn->last_update_time = now;
					conn_send_control(conn, NET_CTRLMSG_CONNECTACCEPT, 0, 0);
					if(config.debug)
						dbg_msg("connection", "got connection, sending connect+accept");			
				}
			}
			else if(conn->state == NET_CONNSTATE_CONNECT)
			{
				/* connection made */
				if(ctrlmsg == NET_CTRLMSG_CONNECTACCEPT)
				{
					conn_send_control(conn, NET_CTRLMSG_ACCEPT, 0, 0);
					conn->state = NET_CONNSTATE_ONLINE;
					if(config.debug)
						dbg_msg("connection", "got connect+accept, sending accept. connection online");
				}
			}
			else if(conn->state == NET_CONNSTATE_ONLINE)
			{
				/* connection made */
				/*
				if(ctrlmsg == NET_CTRLMSG_CONNECTACCEPT)
				{
					
				}*/
			}
		}
	}
	else
	{
		if(conn->state == NET_CONNSTATE_PENDING)
		{
			conn->state = NET_CONNSTATE_ONLINE;
			if(config.debug)
				dbg_msg("connection", "connecting online");
		}
	}
	
	if(conn->state == NET_CONNSTATE_ONLINE)
	{
		
		conn_ack(conn, packet->ack);
	}
	
	return 1;
}

int conn_update(NETCONNECTION *conn)
{
	int64 now = time_get();

	if(conn->state == NET_CONNSTATE_OFFLINE || conn->state == NET_CONNSTATE_ERROR)
		return 0;
	
	/* check for timeout */
	if(conn->state != NET_CONNSTATE_OFFLINE &&
		conn->state != NET_CONNSTATE_CONNECT &&
		(now-conn->last_recv_time) > time_freq()*10)
	{
		conn->state = NET_CONNSTATE_ERROR;
		conn_set_error(conn, "timeout");
	}

	/* fix resends */
	if(ringbuf_first(conn->buffer))
	{
		NETCHUNKDATA *resend = (NETCHUNKDATA *)ringbuf_first(conn->buffer);

		/* check if we have some really old stuff laying around and abort if not acked */
		if(now-resend->first_send_time > time_freq()*10)
		{
			conn->state = NET_CONNSTATE_ERROR;
			conn_set_error(conn, "too weak connection (not acked for 10 seconds)");
		}
		else
		{
			/* resend packet if we havn't got it acked in 1 second */
			if(now-resend->last_send_time > time_freq())
				conn_resend_chunk(conn, resend);
		}
	}
	
	/* send keep alives if nothing has happend for 250ms */
	if(conn->state == NET_CONNSTATE_ONLINE)
	{
		if(time_get()-conn->last_send_time > time_freq()/2) /* flush connection after 500ms if needed */
		{
			int num_flushed_chunks = conn_flush(conn);
			if(num_flushed_chunks && config.debug)
				dbg_msg("connection", "flushed connection due to timeout. %d chunks.", num_flushed_chunks);
		}
			
		if(time_get()-conn->last_send_time > time_freq())
			conn_send_control(conn, NET_CTRLMSG_KEEPALIVE, 0, 0);
	}
	else if(conn->state == NET_CONNSTATE_CONNECT)
	{
		if(time_get()-conn->last_send_time > time_freq()/2) /* send a new connect every 500ms */
			conn_send_control(conn, NET_CTRLMSG_CONNECT, 0, 0);
	}
	else if(conn->state == NET_CONNSTATE_PENDING)
	{
		if(time_get()-conn->last_send_time > time_freq()/2) /* send a new connect/accept every 500ms */
			conn_send_control(conn, NET_CTRLMSG_CONNECTACCEPT, 0, 0);
	}
	
	return 0;
}
