/************************************************************************
* net_tcp.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"
#define _NET_LOCAL
#include "net.h"

#include <string.h>
#ifndef WIN32
#include <unistd.h>
#include <errno.h>
#endif

/* create a new tcp client connection */
net_connection_t *tcp_client_connect(char* host, char* port)
{
	net_connection_t *n = net_connection();
	n->type = NETTYPE_TCP;
	n->host = strdup(host);
	n->port = strdup(port);

	tcp_client_reconnect(n);

	return n;
}

/* reconnect a closed tcp client connection */
int tcp_client_reconnect(net_connection_t *n)
{
	int max_retries = config_get_int("net.max_retries");
	if (!max_retries)
		max_retries = 5;
	if (n->state == NETSTATE_OPEN || n->tries > max_retries)
		return 2;

	n->tries++;

	if (n->state == NETSTATE_UNUSED) {
		int f = config_get_int("net.ipv4");
		int s = config_get_int("net.ipv6");
		memset(&n->hints, 0, sizeof(n->hints));
		if (s == f) {
			n->hints.ai_family = AF_UNSPEC;
		}else if (s) {
			n->hints.ai_family = AF_INET6;
		}else if (f) {
			n->hints.ai_family = AF_INET;
		}
		n->hints.ai_socktype = SOCK_STREAM;

		/* resolve hostname */
		if (getaddrinfo(n->host, n->port, &n->hints, &n->addr)) {
			vlprintf(CN_ERROR "Unable to resolve host '%s'",n->host);
			return 1;
		}

		n->state = NETSTATE_CLOSED;
	}

	/* open socket */
	if ((n->fd = socket(n->addr->ai_family, n->addr->ai_socktype, n->addr->ai_protocol)) == -1) {
		vlprintf(CN_ERROR "Unable to reconnect to host %s",n->host);
		return 1;
	}

	/* connect to server */
	if (connect(n->fd, n->addr->ai_addr, n->addr->ai_addrlen)) {
		shutdown(n->fd,2);
		vlprintf(CN_ERROR "Unable to reconnect to host %s",n->host);
		return 1;
	}

	n->state = NETSTATE_OPEN;
	n->tries = 0;

	return 0;
}

/* create a new tcp server connection */
net_connection_t *tcp_host_connect(char* host, char* port)
{
	net_connection_t *n = net_connection();
	if (!host)
		host = "*";

	n->type = NETTYPE_TCP_HOST;
	n->tries = 0;
	n->host = strdup(host);
	n->port = strdup(port);
	n->peers = array_create(ARRAY_TYPE_PTR);


	if (tcp_host_reconnect(n)) {
		net_close(n);
		return NULL;
	}

	return n;
}

/* start listening for tcp clients */
int tcp_host_reconnect(net_connection_t *n)
{
	int max_retries = config_get_int("net.max_retries");
	if (!max_retries)
		max_retries = 5;
	if (n->state == NETSTATE_OPEN || n->tries > max_retries)
		return 2;

	n->tries++;

	if (n->state == NETSTATE_UNUSED) {
		char* nhost = NULL;
		int f = config_get_int("net.ipv4");
		int s = config_get_int("net.ipv6");

		memset(&n->hints, 0, sizeof(n->hints));
		if (s == f) {
			n->hints.ai_family = AF_UNSPEC;
		}else if (s) {
			n->hints.ai_family = AF_INET6;
		}else if (f) {
			n->hints.ai_family = AF_INET;
		}
		n->hints.ai_socktype = SOCK_STREAM;
		n->hints.ai_flags = AI_PASSIVE;

		if (strcmp(n->host,"*"))
			nhost = n->host;

		/* resolve hostname */
		if (getaddrinfo(nhost, n->port, &n->hints, &n->addr)) {
			vlprintf(CN_ERROR "Unable to resolve host '%s'",n->host);
			return 1;
		}

		n->state = NETSTATE_CLOSED;
	}

	/* open socket */
	if ((n->fd = socket(n->hints.ai_family, n->hints.ai_socktype, n->hints.ai_protocol)) == -1) {
		vlprintf(CN_ERROR "Unable to open port %u",n->port);
		return 1;
	}
	/* bind to the socket */
	if ((bind(n->fd, n->addr->ai_addr,sizeof(*n->addr->ai_addr))) < 0) {
		vlprintf(CN_ERROR "Unable to bind port %u",n->port);
		return 1;
	}
	if (listen(n->fd,64) < 0) {
		vlprintf(CN_ERROR "Unable to listen on port %u",n->port);
		return 1;
	}

	n->state = NETSTATE_OPEN;
	n->tries = 0;

	return 0;
}

