/*
 * Copyright (c) 2016, 2023 Jonas 'Sortie' Termansen.
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 * nc.c
 * Network client and server.
 */

#include <sys/socket.h>

#include <arpa/inet.h>
#include <err.h>
#include <errno.h>
#include <netdb.h>
#include <netinet/in.h>
#include <pthread.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

static int fd;
static char outgoing[65536];
static char incoming[65536];

static void* write_thread(void* ctx)
{
	(void) ctx;
	ssize_t amount;
	while ( 0 < (amount = read(0, outgoing, sizeof(outgoing))) )
	{
		ssize_t sofar = 0;
		while ( sofar < amount )
		{
			// TODO: How to handle EPIPE. Is MSG_NOSIGNAL desirable?
			int flags = MSG_NOSIGNAL;
			ssize_t done = send(fd, outgoing + sofar, amount - sofar, flags);
			if ( done <= 0 )
				err(1, "send");
			sofar += done;
		}
	}
	if ( amount < 0 )
		err(1, "stdin: read");
	if ( shutdown(fd, SHUT_WR) < 0 )
		err(1, "shutdown");
	return NULL;
}

int main(int argc, char* argv[])
{
	bool flag_ipv4 = false;
	bool flag_ipv6 = false;
	bool flag_listen = false;
	bool flag_udp = false;
	bool flag_verbose = false;

	int opt;
	while ( (opt = getopt(argc, argv, "46luv")) != -1 )
	{
		switch ( opt )
		{
		case '4': flag_ipv4 = true; break;
		case '6': flag_ipv6 = true; break;
		case 'l': flag_listen = true; break;
		case 'u': flag_udp = true; break;
		case 'v': flag_verbose = true; break;
		default: return 1;
		}
	}

	if ( argc - optind < 1 )
		errx(1, "No host given");
	if ( argc - optind < 2 )
		errx(1, "No service given");
	if ( argc - optind > 2 )
		errx(1, "Unexpected extra operand: %s", argv[optind + 2]);
	const char* host = argv[optind + 0];
	const char* service = argv[optind + 1];

	if ( 1 < flag_ipv4 + flag_ipv6 )
		errx(1, "The -4 and -6 options are incompatible");

	struct addrinfo hints =
	{
		.ai_flags = flag_listen ? AI_PASSIVE : 0,
		.ai_family = flag_ipv6 ? AF_INET6 : flag_ipv4 ? AF_INET : AF_UNSPEC,
		.ai_socktype = flag_udp ? SOCK_DGRAM : SOCK_STREAM,
		.ai_protocol = 0,
	};
	struct addrinfo* res0 = NULL;
	int status = getaddrinfo(host, service, &hints, &res0);
	if ( status == EAI_SYSTEM )
		err(1, "%s", host);
	if ( status )
		errx(1, "%s: %s", host, gai_strerror(status));
	if ( !res0 )
		errx(1, "%s: %s", host, gai_strerror(EAI_NONAME));

	for ( struct addrinfo* res = res0; res; res = res->ai_next )
	{
		if ( (fd = socket(res->ai_family, res->ai_socktype,
		                  res->ai_protocol)) < 0 )
		{
			if ( res->ai_next )
				continue;
			err(1, "socket");
		}
		if ( flag_listen )
		{
			if ( bind(fd, res->ai_addr, res->ai_addrlen) < 0 )
			{
				close(fd);
				if ( res->ai_next )
					continue;
				err(1, "bind: %s: %s", host, service);
			}
			if ( listen(fd, 1) < 0 )
				err(1, "listen: %s: %s", host, service);
			if ( flag_verbose )
			{
				char host[NI_MAXHOST];
				char serv[NI_MAXSERV];
				if ( getnameinfo(res->ai_addr, res->ai_addrlen,
				                 host, sizeof(host), serv, sizeof(serv),
				                 NI_NUMERICHOST | NI_NUMERICSERV) < 0 )
				{
					strlcpy(host, "unknown", sizeof(host));
					strlcpy(serv, "unknown", sizeof(serv));
				}
				fprintf(stderr, "Listening on %s:%s\n", host, serv);
			}
			// TODO: UDP support.
			struct sockaddr_storage addr;
			socklen_t addrlen = sizeof(addr);
			int nfd = accept(fd, (struct sockaddr*) &addr, &addrlen);
			if ( nfd < 0 )
				err(1, "accept: %s: %s", host, service);
			close(fd);
			fd = nfd;
			if ( flag_verbose )
			{
				char host[NI_MAXHOST];
				char serv[NI_MAXSERV];
				if ( getnameinfo((const struct sockaddr*) &addr, addrlen,
				                 host, sizeof(host), serv, sizeof(serv),
				                 NI_NUMERICHOST | NI_NUMERICSERV) < 0 )
				{
					strlcpy(host, "unknown", sizeof(host));
					strlcpy(serv, "unknown", sizeof(serv));
				}
				fprintf(stderr, "Connection from %s:%s\n", host, serv);
			}
		}
		else
		{
			if ( connect(fd, res->ai_addr, res->ai_addrlen) < 0 )
			{
				close(fd);
				if ( res->ai_next )
					continue;
				err(1, "connect: %s: %s", host, service);
			}
			if ( flag_verbose )
			{
				char host[NI_MAXHOST];
				char serv[NI_MAXSERV];
				if ( getnameinfo(res->ai_addr, res->ai_addrlen,
				                 host, sizeof(host), serv, sizeof(serv),
				                 NI_NUMERICHOST | NI_NUMERICSERV) < 0 )
				{
					strlcpy(host, "unknown", sizeof(host));
					strlcpy(serv, "unknown", sizeof(serv));
				}
				fprintf(stderr, "Connected to %s:%s\n", host, serv);
			}
		}
		break;
	}

	freeaddrinfo(res0);

	pthread_t write_pthread;
	int errnum;
	if ( (errnum = pthread_create(&write_pthread, NULL, write_thread, NULL)) )
	{
		errno = errnum;
		err(1, "pthread_create");
	}

	ssize_t amount;
	while ( 0 < (amount = recv(fd, incoming, sizeof(incoming), 0)) )
	{
		ssize_t sofar = 0;
		while ( sofar < amount )
		{
			// TODO: How to handle if this causes SIGPIPE?
			ssize_t done = write(1, incoming + sofar, amount - sofar);
			if ( done <= 0 )
				err(1, "stdout: write");
			sofar += done;
		}
	}
	if ( amount < 0 )
		err(1, "recv");

	pthread_join(write_pthread, NULL);

	close(fd);

	return 0;
}
