/************************************************************************
* net.c
* voxelands - 3d voxel world sandbox game
* Copyright (C) Lisa 'darkrose' Milne 2016 <lisa@ltmnet.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program.  If not, see <http://www.gnu.org/licenses/>
************************************************************************/

#include "common.h"
#include "list.h"
#include "array.h"
#define _NET_LOCAL
#include "net.h"

#include <stdio.h>
#include <stdarg.h>
#ifndef WIN32
#include <unistd.h>
#include <fcntl.h>
#endif

struct netfuncs_s {
	int (*reconnect)(net_connection_t *n);
	int (*write)(net_connection_t *n, void *buff, unsigned int size);
	int (*read)(net_connection_t *n, void *buff, unsigned int size);
	int (*readline)(net_connection_t *n, void *buf, unsigned int size);
	int (*broadcast)(net_connection_t *n, void *buff, unsigned int size);
	int (*accept)(net_connection_t *n);
	int (*pending)(net_connection_t *n);
};

static struct {
	net_connection_t *connections;
	int ids;
	int isinit;
	struct netfuncs_s funcs[4];
#ifdef WIN32
	WSADATA wsa_data;
#endif
} net = {
	NULL,
	0,
	0,
	{
		{
			tcp_client_reconnect,
			tcp_write,
			tcp_read,
			tcp_readline,
			NULL,
			NULL,
			tcp_pending
		},
		{
			udp_client_reconnect,
			udp_write,
			udp_read,
			udp_readline,
			NULL,
			NULL,
			udp_pending
		},
		{
			tcp_host_reconnect,
			tcp_write,
			tcp_read,
			tcp_readline,
			tcp_broadcast,
			tcp_accept,
			tcp_pending
		},
		{
			udp_host_reconnect,
			udp_write,
			udp_read,
			udp_readline,
			udp_broadcast,
			udp_accept,
			udp_pending
		}
	}
};

int net_init()
{
	if (net.isinit)
		return 0;
#ifdef WIN32
	WSAStartup(MAKEWORD(2, 2), &wsaData);
#endif
	net.isinit = 1;
	return 0;
}

/* create a new net connection */
net_connection_t *net_connection()
{
	net_connection_t *n = malloc(sizeof(net_connection_t));

	n->state = NETSTATE_UNUSED;
	n->tries = 0;
	n->type = NETSTATE_UNUSED;
	n->host = NULL;
	n->port = NULL;
	n->id = net.ids++;
	n->peers = NULL;

	net.connections = list_push((void**)&net.connections,n);

	return n;
}

/* find a net connection */
net_connection_t *net_fetch(int id)
{
	net_connection_t *n = net.connections;

	while (n) {
		if (n->id == id)
			break;
		n = n->next;
	}

	return n;
}

