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

typedef struct
{
	NETCONNECTION conn;
} NETSLOT;

typedef struct NETBAN
{
	NETBANINFO info;
	
	/* hash list */
	struct NETBAN *hashnext;
	struct NETBAN *hashprev;
	
	/* used or free list */
	struct NETBAN *next;
	struct NETBAN *prev;
} NETBAN;

#define MACRO_LIST_LINK_FIRST(object, first, prev, next) \
	{ if(first) first->prev = object; \
	object->prev = (void*)0; \
	object->next = first; \
	first = object; }
	
#define MACRO_LIST_LINK_AFTER(object, after, prev, next) \
	{ object->prev = after; \
	object->next = after->next; \
	after->next = object; \
	if(object->next) \
		object->next->prev = object; \
	}

#define MACRO_LIST_UNLINK(object, first, prev, next) \
	{ if(object->next) object->next->prev = object->prev; \
	if(object->prev) object->prev->next = object->next; \
	else first = object->next; \
	object->next = 0; object->prev = 0; }
	
#define MACRO_LIST_FIND(start, next, expression) \
	{ while(start && !(expression)) start = start->next; }

struct NETSERVER
{
	NETSOCKET socket;
	NETSLOT slots[NET_MAX_CLIENTS];
	int max_clients;

	NETBAN *bans[256];
	NETBAN banpool[NET_SERVER_MAXBANS];
	NETBAN *banpool_firstfree;
	NETBAN *banpool_firstused;

	NETFUNC_NEWCLIENT new_client;
	NETFUNC_NEWCLIENT del_client;
	void *user_ptr;
	
	NETRECVINFO recv;
};

NETSERVER *netserver_open(NETADDR bindaddr, int max_clients, int flags)
{
	int i;
	NETSERVER *server;
	NETSOCKET socket = net_udp_create(bindaddr);
	if(socket == NETSOCKET_INVALID)
		return 0;
	
	server = (NETSERVER *)mem_alloc(sizeof(NETSERVER), 1);
	mem_zero(server, sizeof(NETSERVER));
	server->socket = socket;
	server->max_clients = max_clients;
	if(server->max_clients > NET_MAX_CLIENTS)
		server->max_clients = NET_MAX_CLIENTS;
	if(server->max_clients < 1)
		server->max_clients = 1;
	
	for(i = 0; i < NET_MAX_CLIENTS; i++)
		conn_init(&server->slots[i].conn, server->socket);
	
	/* setup all pointers for bans */
	for(i = 1; i < NET_SERVER_MAXBANS-1; i++)
	{
		server->banpool[i].next = &server->banpool[i+1];
		server->banpool[i].prev = &server->banpool[i-1];
	}
	
	server->banpool[0].next = &server->banpool[1];
	server->banpool[NET_SERVER_MAXBANS-1].prev = &server->banpool[NET_SERVER_MAXBANS-2];
	server->banpool_firstfree = &server->banpool[0];

	return server;
}

int netserver_set_callbacks(NETSERVER *s, NETFUNC_NEWCLIENT new_client, NETFUNC_DELCLIENT del_client, void *user)
{
	s->new_client = new_client;
	s->del_client = del_client;
	s->user_ptr = user;
	return 0;
}

int netserver_max_clients(NETSERVER *s)
{
	return s->max_clients;
}

int netserver_close(NETSERVER *s)
{
	/* TODO: implement me */
	return 0;
}

int netserver_drop(NETSERVER *s, int client_id, const char *reason)
{
	/* TODO: insert lots of checks here */
	NETADDR addr;
	netserver_client_addr(s, client_id, &addr);
	
	dbg_msg("net_server", "client dropped. cid=%d ip=%d.%d.%d.%d reason=\"%s\"",
		client_id,
		addr.ip[0], addr.ip[1], addr.ip[2], addr.ip[3],
		reason
		);
	conn_disconnect(&s->slots[client_id].conn, reason);

	if(s->del_client)
		s->del_client(client_id, s->user_ptr);
		
	return 0;
}

