#include <gio/gio.h>
#include <gio/gnetworking.h>
#include <errno.h>
#include <stdlib.h>

#define MULTICAST_GROUP "224.0.0.99"

gboolean use_tcp, use_multicast, use_broadcast;
gboolean client, listening, reuseaddr, reuseport;

static GOptionEntry entries[] = {
  { "client", 'c', 0, G_OPTION_ARG_NONE, &client, "Act as client", NULL },
  { "tcp", 't', 0, G_OPTION_ARG_NONE, &use_tcp, "Use TCP", NULL },
  { "multicast", 'm', 0, G_OPTION_ARG_NONE, &use_multicast, "Use multicast", NULL },
  { "broadcast", 'b', 0, G_OPTION_ARG_NONE, &use_broadcast, "Use broadcast", NULL },
  { "listen", 'l', 0, G_OPTION_ARG_NONE, &listening, "Listening server", NULL },
  { "reuseaddr", 'a', 0, G_OPTION_ARG_NONE, &reuseaddr, "Set SO_REUSEADDR", NULL },
  { "reuseport", 'p', 0, G_OPTION_ARG_NONE, &reuseport, "Set SO_REUSEPORT", NULL },
  { NULL }
};

int
main (int argc, char **argv)
{
  GOptionContext *opts;
  GError *error = NULL;
  GSocket *sock, *peer = NULL;
  GInetAddress *iaddr;
  GSocketAddress *addr;
  char *addrstr;
  int port;
  char buf[1024];

  opts = g_option_context_new (NULL);
  g_option_context_add_main_entries (opts, entries, NULL);
  if (!g_option_context_parse (opts, &argc, &argv, &error))
    {
      g_printerr ("Could not parse arguments: %s\n",
		  error->message);
      g_printerr ("%s",
		  g_option_context_get_help (opts, TRUE, NULL));
      exit (1);
    }
  g_option_context_free (opts);

  if (use_multicast && use_tcp)
    {
      g_printerr ("-m and -t are mutually exclusive\n");
      exit (1);
    }
  else if (use_broadcast && use_multicast)
    {
      g_printerr ("-b and -m are mutually exclusive\n");
      exit (1);
    }
  else if (client && listening)
    {
      g_printerr ("-c and -l are mutually exclusive\n");
      exit (1);
    }

  sock = g_socket_new (G_SOCKET_FAMILY_IPV4,
		       use_tcp ? G_SOCKET_TYPE_STREAM : G_SOCKET_TYPE_DATAGRAM,
		       G_SOCKET_PROTOCOL_DEFAULT,
		       NULL);
  if (use_broadcast)
    g_socket_set_broadcast (sock, TRUE);
  
  if (reuseaddr)
    {
      if (!g_socket_set_option (sock, SOL_SOCKET, SO_REUSEADDR, TRUE, &error))
	{
	  g_printerr ("Could not set SO_REUSEADDR: %s\n",
		      error->message);
	  exit (1);
	}
    }
  if (reuseport)
    {
#ifdef SO_REUSEPORT
      if (!g_socket_set_option (sock, SOL_SOCKET, SO_REUSEPORT, TRUE, &error))
	{
	  g_printerr ("Could not set SO_REUSEPORT: %s\n",
		      error->message);
	  exit (1);
	}
#else
      g_printerr ("No SO_REUSEPORT on this platform\n");
      exit (1);
#endif
    }

  if (use_multicast)
    {
      if (listening || client)
	{
	  iaddr = g_inet_address_new_from_string (MULTICAST_GROUP);
	  if (!g_socket_join_multicast_group (sock, iaddr, FALSE, NULL, &error))
	    {
	      g_printerr ("Could not join multicast group: %s\n",
			  error->message);
	      exit (1);
	    }
	  g_object_unref (iaddr);
	}
      else
	g_socket_set_multicast_loopback (sock, TRUE);
    }

  if (client)
    {
      if (use_multicast)
	iaddr = g_inet_address_new_from_string (MULTICAST_GROUP);
      else
	iaddr = g_inet_address_new_loopback (G_SOCKET_FAMILY_IPV4);
      addr = g_inet_socket_address_new (iaddr, 9999);

      if (!g_socket_connect (sock, addr, NULL, &error))
	{
	  g_printerr ("Could not connect: %s\n",
		      error->message);
	  exit (1);
	}
    }
  else
    {
      struct sockaddr_in sin;

      iaddr = g_inet_address_new_loopback (G_SOCKET_FAMILY_IPV4);

      memset (&sin, 0, sizeof (sin));
      sin.sin_family = AF_INET;
      sin.sin_addr.s_addr = 0;
      sin.sin_port = htons (9999);

      /* We can't use g_socket_bind() here since it might not set the
       * sockopts we wanted.
       */
      if (bind (g_socket_get_fd (sock),
		(struct sockaddr *) &sin,
		sizeof (sin)) < 0)
	{
	  g_printerr ("Could not bind port: %s\n",
		      g_strerror (errno));
	  exit (1);
	}
    }

  addrstr = g_inet_address_to_string (iaddr);
  g_object_unref (iaddr);

  g_print ("%s %s%s%s%s%s%s socket to %s:9999\n",
	   client ? "Connected" : "Bound",
	   listening ? "listening " : "",
	   reuseaddr ? "reuseaddr " : "",
	   reuseport ? "reuseport " : "",
	   use_multicast ? "multicast " : "",
	   use_broadcast ? "broadcast " : "",
	   use_tcp ? "TCP" : "UDP",
	   addrstr);
  g_free (addrstr);

  if (listening && use_tcp && !g_socket_listen (sock, &error))
    {
      g_printerr ("Could not listen: %s\n",
		  error->message);
      exit (1);
    }

  g_socket_set_blocking (sock, TRUE);

  if (listening)
    {
      GSocketAddress *recvaddr;

      while (TRUE)
	{
	  if (use_tcp)
	    {
	      peer = g_socket_accept (sock, NULL, &error);
	      if (peer)
		recvaddr = g_socket_get_remote_address (peer, NULL);
	    }
	  else
	    g_socket_receive_from (sock, &recvaddr, buf, sizeof (buf), NULL, &error);

	  if (error)
	    {
	      g_printerr ("Failed: %s\n", error->message);
	      exit (1);
	    }

	  iaddr = g_inet_socket_address_get_address (G_INET_SOCKET_ADDRESS (recvaddr));
	  port = g_inet_socket_address_get_port (G_INET_SOCKET_ADDRESS (recvaddr));
	  addrstr = g_inet_address_to_string (iaddr);
	  g_print ("got connection from %s:%d\n", addrstr, port);
	  g_free (addrstr);
	  g_object_unref (recvaddr);
	  g_clear_object (&peer);
	}
    }
  else if (client)
    {
      char *data = "data";

      g_socket_send_to (sock, addr, data, strlen (data), NULL, &error);
      if (error)
	{
	  g_printerr ("Failed: %s\n", error->message);
	  exit (1);
	}
    }
  else
    {
      fgets (buf, sizeof (buf), stdin);
    }

  g_socket_close (sock, NULL);
  g_object_unref (addr);

  return 0;
}
