/* Ken Bass (kbass@kenbass.com) 09/13/2003
**
** MythTV UDP Relay
**
*/

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdio.h>
#include <sys/timeb.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <getopt.h>
#include <termios.h>
#include <libxml/tree.h>
#include <libxml/parser.h>
#include <libxml/xmlmemory.h>
#include <libxml/debugXML.h>
#include <libxml/HTMLtree.h>
#include <libxml/xmlIO.h>
#include <libxml/DOCBparser.h>
#include <libxml/xinclude.h>
#include <libxml/catalog.h>
#include <libxslt/xslt.h>
#include <libxslt/xsltInternals.h>
#include <libxslt/transform.h>
#include <libxslt/xsltutils.h>

#define UDP_RCV_BUFSIZE (64*1024)

int verbose = 0;
char xml_filename[255];

typedef struct 
{
  int fd;
  int port;
  struct sockaddr_in addr;
  char bcast_addr[16];
  int bcast_addr_hex;
} SOCKET_HANDLE;

typedef struct 
{

  SOCKET_HANDLE rcv_sock_handle;
  SOCKET_HANDLE snd_sock_handle;
  char udp_rcv_buffer[UDP_RCV_BUFSIZE];
  char xml_output_buffer[UDP_RCV_BUFSIZE];
  char xslt_filename[255];
} GLOBALS;

int ipaddr_to_int(const char *stringIP, unsigned int *ip)
{
  int a,b,c,d;
  
  if (4 != sscanf(stringIP, "%d.%d.%d.%d", &a, &b, &c, &d))
    {
      return -1;
    }
  
  *ip = ((a&0xFF) << 24) +
    ((b&0xFF) << 16) +
    ((c&0xFF) <<  8) +
    (d&0xFF);
  
  return 0;
}

int setup_rcv_socket(SOCKET_HANDLE* sock, int port)
{
  int yes = 1;
  sock->port = port;
  
  /* create what looks like an ordinary UDP socket */
  if ( (sock->fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0 )
    {
      perror("socket");
      return -1;
    }
  
  /* set up destination address */
  sock->addr.sin_family = AF_INET;
  sock->addr.sin_addr.s_addr = htonl(INADDR_ANY);
  sock->addr.sin_port = htons(port);
  
  /* set up for reusable broadcast port */
  if (setsockopt(sock->fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int) ) < 0)
    {
      perror("setsockopt");
      return -2;
    }
  
  /* bind to receive address */
  if ( bind(sock->fd, (struct sockaddr*) &(sock->addr), sizeof(sock->addr)) < 0 )
    {
      perror("bind");
      return -3;
    }
  
  return 0;
}

int setup_snd_socket(SOCKET_HANDLE* sock, int port, char *addr)
{
  int yes = 1;

  strcpy(sock->bcast_addr, addr);
  if (ipaddr_to_int(sock->bcast_addr, &sock->bcast_addr_hex) < 0)
  {
    printf("Error in Broadcast address %s\n",
	   sock->bcast_addr);
    return -1;
  }

  sock->port = port;

  /* create what looks like an ordinary UDP socket */
  if ( (sock->fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0 )
    {
      perror("socket");
      return -1;
    }
  
  /* set up destination address */
  sock->addr.sin_family = AF_INET;
  sock->addr.sin_addr.s_addr = htonl(sock->bcast_addr_hex);
  sock->addr.sin_port = htons(sock->port);
  
  /* set up for reusable broadcast port */
  if (setsockopt(sock->fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int) ) < 0)
    {
      perror("setsockopt");
      return -2;
    }

  /* setsockopt */
  if (setsockopt(sock->fd, SOL_SOCKET, SO_BROADCAST, &yes, sizeof(int) ) < 0) 
    {
      perror("so_broadcast");
      return -3;
    }
 
  return 0;
}

int send_xml_output(GLOBALS *globals, SOCKET_HANDLE* sock)
{
  int rc;

  /* now just sendto() our destination! */
  rc = sendto(sock->fd,
	      globals->xml_output_buffer,
	      strlen(globals->xml_output_buffer),0,(struct sockaddr *) &sock->addr,
	      sizeof(sock->addr));
  if (rc < 0)
    {
      perror("sendto");
      return -1;
    }
  else
    {
      if (verbose)
      {
	printf("Sent UDP/XML packet to IP %s port %d (%d bytes)\n", 
	       sock->bcast_addr, sock->port, rc);
      }
    }

  return 0;
  
} 

/* Transform received XML data using XSL */
int process_udp_xml(GLOBALS *globals)
{
  xsltStylesheetPtr cur = NULL;
  xmlDocPtr doc, res;
  xmlChar *xml_output;
  int xml_output_len;
  int rc;

  xmlSubstituteEntitiesDefault(1);
  xmlLoadExtDtdDefaultValue = 1;
  cur = xsltParseStylesheetFile((const xmlChar *)globals->xslt_filename);
  if (cur == NULL)
    {
      xsltCleanupGlobals();
      xmlCleanupParser();
      return -1;
    }

  doc = xmlParseMemory(globals->udp_rcv_buffer, UDP_RCV_BUFSIZE);
  if (doc == NULL)
    {
      xsltFreeStylesheet(cur);
      xsltCleanupGlobals();
      xmlCleanupParser();
      return -1;
    }
  res = xsltApplyStylesheet(cur, doc, NULL);
  if (res == NULL)
    {
      xmlFreeDoc(res);
      xsltFreeStylesheet(cur);
      xsltCleanupGlobals();
      xmlCleanupParser();
      return -1;
    }

  rc = xsltSaveResultToString(&xml_output,
				&xml_output_len,
				res,
				cur);

  /* If transformation is empty - error. Its possible the incoming
  ** XML was not an event the XSLT handles */
  if ((rc < 0) || (xml_output_len == 0))
  {
    
    xsltFreeStylesheet(cur);
    xmlFreeDoc(res);
    xmlFreeDoc(doc);
    
    xsltCleanupGlobals();
    xmlCleanupParser();
    
    return -1;
  }

  strncpy(globals->xml_output_buffer, xml_output, UDP_RCV_BUFSIZE-1);
  
  xmlFree(xml_output);

  xsltFreeStylesheet(cur);
  xmlFreeDoc(res);
  xmlFreeDoc(doc);
  
  xsltCleanupGlobals();
  xmlCleanupParser();

  return 0;
}