/* write data to a tcp connection */
int tcp_write(net_connection_t *n, void *buff, unsigned int size)
{
	int r = 0;
	if (n->state == NETSTATE_OPEN) {
		r = (int)write(n->fd,buff,size);
		if (r < 1) {
			vlprintf(CN_ERROR "failed to write to connection %d (%d)",n->fd,errno);
			shutdown(n->fd,2);
			n->state = NETSTATE_CLOSED;
		}
	}
	return r;
}

static int tcp_fill_buff(net_connection_t *n)
{
	unsigned char buff[1024];
	int r;
	int l = n->buff.tcp.end-n->buff.tcp.start;
	if (!n->buff.tcp.start && l == 1024)
		return 0;

	memcpy(buff,n->buff.tcp.buff+n->buff.tcp.start,l);

	errno = 0;
	r = (int)recv(n->fd,buff,1024-l,0);
	if (r < 1) {
#ifndef WIN32
		if (errno != EAGAIN && errno != EWOULDBLOCK) {
			shutdown(n->fd,2);
			n->state = NETSTATE_CLOSED;
		}
#else
		shutdown(n->fd,2);
		n->state = RTG_NET_CLOSED;
#endif
	}else{
		l += r;
	}

	memcpy(n->buff.tcp.buff,buff,l);

	n->buff.tcp.start = 0;
	n->buff.tcp.end = l;

	return 0;
}

/* determine if data is pending on a tcp connection */
int tcp_pending(net_connection_t *n)
{
	if (n->state != NETSTATE_OPEN)
		return 0;

	tcp_fill_buff(n);
	if (n->buff.tcp.end > n->buff.tcp.start)
		return 1;
	return 0;
}

/* read data from a tcp connection */
int tcp_read(net_connection_t *n, void *buff, unsigned int size)
{
	if (n->state != NETSTATE_OPEN)
		return 0;

	tcp_fill_buff(n);
	if (size < n->buff.tcp.end)
		size = n->buff.tcp.end;

	if (size < 1)
		return 0;

	memcpy(n->buff.tcp.buff,buff,size);
	n->buff.tcp.start += size;

	return size;
}

/* read a line from a tcp connection */
int tcp_readline(net_connection_t *n, void *buf, unsigned int size)
{
	unsigned int i;
	char* buff = buf;
	if (n->state != NETSTATE_OPEN)
		return 0;

	for (i=0; i<size; i++) {
		if (n->buff.tcp.start >= n->buff.tcp.end)
			tcp_fill_buff(n);
		if (n->buff.tcp.start >= n->buff.tcp.end)
			break;

		buff[i] = n->buff.tcp.buff[n->buff.tcp.start++];
		if (buff[i] == '\n') {
			buff[i] = 0;
			break;
		}else if (i > size-2) {
			buff[i+1] = 0;
			break;
		}
	}
	return i;
}

/* broadcast data to all of a tcp host's peers */
int tcp_broadcast(net_connection_t *n, void *buff, unsigned int size)
{
	int i;
	net_connection_t **p;
	if (!n || n->state != NETSTATE_OPEN || !n->peers || !n->peers->length)
		return 0;

	p = n->peers->data;
	for (i=0; i<n->peers->length; i++) {
		if (!p[i])
			continue;
		tcp_write(p[i],buff,size);
	}

	return i;
}

/* accept new connection to a tcp host */
int tcp_accept(net_connection_t *n)
{
	net_connection_t *c;
	array_t *a = net_select(0,0,n);
	if (!a)
		return -1;

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

	c = net_connection();
	c->type = n->type;
	c->state = NETSTATE_OPEN;

	c->fd = accept(n->fd, (struct sockaddr *)&c->remote_addr, &c->remote_addr_len);

	if (c->fd > -1) {
		int i;
		net_connection_t **p = n->peers->data;
		for (i=0; i<n->peers->length; i++) {
			if (!p[i]) {
				p[i] = c;
				return i;
			}
		}
		array_push_ptr(n->peers,c);
		return n->peers->length-1;
	}

	net_close(c);
	return -1;
}