int netserver_ban_get(NETSERVER *s, int index, NETBANINFO *info)
{
	NETBAN *ban;
	for(ban = s->banpool_firstused; ban && index; ban = ban->next, index--)
		{}
		
	if(!ban)
		return 0;
	*info = ban->info;
	return 1;
}

int netserver_ban_num(NETSERVER *s)
{
	int count = 0;
	NETBAN *ban;
	for(ban = s->banpool_firstused; ban; ban = ban->next)
		count++;
	return count;
}

static void netserver_ban_remove_by_object(NETSERVER *s, NETBAN *ban)
{
	int iphash = (ban->info.addr.ip[0]+ban->info.addr.ip[1]+ban->info.addr.ip[2]+ban->info.addr.ip[3])&0xff;
	dbg_msg("netserver", "removing ban on %d.%d.%d.%d",
		ban->info.addr.ip[0], ban->info.addr.ip[1], ban->info.addr.ip[2], ban->info.addr.ip[3]);
	MACRO_LIST_UNLINK(ban, s->banpool_firstused, prev, next);
	MACRO_LIST_UNLINK(ban, s->bans[iphash], hashprev, hashnext);
	MACRO_LIST_LINK_FIRST(ban, s->banpool_firstfree, prev, next);
}

int netserver_ban_remove(NETSERVER *s, NETADDR addr)
{
	int iphash = (addr.ip[0]+addr.ip[1]+addr.ip[2]+addr.ip[3])&0xff;
	NETBAN *ban = s->bans[iphash];
	
	MACRO_LIST_FIND(ban, hashnext, net_addr_comp(&ban->info.addr, &addr) == 0);
	
	if(ban)
	{
		netserver_ban_remove_by_object(s, ban);
		return 0;
	}
	
	return -1;
}

int netserver_ban_add(NETSERVER *s, NETADDR addr, int seconds)
{
	int iphash = (addr.ip[0]+addr.ip[1]+addr.ip[2]+addr.ip[3])&0xff;
	unsigned stamp = 0xffffffff;
	NETBAN *ban;
	
	/* remove the port */
	addr.port = 0;
	
	if(seconds)
		stamp = time_timestamp() + seconds;
		
	/* search to see if it already exists */
	ban = s->bans[iphash];
	MACRO_LIST_FIND(ban, hashnext, net_addr_comp(&ban->info.addr, &addr) == 0);
	if(ban)
	{
		/* adjust the ban */
		ban->info.expires = stamp;
		return 0;
	}
	
	if(!s->banpool_firstfree)
		return -1;

	/* fetch and clear the new ban */
	ban = s->banpool_firstfree;
	MACRO_LIST_UNLINK(ban, s->banpool_firstfree, prev, next);
	
	/* setup the ban info */
	ban->info.expires = stamp;
	ban->info.addr = addr;
	
	/* add it to the ban hash */
	MACRO_LIST_LINK_FIRST(ban, s->bans[iphash], hashprev, hashnext);
	
	/* insert it into the used list */
	{
		if(s->banpool_firstused)
		{
			NETBAN *insert_after = s->banpool_firstused;
			MACRO_LIST_FIND(insert_after, next, stamp < insert_after->info.expires);
			
			if(insert_after)
				insert_after = insert_after->prev;
			else
			{
				/* add to last */
				insert_after = s->banpool_firstused;
				while(insert_after->next)
					insert_after = insert_after->next;
			}
			
			if(insert_after)
			{
				MACRO_LIST_LINK_AFTER(ban, insert_after, prev, next);
			}
			else
			{
				MACRO_LIST_LINK_FIRST(ban, s->banpool_firstused, prev, next);
			}
		}
		else
		{
			MACRO_LIST_LINK_FIRST(ban, s->banpool_firstused, prev, next);
		}
	}

	/* drop banned clients */	
	{
		char buf[128];
		int i;
		NETADDR banaddr;
		
		if(seconds)
			str_format(buf, sizeof(buf), "you have been banned for %d minutes", seconds/60);
		else
			str_format(buf, sizeof(buf), "you have been banned for life");
		
		for(i = 0; i < s->max_clients; i++)
		{
			banaddr = s->slots[i].conn.peeraddr;
			banaddr.port = 0;
			
			if(net_addr_comp(&addr, &banaddr) == 0)
				netserver_drop(s, i, buf);
		}
	}
	return 0;
}

