#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <sys/time.h>
#include <fcntl.h>
#include <unistd.h>
#include "scm.h"
#ifdef sun
# include <sys/filio.h>
# include <sys/ioctl.h>
# include <strings.h>   /* for bzero */
#endif

#ifndef STDC_HEADERS
	int close P((int fd));
#else /* added by Denys Duchier */
# ifdef SVR4
#  include <unistd.h>
# endif
#endif /* STDC_HEADERS */

#define MAXLINE 2048

/* SELECT() is BSD select() on the current operating system */
#ifdef hpux /* (fd_set *)s are replaced with (int *)s, for some reason */
#define SELECT(num, rd, wr, ex, to) \
  select(num, (int *)rd, (int *)wr, (int *)ex, to)
#else
#define SELECT(num, rd, wr, ex, to) select(num, rd, wr, ex, to)
#endif

/*******************/
/* udp definitions */
/*******************/
#define MAKUDP(fd, info) cons(tc16_udp, makudp(fd, &info));
#define UDPP(obj) (NIMP(obj) && (TYP16(obj) == tc16_udp))

long tc16_udp;

/* udp object structure */

typedef struct
{
  int fd;
  struct sockaddr_in info;
} udpobj;

SCM makudp(int fd, struct sockaddr_in *info)
     /* allocate and initialize a udp struct */
{
  udpobj *obj = (udpobj *)malloc(sizeof(udpobj));
  obj->fd = fd;
  obj->info = *info; /* make a local copy */
  return (SCM) obj;
}

/* udp -- smob.print */
int printudp(SCM exp, SCM port, int writing)
{
  udpobj *udp = (udpobj *)CDR(exp);
  writing = writing;
  if (udp->fd)
    {
      lputs("#<udp ", port);
      intprint(udp->fd, 10, port);
      lputs(">", port);
    }
  else
    lputs("#<Closed-udp>", port);
  return 1;
}

/* udp -- smob.free */
sizet freeudp(CELLPTR obj)
{
  udpobj *udp = (udpobj *)CDR(obj);
  if (udp->fd)
    close(udp->fd);
  free(udp);
  return sizeof(udpobj);
}

/* udp smob definition */

static smobfuns udpsmob =
{
  mark0,    /* no storage allocated to mark */
  freeudp,  /* custom free */
  printudp, /* custom print */
  0         /* no two upd objects can be the same */
};


/***********************************/
/* "udp:udp?"                      */
/* Is the SCM object a UDP object? */
/***********************************/
static char s_isudp[] = "udp:udp?";
static SCM l_isudp(SCM obj)
{
  return UDPP(obj) ? BOOL_T : BOOL_F;
}

static SCM addrint2string(unsigned long ipnum)
     /*** NOTE: this takes an int in HOST byte order ***/
{
  char *ipname = (char *)malloc(16); /* "aaa.bbb.ccc.ddd0" */
  SCM ret;
  sprintf(ipname, "%d.%d.%d.%d", (int) ipnum>>24, (int) ipnum>>16&0xff,
          (int) ipnum>>8&0xff, (int) ipnum&0xff);/* turn the num into a str */
  ret = makfrom0str(ipname);
  free(ipname);
  return ret;
}

static unsigned long addrstring2int(SCM hostname)
     /*** NOTE: this returns an int in NETWORK byte order ***/
{
  struct hostent *entry;
  DEFER_INTS;
  SYSCALL(entry = gethostbyname(CHARS(hostname)););
  ALLOW_INTS;
  if (!entry)
    return 0;

  return ((struct in_addr *)entry->h_addr)->s_addr;
}

/************************************/
/* "udp:addr"                       */
/* Returns the IP address associated*/
/* with the given UDP object        */
/************************************/
static char s_udp_addr[] = "udp:addr";
static SCM l_udp_addr(SCM udpexp)
{
  udpobj *udp;
  ASSERT(UDPP(udpexp), udpexp, ARG1, s_udp_addr);
  udp = (udpobj *)CDR(udpexp);
  return addrint2string(ntohl(udp->info.sin_addr.s_addr));
}

/************************************/
/* "udp:port"                       */
/* Returns the port associated with */
/* the given UDP object             */
/************************************/
static char s_udp_port[] = "udp:port";
static SCM l_udp_port(SCM udpexp)
{
  udpobj *udp;
  ASSERT(UDPP(udpexp), udpexp, ARG1, s_udp_port);
  udp = (udpobj *)CDR(udpexp);
  return MAKINUM(ntohs(udp->info.sin_port));
}