int waitfor_udp(GLOBALS *globals, SOCKET_HANDLE* sock)
{
  int  nbytes;       /* Number of bytes read */
  fd_set udp_rd_set;
  int rv;

  FD_ZERO(&udp_rd_set);
  FD_SET (sock->fd, &udp_rd_set);

  memset (globals->udp_rcv_buffer, 0, UDP_RCV_BUFSIZE);

  rv = select(sock->fd+1, &udp_rd_set, NULL, NULL, NULL);
  if (rv < 0)
  {
    printf("Select failed\n");
    return -1;
  }

  if (FD_ISSET(sock->fd, &udp_rd_set))
    {
      nbytes = read(sock->fd, globals->udp_rcv_buffer, UDP_RCV_BUFSIZE);
      if (nbytes <= 0)
      {
	return -1;
      }
      else
      {
	if (verbose)
	{
	  printf("got UDP data (%d bytes)\n", nbytes);
	}
      }
    }

  return 0;
}

void print_help(char *progname)
{
  printf("\nUsage: %s [OPTION]\n", progname);
  printf("A caller id UDP broadcast utility for MythTV notify.\n\n");
  printf("  -i, --udpport_in  : UDP port to monitor (--udpport_in=6947)\n");
  printf("  -o, --udpport_out : UDP port to monitor (--udpport_out=6948)\n");
  printf("  -b, --bcast     : UDP broadcast address (--bcast=255.255.255.255)\n");
  printf("  -x, --xslfile   : XSL file to use as a template (--xslfile=cid.xml)\n");
  printf("  -v, --verbose   : some verbose debug stuff\n");
  printf("\nAn XSL (XSLT) file is required - it is used to transform the incoming\n");
  printf("XML to the output XML (mythnotify)\n");
}

int main(int argc, char *argv[])
{
  int file_arg_found = 0;
  static GLOBALS globals;
  int done = 0;
  int option_index = 0, c;
  int rcv_udp_port;
  int snd_udp_port;
  char bcast_addr[16];
  int bcast_addr_hex;

  static struct option long_options[] = 
  {
    {"udpport_in", optional_argument, 0, 'i'},
    {"udpport_out", optional_argument, 0, 'o'},
    {"bcast", optional_argument, 0, 'b'},
    {"xslfile", required_argument, 0, 'x'},
    {"help", no_argument, 0, 'h'},
    {"verbose", no_argument, 0, 'v'},
    {0, 0, 0, 0}
  };

  /* Defaults */
  strcpy(bcast_addr, "255.255.255.255");
  ipaddr_to_int(bcast_addr, &bcast_addr_hex);
  rcv_udp_port = 6947;
  snd_udp_port = 6948;
  strcpy(globals.xslt_filename, "");

  while (1) 
  {
    c = getopt_long (argc, argv, "i:o:b:x:hv",
		     long_options, &option_index);
    if (c == -1)
      break;
    
    switch (c) 
    {
      
      case 'i':
	rcv_udp_port = atoi(optarg);
	break;

      case 'o':
	snd_udp_port = atoi(optarg);
	break;

      case 'b':
	strncpy(bcast_addr, optarg, sizeof(bcast_addr));
	if (ipaddr_to_int(bcast_addr, &bcast_addr_hex) < 0)
	{
	  printf("Error in Broadcast address %s\n",
		 bcast_addr);
	  exit(1);
	}
	break;

      case 'x':
	strncpy(globals.xslt_filename, optarg, sizeof(globals.xslt_filename)-1);
	file_arg_found = 1;
	break;

      case 'h':
	print_help(argv[0]);
	exit(0);
	break;	

      case 'v':
	verbose = 1;
	printf("Verbose mode enabled\n");
	break;

      default:
	print_help(argv[0]);
	exit(0);
    }
  }

  if (!file_arg_found)
  {
    printf("No xslfile provided; --xslfile=file.xsl arg required\n");
    return -1;
  }

  if (rcv_udp_port == snd_udp_port)
    {
      printf("UDP input and output ports cannot be the same\n");
      return -1;
    }

  if (setup_rcv_socket(&globals.rcv_sock_handle, rcv_udp_port) < 0)
  {
    printf("error in setup_rcv_sockets\n");
    return -1;
  }

  if (setup_snd_socket(&globals.snd_sock_handle, 
		       snd_udp_port, bcast_addr) < 0)
  {
    printf("error in setup_snd_sockets\n");
    return -1;
  }

  done = 0;
  while (!done)
  {
    if (waitfor_udp(&globals, &globals.rcv_sock_handle) >= 0)
      {

	if (process_udp_xml(&globals) >= 0)
	{

	  if (verbose)
	  {
	    printf("%s", globals.xml_output_buffer);
	  }
	  
	  send_xml_output(&globals, &globals.snd_sock_handle);
	}
      }
  }

  return 0;
}