int netserver_update(NETSERVER *s)
{
	unsigned now = time_timestamp();
	
	int i;
	for(i = 0; i < s->max_clients; i++)
	{
		conn_update(&s->slots[i].conn);
		if(s->slots[i].conn.state == NET_CONNSTATE_ERROR)
			netserver_drop(s, i, conn_error(&s->slots[i].conn));
	}
	
	/* remove expired bans */
	while(s->banpool_firstused && s->banpool_firstused->info.expires < now)
	{
		NETBAN *ban = s->banpool_firstused;
		netserver_ban_remove_by_object(s, ban);
	}
	
	(void)now;
	
	return 0;
}

/*
	TODO: chopp up this function into smaller working parts
*/
int netserver_recv(NETSERVER *s, NETCHUNK *chunk)
{
	unsigned now = time_timestamp();
	
	while(1)
	{
		NETADDR addr;
		int i, bytes, found;
			
		/* check for a chunk */
		if(recvinfo_fetch_chunk(&s->recv, chunk))
			return 1;
		
		/* TODO: empty the recvinfo */
		bytes = net_udp_recv(s->socket, &addr, s->recv.buffer, NET_MAX_PACKETSIZE);

		/* no more packets for now */
		if(bytes <= 0)
			break;
		
		if(unpack_packet(s->recv.buffer, bytes, &s->recv.data) == 0)
		{
			NETBAN *ban = 0;
			NETADDR banaddr = addr;
			int iphash = (addr.ip[0]+addr.ip[1]+addr.ip[2]+addr.ip[3])&0xff;
			found = 0;
			banaddr.port = 0;
			
			/* search a ban */
			for(ban = s->bans[iphash]; ban; ban = ban->hashnext)
			{
				if(net_addr_comp(&ban->info.addr, &banaddr) == 0)
					break;
			}
			
			/* check if we just should drop the packet */
			if(ban)
			{
				// banned, reply with a message
				char banstr[128];
				if(ban->info.expires)
				{
					int mins = ((ban->info.expires - now)+59)/60;
					if(mins == 1)
						str_format(banstr, sizeof(banstr), "banned for %d minute", mins);
					else
						str_format(banstr, sizeof(banstr), "banned for %d minutes", mins);
				}
				else
					str_format(banstr, sizeof(banstr), "banned for life");
				send_controlmsg(s->socket, &addr, 0, NET_CTRLMSG_CLOSE, banstr, str_length(banstr)+1);
				continue;
			}
			
			if(s->recv.data.flags&NET_PACKETFLAG_CONNLESS)
			{
				chunk->flags = NETSENDFLAG_CONNLESS;
				chunk->client_id = -1;
				chunk->address = addr;
				chunk->data_size = s->recv.data.data_size;
				chunk->data = s->recv.data.chunk_data;
				return 1;
			}
			else
			{			
				/* TODO: check size here */
				if(s->recv.data.flags&NET_PACKETFLAG_CONTROL && s->recv.data.chunk_data[0] == NET_CTRLMSG_CONNECT)
				{
					found = 0;
					int similar = 0;
				
					/* check if we already got this client */
					for(i = 0; i < s->max_clients; i++)
					{
						if(s->slots[i].conn.state != NET_CONNSTATE_OFFLINE)
						{
							if(net_addr_comp(&s->slots[i].conn.peeraddr, &addr) == 0)
							{
								found = 1; /* silent ignore.. we got this client already */
								break;
							}
							if(s->slots[i].conn.peeraddr.type == addr.type && mem_comp(s->slots[i].conn.peeraddr.ip, addr.ip, sizeof(addr.ip)) == 0)
								similar++;
						}
					}

					
					/* client that wants to connect */
					if(!found)
					{
						if(config.sv_max_clients_per_ip && config.sv_max_clients_per_ip <= similar)
						{
							char sorry[160];
							str_format(sorry, sizeof(sorry), "Due to some jerks filling the server with fake clients, at most %d client(s) per IP are allowed to connect. Sorry.", config.sv_max_clients_per_ip);
							send_controlmsg(s->socket, &addr, 0, NET_CTRLMSG_CLOSE, sorry, str_length(sorry)+1);
							continue;
						}
						for(i = 0; i < s->max_clients; i++)
						{
							if(s->slots[i].conn.state == NET_CONNSTATE_OFFLINE)
							{
								found = 1;
								conn_feed(&s->slots[i].conn, &s->recv.data, &addr);
								if(s->new_client)
									s->new_client(i, s->user_ptr);
								break;
							}
						}
						
						if(!found)
						{
							const char fullmsg[] = "server is full";
							send_controlmsg(s->socket, &addr, 0, NET_CTRLMSG_CLOSE, fullmsg, sizeof(fullmsg));
						}
					}
				}
				else
				{
					/* normal packet, find matching slot */
					for(i = 0; i < s->max_clients; i++)
					{
						if(net_addr_comp(&s->slots[i].conn.peeraddr, &addr) == 0)
						{
							if(conn_feed(&s->slots[i].conn, &s->recv.data, &addr))
							{
								if(s->recv.data.data_size)
									recvinfo_start(&s->recv, &addr, &s->slots[i].conn, i);
							}
						}
					}
				}
			}
		}
	}
	return 0;
}