static char s_udp_fd[] = "udp:fd";
static SCM l_udp_fd(SCM udpexp)
{
  udpobj *udp;
  ASSERT(UDPP(udpexp), udpexp, ARG1, s_udp_fd);
  udp = (udpobj *)CDR(udpexp);
  return MAKINUM(udp->fd);
}

/*****************************************************/
/* "udp:closed?"                                      */
/* Returns true if the UDP socket is closed (fd = 0) */
/* or false if it is open (fd != 0)                  */
/*****************************************************/
static char s_udp_closedp[] = "udp:closed?";
static SCM l_udp_closedp(SCM udpexp)
{
  ASSERT(UDPP(udpexp), udpexp, ARG1, s_udp_closedp);
  return ((udpobj *)CDR(udpexp))->fd ? BOOL_F : BOOL_T;
}

/*********************************************************************/
/* "udp:create"                                                      */
/* Creates a UDP socket bound to port <portnum> on the local machine */
/* If <portnum> is not specified, an arbitrary port is chosen        */
/* Returns a UDP object if successful or false if not                */
/*********************************************************************/
static char s_udp_create[] = "udp:create";
SCM l_udp_create(portnum)
     SCM portnum;
{
  struct sockaddr_in serv_addr;
  int sockfd, sts;

  /* Check portnum for validity */
  if UNBNDP(portnum) portnum = INUM0;
  else ASSERT(INUMP(portnum), portnum, ARG1, s_udp_create);

  /* Open a UDP socket */
  DEFER_INTS;
  SYSCALL(sockfd = socket(AF_INET, SOCK_DGRAM, 0););
  if (-1==sockfd) wta(UNDEFINED, (char *)NALLOC, s_udp_create);
  ALLOW_INTS;

  /* Bind socket to port */
  bzero((char *) &serv_addr, sizeof(serv_addr));
  serv_addr.sin_family = AF_INET;
  serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
  serv_addr.sin_port = htons(INUM(portnum));
  SYSCALL(sts = bind(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)););

  /* Return udp object or false if not succesful */
  return sts ? BOOL_F : MAKUDP(sockfd, serv_addr);
}

/********************************************************************/
/* "udp:close"                                                      */
/* Close the udp socket, sets the fd to 0                           */
/* Returns the UDP object if the close was successful, false if not */
/********************************************************************/
static char s_udp_close[] = "udp:close";
SCM l_udp_close(exp)
     SCM exp;
{
  udpobj *udp;
  int sts;

  ASSERT(UDPP(exp), exp, ARG1, s_udp_close);
  udp = (udpobj *)CDR(exp);
  ASSERT(udp->fd, exp, ARG1, s_udp_close); /* already closed */
  SYSCALL(sts = close(udp->fd););
  if (sts) return BOOL_F;
  udp->fd = 0;
  return exp;
}

/**********************************************************************/
/* "udp:receive"                                                      */
/* Receive data from the specified UDP port                           */
/* Blocks until data is available                                     */
/* Returns a list of (the received data as a string, the remote host, */
/* and the remote port), or false if there was an error in receiving  */
/**********************************************************************/
static char s_udp_receive[] = "udp:receive";
SCM l_udp_receive(udpexp)
     SCM udpexp;
{
  int n, sockfd, addrlen;
  char recvline[MAXLINE];
  struct sockaddr_in remote_addr;

  /* Check port for validity */
  ASSERT(UDPP(udpexp), udpexp, ARG1, s_udp_receive);

  /* Extract needed info */
  sockfd = ((udpobj *)CDR(udpexp)) ->fd;
  
  ASSERT(sockfd, udpexp, ARG1, s_udp_receive); /* Closed, don't read stdin */

  addrlen = sizeof(remote_addr);
  DEFER_INTS;
  n = recvfrom(sockfd, recvline, MAXLINE, 0, 
	       (struct sockaddr *) &remote_addr, &addrlen);
  ALLOW_INTS;
  if (n<0) return BOOL_F;

  /* Return a list of (message, address, port) */
  /* Fix this so it doesn't cost so much       */
  return cons(makfromstr(recvline, n),
	       cons2(addrint2string(ntohl(remote_addr.sin_addr.s_addr)),
		     MAKINUM(ntohs(remote_addr.sin_port)),
		     EOL));
}

