/*
 * Copyright 2016 by Ludovic Pouzenc <ludovic@pouzenc.fr>
 *
 * Greatly inspired from msock.h written by Christian Beier <dontmind@sdf.org>
 */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include "sockets.h"

int mcast_recv_socket(char *mcast_ip, char *port, int wanted_so_rcvbuf) {

	int sock;
	struct addrinfo hints  = { 0 };    /* Hints for name lookup */
	struct addrinfo *ai_local = NULL;         /* Local address to bind to */
	struct addrinfo *mcast_ai = NULL;     /* Multicast Address */
	int yes=1;
	int status, optval;
	socklen_t optval_len;
	int dfltrcvbuf;

	/* Resolve the multicast group address */
	hints.ai_family = PF_UNSPEC;
	hints.ai_flags  = AI_NUMERICHOST;
	if ((status = getaddrinfo(mcast_ip, NULL, &hints, &mcast_ai)) != 0) {
		fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(status));
		goto error;
	}

	/*
	 * Get a local address with the same family (IPv4 or IPv6) as our multicast group
	 * This is for receiving on a certain port.
	 */
	hints.ai_family   = mcast_ai->ai_family;
	hints.ai_socktype = SOCK_DGRAM;
	hints.ai_flags    = AI_PASSIVE; /* Return an address we can bind to */
	if ( getaddrinfo(NULL, port, &hints, &ai_local) != 0 ) {
		fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(status));
		goto error;
	}

	/* Create socket for receiving datagrams */
	if ( (sock = socket(ai_local->ai_family, ai_local->ai_socktype, 0)) < 0 ) {
		perror("socket() failed");
		goto error;
	}

	/*
	 * Enable SO_REUSEADDR to allow multiple instances of this
	 * application to receive copies of the multicast datagrams.
	 */
	if (setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,(char*)&yes,sizeof(int)) == -1) {
		perror("setsockopt");
		goto error;
	}

	/* Bind the local address to the multicast port */
	if ( bind(sock, ai_local->ai_addr, ai_local->ai_addrlen) != 0 ) {
		perror("bind() failed");
		goto error;
	}

	/* get/set socket receive buffer */
	optval=0;
	optval_len = sizeof(optval);
	if(getsockopt(sock, SOL_SOCKET, SO_RCVBUF,(char*)&optval, &optval_len) !=0) {
		perror("getsockopt");
		goto error;
	}
	dfltrcvbuf = optval;
	optval = wanted_so_rcvbuf;
	if(setsockopt(sock,SOL_SOCKET,SO_RCVBUF,(char*)&optval,sizeof(optval)) != 0) {
		perror("setsockopt");
		goto error;
	}
	if(getsockopt(sock, SOL_SOCKET, SO_RCVBUF,(char*)&optval, &optval_len) != 0) {
		perror("getsockopt");
		goto error;
	}
	fprintf(stderr, "tried to set socket receive buffer from %d to %d, got %d\n",
			dfltrcvbuf, wanted_so_rcvbuf, optval);


	/* Join the multicast group. We do this seperately depending on whether we
	 * are using IPv4 or IPv6.
	 */
	if ( mcast_ai->ai_family  == PF_INET &&
			mcast_ai->ai_addrlen == sizeof(struct sockaddr_in) ) /* IPv4 */
	{
		struct ip_mreq multicastRequest;  /* Multicast address join structure */

		/* Specify the multicast group */
		memcpy(&multicastRequest.imr_multiaddr,
				&((struct sockaddr_in*)(mcast_ai->ai_addr))->sin_addr,
				sizeof(multicastRequest.imr_multiaddr));

		/* Accept multicast from any interface */
		multicastRequest.imr_interface.s_addr = htonl(INADDR_ANY);

		/* Join the multicast address */
		if ( setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, (char*) &multicastRequest, sizeof(multicastRequest)) != 0 ) {
			perror("setsockopt() failed");
			goto error;
		}
	}
	else if ( mcast_ai->ai_family  == PF_INET6 &&
			mcast_ai->ai_addrlen == sizeof(struct sockaddr_in6) ) /* IPv6 */
	{
		struct ipv6_mreq multicastRequest;  /* Multicast address join structure */

		/* Specify the multicast group */
		memcpy(&multicastRequest.ipv6mr_multiaddr,
				&((struct sockaddr_in6*)(mcast_ai->ai_addr))->sin6_addr,
				sizeof(multicastRequest.ipv6mr_multiaddr));

		/* Accept multicast from any interface */
		multicastRequest.ipv6mr_interface = 0;

		/* Join the multicast address */
		if ( setsockopt(sock, IPPROTO_IPV6, IPV6_ADD_MEMBERSHIP, (char*) &multicastRequest, sizeof(multicastRequest)) != 0 ) {
			perror("setsockopt() failed");
			goto error;
		}
	}
	else {
		perror("Neither IPv4 or IPv6");
		goto error;
	}


	if(ai_local)
		freeaddrinfo(ai_local);
	if(mcast_ai)
		freeaddrinfo(mcast_ai);

	return sock;

error:
	if(ai_local)
		freeaddrinfo(ai_local);
	if(mcast_ai)
		freeaddrinfo(mcast_ai);

	return -1;
}