/* get the state of a net connection */
int net_state(net_connection_t *n)
{
#ifndef WIN32
	int fopts;
	if (!n)
		return NETSTATE_CLOSED;
	if (n->state == NETSTATE_OPEN) {
		if (fcntl(n->fd, F_GETFL, &fopts) < 0) {
			close(n->fd);
#else
	/* windows 'almost the same, but not quite' urgh */
	unsigned long fopts;
	if (!n)
		return NETSTATE_CLOSED;
	if (n->state == NETSTATE_OPEN) {
		if (ioctlsocket(n->fd, FIONREAD, &fopts) < 0) {
			closesocket(n->fd);
#endif
			n->state = NETSTATE_CLOSED;
		}
	}
	return n->state;
}

/* close a net connection */
void net_close(net_connection_t *n)
{
	if (!n)
		return;

	if (net_state(n) == NETSTATE_OPEN)
		shutdown(n->fd,2);
#ifndef WIN32
	close(n->fd);
#else
	closesocket(n->fd);
#endif

	list_remove((void**)&net.connections,n);
	freeaddrinfo(n->addr);
	if (n->host)
		free(n->host);
	if (n->port)
		free(n->port);
	if (n->peers) {
		net_connection_t *p;
		while ((p = array_pop_ptr(n->peers))) {
			net_close(p);
		}
		array_free(n->peers,1);
	}
	free(n);
}

/* disconnect a net connection */
void net_disconnect(net_connection_t *n)
{
	if (net_state(n) == NETSTATE_OPEN)
		shutdown(n->fd,2);
#ifndef WIN32
	close(n->fd);
#else
	closesocket(n->fd);
#endif
	if (n->peers) {
		net_connection_t *p;
		while ((p = array_pop_ptr(n->peers))) {
			net_close(p);
		}
		array_free(n->peers,1);
		n->peers = NULL;
	}

	n->state = NETSTATE_CLOSED;
}

/* write data to a net connection */
int net_write(net_connection_t *n, void *buff, unsigned int size)
{
	return net.funcs[n->type].write(n,buff,size);
}

/* write a string to a net connection */
int net_writef(net_connection_t *n, char* fmt, ...)
{
	int l;
	char buff[1024];
	va_list ap;
	va_start(ap, fmt);
	l = vsnprintf(buff, 1024, fmt, ap);
	va_end(ap);

	if (l < 1)
		return l;

	return net.funcs[n->type].write(n,buff,l);
}

/* read data from a net connection */
int net_read(net_connection_t *n, void *buff, unsigned int size)
{
	return net.funcs[n->type].read(n,buff,size);
}

/* read a line from a net connection */
int net_readline(net_connection_t *n, void *buff, unsigned int size)
{
	return net.funcs[n->type].readline(n,buff,size);
}

/* broadcast data to all clients of a net connection */
int net_broadcast(net_connection_t *n, void *buff, unsigned int size)
{
	if (net.funcs[n->type].broadcast)
		return net.funcs[n->type].broadcast(n,buff,size);
	return 0;
}

/* accept new connection from a net connection */
int net_accept(net_connection_t *n)
{
	if (net.funcs[n->type].accept)
		return net.funcs[n->type].accept(n);
	return -1;
}

/* wait for data on a net connection */
array_t *net_select(unsigned int msec, int reconnect, net_connection_t *n)
{
	array_t *r;
	array_t a;

	array_init(&a,ARRAY_TYPE_PTR);

	array_push_ptr(&a,n);

	r = net_select_array(msec,reconnect,&a);

	return r;
}

/* wait for data on an array of net connections */
array_t *net_select_array(unsigned int msec, int reconnect, array_t *connections)
{
	fd_set rfds;
	struct timeval tv;
	int m = 0;
	int cont = 1;
	int i;
	int s;
	int st;
	int rl;
	unsigned int usec = 1000;
	array_t *a = array_copy(connections);
	net_connection_t **n = a->data;
	array_t *r = array_create(ARRAY_TYPE_PTR);

	if (!a)
		return r;
	if (!r) {
		array_free(a,1);
		return r;
	}

	if (!msec)
		msec = 100;

	usec *= msec;

	tv.tv_sec = 0;
	tv.tv_usec = usec;

	for (i=0; i<a->length; i++) {
		if (net.funcs[n[i]->type].pending(n[i])) {
			array_push_ptr(r,n[i]);
			n[i] = NULL;
		}
	}

	if (r->length == a->length) {
		array_free(a,1);
		return r;
	}

	rl = r->length;

	while (cont) {
		if (!tv.tv_usec)
			break;
		FD_ZERO(&rfds);
		m = 0;
		for (i=0; i<a->length; i++) {
			if (n[i] && net_state(n[i]) == NETSTATE_OPEN) {
				FD_SET(n[i]->fd, &rfds);
				if (n[i]->fd > m)
					m = n[i]->fd;
			}
		}
		if (!m) {
			array_free(a,1);
			array_free(r,1);
			return NULL;
		}
		m++;

		s = select(m, &rfds, NULL, NULL, &tv);
		if (s < 1) {
			if (reconnect) {
				for (i=0; i<a->length; i++) {
					if (n[i] && net_state(n[i]) == NETSTATE_CLOSED)
						net.funcs[n[i]->type].reconnect(n[i]);
				}
			}
		}else{
			for (i=0; i<a->length; i++) {
				if (!n[i])
					continue;
				st = net_state(n[i]);
				switch (st) {
				case NETSTATE_OPEN:
					if (FD_ISSET(n[i]->fd,&rfds))
						array_push_ptr(r,n[i]);
					break;
				case NETSTATE_CLOSED:
					if (reconnect)
						net.funcs[n[i]->type].reconnect(n[i]);
					break;
				default:;
				}
			}
			if (r->length > rl)
				break;
		}
	}

	array_free(a,1);

	return r;
}

/* wait for data from a host's peers */
array_t *net_select_peers(net_connection_t *n, unsigned int msec)
{
	int r;
	array_t *a;

	switch (n->type) {
	case NETTYPE_TCP_HOST:
		return net_select_array(msec,0,n->peers);
	case NETTYPE_UDP_HOST:
		r = udp_accept(n);
		if (r < 0)
			return NULL;
		a = array_create(ARRAY_TYPE_PTR);
		array_push_ptr(a,array_get_ptr(n->peers,r));
		return a;
	default:;
	}
	return NULL;
}