int netserver_send(NETSERVER *s, NETCHUNK *chunk)
{
	if(chunk->data_size >= NET_MAX_PAYLOAD)
	{
		dbg_msg("netserver", "packet payload too big. %d. dropping packet", chunk->data_size);
		return -1;
	}
	
	if(chunk->flags&NETSENDFLAG_CONNLESS)
	{
		/* send connectionless packet */
		send_packet_connless(s->socket, &chunk->address, chunk->data, chunk->data_size);
	}
	else
	{
		int f = 0;
		dbg_assert(chunk->client_id >= 0, "errornous client id");
		dbg_assert(chunk->client_id < s->max_clients, "errornous client id");
		
		if(chunk->flags&NETSENDFLAG_VITAL)
			f = NET_CHUNKFLAG_VITAL;
		
		conn_queue_chunk(&s->slots[chunk->client_id].conn, f, chunk->data_size, chunk->data);

		if(chunk->flags&NETSENDFLAG_FLUSH)
			conn_flush(&s->slots[chunk->client_id].conn);
	}
	return 0;
}

void netserver_stats(NETSERVER *s, NETSTATS *stats)
{
	int num_stats = sizeof(NETSTATS)/sizeof(int);
	int *istats = (int *)stats;
	int c, i;

	mem_zero(stats, sizeof(NETSTATS));
	
	for(c = 0; c < s->max_clients; c++)
	{
		if(s->slots[c].conn.state != NET_CONNSTATE_OFFLINE)
		{
			int *sstats = (int *)(&(s->slots[c].conn.stats));
			for(i = 0; i < num_stats; i++)
				istats[i] += sstats[i];
		}
	}
}

NETSOCKET netserver_socket(NETSERVER *s)
{
	return s->socket;
}


int netserver_client_addr(NETSERVER *s, int client_id, NETADDR *addr)
{
	*addr = s->slots[client_id].conn.peeraddr;
	return 1;
}
