#include "extremite.h"

#include <arpa/inet.h>
#include <fcntl.h>
#include <linux/if.h>
#include <linux/if_tun.h>
#include <netdb.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>


#define BUFSIZE 1024


static int setup_out(int port) {
	/* Create the socket. */
	int server = socket(AF_INET6, SOCK_STREAM, 0);
	if (server == -1) {
		perror("socket");
		exit(1);
	}

	/* Enable socket re-use, makes debugging easier. */
	setsockopt(server, SOL_SOCKET, SO_REUSEADDR, &(int) {1}, sizeof (int));

	/* Bind to any local IPv6 address on port PORT. */
	struct sockaddr_in6 server_addr = {0};
	server_addr.sin6_family = AF_INET6;
	server_addr.sin6_port = htons(port);
	server_addr.sin6_addr = in6addr_any;
	if (bind(server, (struct sockaddr *) &server_addr, sizeof server_addr) == -1) {
		perror("bind");
		exit(1);
	}

	/* Pretty print the matched address. */
	char server_addr_pretty[INET6_ADDRSTRLEN] = "";
	inet_ntop(AF_INET6, &(server_addr.sin6_addr), server_addr_pretty, sizeof server_addr_pretty);
	printf("Écoute sur : %s\n", server_addr_pretty);

	if (listen(server, 1) == -1) {
		perror("listen");
		exit(1);
	}

	return server;
}


static int accept_client(int server) {
	/* Wait for a connection attempt. */
	struct sockaddr_in6 client_addr;
	socklen_t client_addr_len;
	puts("Attente d’un client.");
	int client = accept(server, (struct sockaddr *) &client_addr, &client_addr_len);

	/* We only serve one peer, no need to keep the server socket
	 * open. */
	close(server);

	/* Pretty print the peer’s address */
	char client_addr_pretty[INET6_ADDRSTRLEN] = "";
	inet_ntop(AF_INET6, &(client_addr.sin6_addr), client_addr_pretty, sizeof client_addr_pretty);
	printf("Client connecté : %s\n", client_addr_pretty);

	return client;
}


int ext_out(int port) {
	int server = setup_out(port);
	return accept_client(server);
}



static int setup_in() {
	int s = socket(AF_INET6, SOCK_STREAM, 0);
	if (s == -1) {
		perror("socket");
		exit(1);
	}

	return s;
}


static int try_connect(int s, const char addr[], int port) {
	struct sockaddr_in6 sa = {0};
	sa.sin6_family = AF_INET6;
	sa.sin6_port = htons(port);
	inet_pton(AF_INET6, addr, &sa.sin6_addr);

	puts("Connexion.");
	if (connect(s, (struct sockaddr *) &sa, sizeof sa) == -1) {
		perror("connect");
		return 0;
	}

	char addr_pretty[INET6_ADDRSTRLEN] = "";
	inet_ntop(AF_INET6, &(sa.sin6_addr), addr_pretty, sizeof addr_pretty);
	printf("Connecté à : %s\n", addr_pretty);

	return 1;
}


int ext_in(const char addr[], int port) {
	int s = setup_in();

	/* Attempt a connection every second (waiting for the server to
	 * start). */
	while (!try_connect(s, addr, port)) {
		sleep(1);
	}
	return s;
}



/* On établie ici une connexion bidirectionelle en se connectant à un
 * serveur distant ET en écoutant nous-même sur un port local en
 * attente d’un client. Cela nous donne donc deux connexions
 * matérialisées par deux socket, l’une qui est utilisée en lecture et
 * l’autre en écriture.
 *
 * Il pourrait suffir d’avoir une extremité qui écoute, et l’autre qui
 * se connecte, établissant ainsi une seule connexion qu’on
 * utiliserait en lecture/écriture, mais cela pose le problème de
 * savoir qui écoute et qui se connecte.
 *
 * De plus, si les deux tentent en parallèle de se connecter OU
 * d’accepter une connexion (mais de ne pas attendre que les deux
 * conditions soient vraies, et d’utiliser une seule socket en
 * lecture/écriture, la première disponible), se posent tout un tas de
 * problèmes de synchronisation si les deux extrémités du tunnel
 * essaient de se connecter en même temps par exemple.
 */

void ext_bidir(const char addr[], int port, int local_port, int local_r, int local_w) {
	/* First, let’s establish a full-duplex connection. */
	int server = setup_out(local_port);
	int peer_r; /* Future remote client socket to read from. */
	int peer_w = setup_in(addr, port);

	int client_connected = 0;
	int server_connected = 0;

	struct timeval one_sec = {.tv_sec=1, .tv_usec=0};
	struct timeval *timeout = &one_sec;

	fd_set set;
	while (!(client_connected && server_connected)) {
		if (!client_connected) {
			FD_ZERO(&set);
			FD_SET(server, &set);
		}

		select(server + 1, &set, NULL, NULL, timeout);

		if (FD_ISSET(server, &set)) {
			/* The server socket is readable, a peer is trying to
			 * connect. */
			fputs("acceptation\n", stderr);
			peer_r = accept_client(server);
			FD_CLR(server, &set);
			client_connected = 1;
			fputs("Client connecté.\n", stderr);
		}
		else {
			/* Attempt to connect every second. */
			if (try_connect(peer_w, addr, port)) {
				timeout = NULL;
				server_connected = 1;
				fputs("Connecté au serveur.\n", stderr);
			}
			else {
				timeout->tv_sec = 1;
				timeout->tv_usec = 0;
			}
		}
	}

	fputs("Connexion bidirectionelle établie.\n", stderr);

	fd_set rfd;

	int nfds = peer_r > local_r ? peer_r : local_r;
	nfds++;

	char in_buf[1024];
	char out_buf[1024];

	while (1) {
		FD_ZERO(&rfd);
		FD_SET(peer_r, &rfd);
		FD_SET(local_r, &rfd);

		select(nfds, &rfd, NULL, NULL, NULL);

		if (FD_ISSET(peer_r, &rfd)) {
			/* Ready to read from client. */
			ssize_t n = read(peer_r, in_buf, sizeof in_buf);
			if (n < 0) {
				perror("read (from out)");
				exit(1);
			}
			if (write(local_w, in_buf, n) < 0) {
				perror("write (to local_out)");
				exit(1);
			}
		}
		if (FD_ISSET(local_r, &rfd)) {
			/* Ready to read from tun dev. */
			ssize_t n = read(local_r, out_buf, sizeof out_buf);
			if (n < 0) {
				perror("read (from local_in)");
				exit(1);
			}
			if (write(peer_w, out_buf, n) < 0) {
				perror("write (to in)");
				exit(1);
			}
		}
	}
}
