/*
  msock.c - multicast socket creation routines

  (C) 2016 Christian Beier <dontmind@sdf.org>

*/

#ifndef __MINGW32__
#include <unistd.h>
#endif
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

#include "msock.h"

SOCKET mcast_send_socket(char* multicastIP, char* multicastPort,  int multicastTTL, struct addrinfo **multicastAddr) {

    SOCKET sock;
    struct addrinfo hints = { 0 };    /* Hints for name lookup */
    
    
#ifdef WIN32
    WSADATA trash;
    if(WSAStartup(MAKEWORD(2,0),&trash)!=0)
	DieWithError("Couldn't init Windows Sockets\n");
#endif

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

   

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

    /* 
       Set TTL of multicast packet 
    */
    if ( setsockopt(sock,
		    (*multicastAddr)->ai_family == PF_INET6 ? IPPROTO_IPV6        : IPPROTO_IP,
		    (*multicastAddr)->ai_family == PF_INET6 ? IPV6_MULTICAST_HOPS : IP_MULTICAST_TTL,
		    (char*) &multicastTTL, sizeof(multicastTTL)) != 0 ) {
	perror("setsockopt() failed");
	freeaddrinfo(*multicastAddr);
	return -1;
    }
    
    
    /* 
       set the sending interface 
    */
    if((*multicastAddr)->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(*multicastAddr);
	    return -1;
	}

    }
    if((*multicastAddr)->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(*multicastAddr);
	    return -1;
	}   
	 
    }

     
    return sock;

}



SOCKET mcast_recv_socket(char* multicastIP, char* multicastPort, int multicastRecvBufSize) {

    SOCKET sock;
    struct addrinfo   hints  = { 0 };    /* Hints for name lookup */
    struct addrinfo*  localAddr = 0;         /* Local address to bind to */
    struct addrinfo*  multicastAddr = 0;     /* Multicast Address */
    int yes=1;
  
#ifdef WIN32
    WSADATA trash;
    if(WSAStartup(MAKEWORD(2,0),&trash)!=0)
	DieWithError("Couldn't init Windows Sockets\n");
#endif

    
    /* Resolve the multicast group address */
    hints.ai_family = PF_UNSPEC;
    hints.ai_flags  = AI_NUMERICHOST;
    int status;
    if ((status = getaddrinfo(multicastIP, NULL, &hints, &multicastAddr)) != 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   = multicastAddr->ai_family;
    hints.ai_socktype = SOCK_DGRAM;
    hints.ai_flags    = AI_PASSIVE; /* Return an address we can bind to */
    if ( getaddrinfo(NULL, multicastPort, &hints, &localAddr) != 0 ) {
	fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(status));
	goto error;
    }
  

    /* Create socket for receiving datagrams */
    if ( (sock = socket(localAddr->ai_family, localAddr->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, localAddr->ai_addr, localAddr->ai_addrlen) != 0 ) {
	perror("bind() failed");
	goto error;
    }

    /* get/set socket receive buffer */
    int optval=0;
    socklen_t optval_len = sizeof(optval);
    int dfltrcvbuf;
    if(getsockopt(sock, SOL_SOCKET, SO_RCVBUF,(char*)&optval, &optval_len) !=0) {
	perror("getsockopt");
	goto error;
    }
    dfltrcvbuf = optval;
    optval = multicastRecvBufSize;
    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, multicastRecvBufSize, optval);

  
    
    
    /* Join the multicast group. We do this seperately depending on whether we
     * are using IPv4 or IPv6. 
     */
    if ( multicastAddr->ai_family  == PF_INET &&  
	 multicastAddr->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*)(multicastAddr->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 ( multicastAddr->ai_family  == PF_INET6 &&
	      multicastAddr->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*)(multicastAddr->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(localAddr)
	freeaddrinfo(localAddr);
    if(multicastAddr)
	freeaddrinfo(multicastAddr);
    
    return sock;

 error:
    if(localAddr)
	freeaddrinfo(localAddr);
    if(multicastAddr)
	freeaddrinfo(multicastAddr);

    return -1;
}

SOCKET ucast_client_socket(char* serverIP, char* serverPort) {

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


#ifdef WIN32
	WSADATA trash;
	if(WSAStartup(MAKEWORD(2,0),&trash)!=0)
		DieWithError("Couldn't init Windows Sockets\n");
#endif


	/*
		 Resolve destination address
	 */
	hints.ai_family   = PF_UNSPEC;
	hints.ai_socktype = SOCK_STREAM;
	hints.ai_flags    = AI_NUMERICHOST;
	int status;
	if ((status = getaddrinfo(serverIP, serverPort, &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;
}

SOCKET ucast_server_socket(char* serverPort, int maxPendingConnections) {

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


#ifdef WIN32
	WSADATA trash;
	if(WSAStartup(MAKEWORD(2,0),&trash)!=0)
		DieWithError("Couldn't init Windows Sockets\n");
#endif


	/*
		 Prepare an addrinfo struct for a local socket
	 */
	hints.ai_family   = PF_INET6;
	hints.ai_socktype = SOCK_STREAM;
	hints.ai_flags    = AI_PASSIVE;
	int status;
	if ((status = getaddrinfo(NULL, serverPort, &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, maxPendingConnections) < 0 ) {
		perror("listen() failed");
		close(sock);
	}

	return sock;
}