/******************************************************/
/* "udp:data-ready?"                                  */
/* Is data ready at the specified UDP port?           */
/* Returns true if data is available, or false if not */
/******************************************************/
static char s_udp_data_ready[] = "udp:data-ready?";
SCM l_udp_data_ready(udpexp)
     SCM udpexp;
{
  int result, sockfd;
  fd_set fds;
  struct timeval timeout;

  /* Check port for validity */
  ASSERT(UDPP(udpexp), udpexp, ARG1, s_udp_data_ready);

  /* Extract needed info */
  sockfd = ((udpobj *)CDR(udpexp)) ->fd;
  
  ASSERT(sockfd, udpexp, ARG1, s_udp_data_ready); /* closed */

  /* Create needed structs */
  timeout.tv_sec = 0;
  timeout.tv_usec = 0;
  FD_ZERO(&fds);
  FD_SET(sockfd, &fds);

  /* Is there data waiting? */
  DEFER_INTS;
  SYSCALL(result = SELECT((1+sockfd), &fds, NULL, NULL, &timeout););
  ALLOW_INTS; 
  return (1 == result) ? BOOL_T : BOOL_F;    /* one readable socket, no error */
}

/**********************************************************************/
/* "udp:receive-nonblocking"                                          */
/* Receive data from the specified UDP port                           */
/* Does not block if data is not available                            */
/* Returns a list of (the received data as a string, the remote host, */
/* and the remote port), or false if there was an error in receiving  */
/* or if no data was available                                        */
/**********************************************************************/
static char s_udp_receive_nb[] = "udp:receive-nonblocking";
SCM l_udp_receive_nb(udpexp)
     SCM udpexp;
{
  int n, sockfd, addrlen;
  char recvline[MAXLINE];
  struct sockaddr_in remote_addr;

  /* Check port for validity */
  ASSERT(UDPP(udpexp), udpexp, ARG1, s_udp_receive_nb);

  /* Extract needed info */
  sockfd = ((udpobj *)CDR(udpexp)) ->fd;

  ASSERT(sockfd, udpexp, ARG1, s_udp_receive_nb); /* closed */

  /* Make the socket non-blocking */
  SYSCALL(fcntl(sockfd, F_SETFL, O_NONBLOCK);); 
  
  /* Get data if available */
  addrlen = sizeof(remote_addr);
  n = recvfrom(sockfd, recvline, MAXLINE, 0,
 	       (struct sockaddr *) &remote_addr, &addrlen);
  if (n<0) return BOOL_F;

  /* Make the socket blocking again.  TEMPORARY */
  SYSCALL(fcntl(sockfd, F_SETFL, NULL);); 

  return cons(makfromstr(recvline, n),
	       cons2(addrint2string(remote_addr.sin_addr.s_addr),
		     MAKINUM(remote_addr.sin_port),
		     EOL));
}


/***************************************/
/* "udp:send"                          */
/* Takes a udp object bound to a port, */
/* and sends the data                  */
/***************************************/
static char s_udp_send[] = "udp:send";
/* Parameters: SCM udpexp, addr, portnum, data; */
SCM l_udp_send(udpexp, addr, args)
     SCM udpexp, addr, args;
{
  SCM portnum, data, vargs, *ve = &vargs;
  struct sockaddr_in remote_addr;
  int n, sockfd, on, ret;

  /* Check for right number of arguments and extract the ones in the list */
  vargs = vector(args);
  ASSERT(LENGTH(vargs)==2, UNDEFINED, WNA, s_udp_send);
  ve = VELTS(vargs);
  portnum = ve[0];
  data = ve[1];

  /* Check arguments for validity */
  ASSERT(UDPP(udpexp), udpexp, ARG1, s_udp_send);
  ASSERT(NIMP(addr) && STRINGP(addr), addr, ARG2, s_udp_send);	/* (INUMP(x) || (NIMP(x) && NUMP(x))) */
  ASSERT(INUMP(portnum), portnum, ARG3, s_udp_send);
  ASSERT(NIMP(data) && STRINGP(data), data, ARG4, s_udp_send);
  
  /* Extract needed objects */
  sockfd = ((udpobj *)CDR(udpexp)) ->fd;

  ASSERT(sockfd, udpexp, ARG1, s_udp_send); /* closed */

  /* Set the socket to allow broadcast packets, since we may be using them */
  on = 1;
  ret = setsockopt (sockfd, SOL_SOCKET, SO_BROADCAST, (int *) &on, sizeof (on));
  if (-1==ret);  /* Ignore the return value.  If we can't set the socket to */
                 /* broadcast, try sending anyway, since it might not be a  */
                 /* broadcast address. */

  /* Fill in structure for server we want to send to */
  bzero((char *) &remote_addr, sizeof(remote_addr));
  remote_addr.sin_family = AF_INET;
  remote_addr.sin_addr.s_addr = addrstring2int(addr);
  remote_addr.sin_port = htons(INUM(portnum));

  /* Send stuff!! */
  n = (sendto(sockfd, CHARS(data), LENGTH(data), 0, 
	      (struct sockaddr *) &remote_addr, sizeof (remote_addr)));
  if (LENGTH(data) == n)
    return BOOL_T; /* send was successful */
  else if (-1 == n)
    return BOOL_F; /* send was unsuccessful */
  else
    return MAKINUM(n); /* send was less than successful */

  /* Note that this does not guarantee that the packets were received;
     UDP is a connectionless protocol. */
}