int mcast_send_socket(char* mcast_ip, char* port,  int multicastTTL, struct addrinfo **mcast_ai) {

	int sock;
	struct addrinfo hints = { 0 };    /* Hints for name lookup */
	int status;


	/* Resolve destination address for multicast datagrams */
	hints.ai_family   = PF_UNSPEC;
	hints.ai_socktype = SOCK_DGRAM;
	hints.ai_flags    = AI_NUMERICHOST;
	if ((status = getaddrinfo(mcast_ip, port, &hints, mcast_ai)) != 0 )
	{
		fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(status));
		return -1;
	}

	/* Create socket for sending multicast datagrams */
	if ( (sock = socket((*mcast_ai)->ai_family, (*mcast_ai)->ai_socktype, 0)) < 0 ) {
		perror("socket() failed");
		freeaddrinfo(*mcast_ai);
		return -1;
	}

	/* Set TTL of multicast packet */
	if ( setsockopt(sock,
				(*mcast_ai)->ai_family == PF_INET6 ? IPPROTO_IPV6        : IPPROTO_IP,
				(*mcast_ai)->ai_family == PF_INET6 ? IPV6_MULTICAST_HOPS : IP_MULTICAST_TTL,
				(char*) &multicastTTL, sizeof(multicastTTL)) != 0 ) {
		perror("setsockopt() failed");
		freeaddrinfo(*mcast_ai);
		return -1;
	}

	/* set the sending interface */
	if((*mcast_ai)->ai_family == PF_INET) {
		in_addr_t iface = INADDR_ANY; /* well, yeah, any */
		if(setsockopt (sock,
					IPPROTO_IP,
					IP_MULTICAST_IF,
					(char*)&iface, sizeof(iface)) != 0) {
			perror("interface setsockopt() sending interface");
			freeaddrinfo(*mcast_ai);
			return -1;
		}

	}
	if((*mcast_ai)->ai_family == PF_INET6) {
		unsigned int ifindex = 0; /* 0 means 'default interface'*/
		if(setsockopt (sock,
					IPPROTO_IPV6,
					IPV6_MULTICAST_IF,
					(char*)&ifindex, sizeof(ifindex)) != 0) {
			perror("interface setsockopt() sending interface");
			freeaddrinfo(*mcast_ai);
			return -1;
		}

	}

	return sock;
}


int ucast_server_socket(char* port, int max_pending_conn) {

	int sock;
	struct addrinfo *serverAddr;
	struct addrinfo hints = { 0 };    /* Hints for name lookup */
	int status;


	/* Prepare an addrinfo struct for a local socket */
	hints.ai_family   = PF_INET6;
	hints.ai_socktype = SOCK_STREAM;
	hints.ai_flags    = AI_PASSIVE;
	if ((status = getaddrinfo(NULL, port, &hints, &serverAddr)) != 0 )
	{
		fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(status));
		return -1;
	}
	/* Create socket */
	if ( (sock = socket(serverAddr->ai_family, serverAddr->ai_socktype, serverAddr->ai_protocol)) < 0 ) {
		perror("socket() failed");
		freeaddrinfo(serverAddr);
		return -1;
	}

	/* Accepts also IPv4 traffic if the socket is INET6 */
	if(serverAddr->ai_family == PF_INET6) {
		unsigned int no = 0;
		if(setsockopt (sock,
					IPPROTO_IPV6,
					IPV6_V6ONLY,
					(char*)&no, sizeof(no)) != 0) {
			perror("setsockopt() !IPV6_V6ONLY failed");
			freeaddrinfo(serverAddr);
			return -1;
		}
	}

	/* Bind socket to local address/port */
	if ( bind(sock, serverAddr->ai_addr, serverAddr->ai_addrlen) < 0 ) {
		perror("bind() failed");
		close(sock);
		freeaddrinfo(serverAddr);
		return -1;
	}

	freeaddrinfo(serverAddr);

	/* Start listening incoming connections */
	if ( listen(sock, max_pending_conn) < 0 ) {
		perror("listen() failed");
		close(sock);
	}

	return sock;
}

int ucast_client_socket(char* server_ip, char* port) {

	int sock;
	struct addrinfo *serverAddr;
	struct addrinfo hints = { 0 };    /* Hints for name lookup */
	int status;

	/* Resolve destination address */
	hints.ai_family   = PF_UNSPEC;
	hints.ai_socktype = SOCK_STREAM;
	hints.ai_flags    = AI_NUMERICHOST;
	if ((status = getaddrinfo(server_ip, port, &hints, &serverAddr)) != 0 )
	{
		fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(status));
		return -1;
	}

	/* Create socket */
	if ( (sock = socket(serverAddr->ai_family, serverAddr->ai_socktype, 0)) < 0 ) {
		perror("socket() failed");
		freeaddrinfo(serverAddr);
		return -1;
	}

	/* Connect it to the remote server */
	if ( connect(sock, serverAddr->ai_addr, serverAddr->ai_addrlen) < 0 ) {
		perror("connect() failed");
		close(sock);
		freeaddrinfo(serverAddr);
		return -1;
	}

	freeaddrinfo(serverAddr);
	return sock;
}