/****************************************************************/
/* "udp:send-now"                                               */
/* Send, without binding a local port or returning a UDP object */
/* Works, as far as I can tell                                  */
/****************************************************************/
static char s_udp_send_now[] = "udp:send-now";
SCM l_udp_send_now(addr, portnum, data)
     SCM addr, portnum, data;
{
  struct sockaddr_in serv_addr;
  int sockfd, n, on, ret;

  /* Check portnum and hostname and data for validity */
  ASSERT(NIMP(addr) && STRINGP(addr), addr, ARG1, s_udp_send_now);
  ASSERT(INUMP(portnum), portnum, ARG2, s_udp_send_now);
  ASSERT(NIMP(data) && STRINGP(data), data, ARG3, s_udp_send_now);

  /* Fill in structure for server we want to send to */
  bzero((char *) &serv_addr, sizeof(serv_addr));
  serv_addr.sin_family = AF_INET;
  serv_addr.sin_addr.s_addr = addrstring2int(addr);
  serv_addr.sin_port = htons(INUM(portnum));

  /* Open a UDP socket */
  DEFER_INTS;
  sockfd = socket(AF_INET, SOCK_DGRAM, 0);
  if (-1==sockfd) wta(UNDEFINED, (char *)NALLOC, s_udp_send_now);

  /* Set the socket to allow broadcast packets, since we may be using them */
  on = 1;
  ret = setsockopt (sockfd, SOL_SOCKET, SO_BROADCAST, (int *) &on, sizeof (on));
  if (-1==ret);  /* Ignore the return value.  If we can't set the socket to */
                 /* broadcast, try sending anyway, since it might not be a  */
                 /* broadcast address. */
  ALLOW_INTS;

  /* Send stuff!! */
  n = (sendto(sockfd, CHARS(data), LENGTH(data), 0, 
	      (struct sockaddr *) &serv_addr, sizeof (serv_addr)));
  close (sockfd);

  if (LENGTH(data) == n)
    return BOOL_T; /* send was successful */
  else if (-1 == n)
    return BOOL_F; /* send was unsuccessful */
  else
    return MAKINUM(n); /* send was less than successful */

  /* Note that this does not guarantee that the packets were received;
     UDP is a connectionless protocol. */
}


/* Initialization routines */
static iproc subr1o[]=
{
  {s_udp_create, l_udp_create},
  {0, 0}
};

static iproc subr1s[]=
{
  {s_isudp, l_isudp},
  {s_udp_port, l_udp_port},
  {s_udp_addr, l_udp_addr},
  {s_udp_fd, l_udp_fd},
  {s_udp_closedp, l_udp_closedp},
  {s_udp_close, l_udp_close},
  {s_udp_data_ready, l_udp_data_ready},
  {s_udp_receive, l_udp_receive},
  {s_udp_receive_nb, l_udp_receive_nb},
  {0, 0}
};

static iproc subr3s[]=
{
  {s_udp_send_now, l_udp_send_now},
  {0, 0}
};

static iproc lsubr2s[]=
{
  {s_udp_send, l_udp_send},
  {0, 0}
};

void init_udp()
{
  tc16_udp = newsmob(&udpsmob);
  init_iprocs(subr1o, tc7_subr_1o);
  init_iprocs(subr1s, tc7_subr_1);
  init_iprocs(subr3s, tc7_subr_3);
  init_iprocs(lsubr2s, tc7_lsubr_2);
  add_feature("udp");
}
