/* network.c
 *
 */

#include "stdinclude.h"

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <arpa/telnet.h>

#include "ficsmain.h"
#include "common.h"
#include "utils.h"
#include "playerdb.h"
#include "network.h"
#include "rmalloc.h"
#include "config.h"
#ifdef TIMESEAL
#include "timeseal.h"
#endif

extern int errno;

PRIVATE int sockfd = 0;		/* The socket */
PRIVATE int numConnections = 0;
/* Sparse array */
PUBLIC connection con[512];

PUBLIC int no_file;
PUBLIC int max_connections;

/* Index == fd, for sparse array, quick lookups! wasted memory :( */
PUBLIC int findConnection(int fd)
{
  if (con[fd].status == NETSTAT_EMPTY)
    return -1;
  else
    return fd;
}

PUBLIC int net_addConnection(int fd, unsigned int fromHost)
{
  int noblock = 1;

  if (findConnection(fd) >= 0) {
    fprintf(stderr, "FICS: FD already in connection table!\n");
    return -1;
  }
  if (numConnections >= max_connections)
    return -1;
  if (ioctl(fd, FIONBIO, &noblock) == -1) {
    fprintf(stderr, "Error setting nonblocking mode errno=%d\n", errno);
  }
  con[fd].fd = fd;
  if (fd != 0)
    con[fd].outFd = fd;
  else
    con[fd].outFd = 1;
  con[fd].fromHost = fromHost;
  con[fd].status = NETSTAT_CONNECTED;
#ifdef TIMESEAL
  con[fd].user[0]='\0';
  con[fd].sys[0]='\0';
  con[fd].timeseal = 0;
  con[fd].time = 0;
#endif
  con[fd].numPending = 0;
  con[fd].processed = 0;
  con[fd].outPos = 0;
  if (con[fd].sndbuf == NULL) {
#ifdef DEBUG
    fprintf(stderr, "FICS: nac(%d) allocating sndbuf.\n", fd);
#endif
    con[fd].sndbufpos = 0;
    con[fd].sndbufsize = MAX_STRING_LENGTH;
    con[fd].sndbuf = rmalloc(MAX_STRING_LENGTH);
  } else {
#ifdef DEBUG
    fprintf(stderr, "FICS: nac(%d) reusing old sndbuf size %d pos %d.\n", fd, con[fd].sndbufsize, con[fd].sndbufpos);
#endif
  }
  con[fd].state = 0;
  numConnections++;

#ifdef DEBUG
  fprintf(stderr, "FICS: fd: %d connections: %d  descriptors: %d \n", fd, numConnections, getdtablesize());	/* sparky 3/13/95 */
#endif

  return 0;
}

PRIVATE int remConnection(int fd)
{
  int which;
  if ((which = findConnection(fd)) < 0) {
    return -1;
  }
  numConnections--;
  con[fd].status = NETSTAT_EMPTY;
  if (con[fd].sndbuf == NULL) {
    fprintf(stderr, "FICS: remcon(%d) SNAFU, this shouldn't happen.\n", fd);
  } else {
    if (con[fd].sndbufsize > MAX_STRING_LENGTH) {
      con[fd].sndbufsize = MAX_STRING_LENGTH;
      con[fd].sndbuf = rrealloc(con[fd].sndbuf, MAX_STRING_LENGTH);
    }
    if (con[fd].sndbufpos) {	/* didn't send everything, bummer */
      con[fd].sndbufpos = 0;
    }
  }
  return 0;
}

PRIVATE void net_flushme(int which)
{
  int sent;

  sent = send(con[which].outFd, con[which].sndbuf, con[which].sndbufpos, 0);
  if (sent == -1) {
    if (errno != EPIPE)		/* EPIPE = they've disconnected */
      fprintf(stderr, "FICS: net_flushme(%d) couldn't send, errno=%d.\n", which, errno);
    con[which].sndbufpos = 0;
  } else {
    con[which].sndbufpos -= sent;
    if (con[which].sndbufpos)
      memmove(con[which].sndbuf, con[which].sndbuf + sent, con[which].sndbufpos);
  }
  if (con[which].sndbufsize > MAX_STRING_LENGTH && con[which].sndbufpos < MAX_STRING_LENGTH) {
    /* time to shrink the buffer */
    con[which].sndbuf = rrealloc(con[which].sndbuf, MAX_STRING_LENGTH);
    con[which].sndbufsize = MAX_STRING_LENGTH;
  }
}

PRIVATE void net_flush_all_connections(void)
{
  int which;
  fd_set writefds;
  struct timeval to;

  FD_ZERO(&writefds);
  for (which = 0; which < MAX_PLAYER; which++)
    if (con[which].status == NETSTAT_CONNECTED && con[which].sndbufpos)
      FD_SET(con[which].outFd, &writefds);

  to.tv_usec = 0;
  to.tv_sec = 0;
  select(no_file, NULL, &writefds, NULL, &to);
  for (which = 0; which < MAX_PLAYER; which++) {
    if (FD_ISSET(con[which].outFd, &writefds)) {
      net_flushme(which);
    }
  }
}

PRIVATE void net_flush_connection(int fd)
{
  int which;
  fd_set writefds;
  struct timeval to;

  if (((which = findConnection(fd)) >= 0) && (con[which].sndbufpos)) {
    FD_ZERO(&writefds);
    FD_SET(con[which].outFd, &writefds);
    to.tv_usec = 0;
    to.tv_sec = 0;
    select(no_file, NULL, &writefds, NULL, &to);
    if (FD_ISSET(con[which].outFd, &writefds)) {
      net_flushme(which);
    }
  }
  return;
}

PRIVATE int sendme(int which, char *str, int len)
{
  int i, count;
  fd_set writefds;
  struct timeval to;
  count = len;

  while ((i = ((con[which].sndbufsize - con[which].sndbufpos) < len) ? (con[which].sndbufsize - con[which].sndbufpos) : len) > 0) {
    memmove(con[which].sndbuf + con[which].sndbufpos, str, i);
    con[which].sndbufpos += i;
    if (con[which].sndbufpos == con[which].sndbufsize) {

      FD_ZERO(&writefds);
      FD_SET(con[which].outFd, &writefds);
      to.tv_usec = 0;
      to.tv_sec = 0;
      select(no_file, NULL, &writefds, NULL, &to);
      if (FD_ISSET(con[which].outFd, &writefds)) {
	net_flushme(which);
      } else {
	/* time to grow the buffer */
	con[which].sndbufsize += MAX_STRING_LENGTH;
	con[which].sndbuf = rrealloc(con[which].sndbuf, con[which].sndbufsize);
      }
    }
    str += i;
    len -= i;
  }
  return count;
}

/*
 * -1 for an error other than EWOULDBLOCK.
 * Put <lf> after every <cr> and put \ at the end of overlength lines.
 * Doesn't send anything unless the buffer fills, output waits until
 * flushed
*/
PUBLIC int net_send_string(int fd, char *str, int format)
{
  int which, i, j;

  if ((which = findConnection(fd)) < 0) {
    return -1;
  }
  while (*str) {
    for (i = 0; str[i] >= ' '; i++);
    if (i) {
      if (format && (i >= (j = LINE_WIDTH - con[which].outPos))) {	/* word wrap */
	i = j;
	while (i > 0 && str[i - 1] != ' ')
	  i--;
	while (i > 0 && str[i - 1] == ' ')
	  i--;
	if (i == 0)
	  i = j - 1;
	sendme(which, str, i);
	sendme(which, "\n\r\\   ", 6);
	con[which].outPos = 4;
	while (str[i] == ' ')	/* eat the leading spaces after we wrap */
	  i++;
      } else {
	sendme(which, str, i);
	con[which].outPos += i;
      }
      str += i;
    } else {			/* non-printable stuff handled here */
      switch (*str) {
      case '\t':
	sendme(which, "        ", 8 - (con[which].outPos & 7));
	con[which].outPos &= ~7;
	if (con[which].outPos += 8 >= LINE_WIDTH)
	  con[which].outPos = 0;
	break;
      case '\n':
	sendme(which, "\n\r", 2);
	con[which].outPos = 0;
	break;
      case '\033':
	con[which].outPos -= 3;
      default:
	sendme(which, str, 1);
      }
      str++;
    }
  }
  return 0;
}

/* if we get a complete line (something terminated by \n), copy it to com
   and return 1.
   if we don't get a complete line, but there is no error, return 0.
   if some error, return -1.
 */
PUBLIC int readline2(char *com, int who)
{
  unsigned char *start, *s, *d;
  int howmany, state, fd, pending;

  static unsigned char will_tm[] = {IAC, WILL, TELOPT_TM, '\0'};
  static unsigned char will_sga[] = {IAC, WILL, TELOPT_SGA, '\0'};
  static unsigned char ayt[] = "[Responding to AYT: Yes, I'm here.]\n";

  state = con[who].state;
  if ((state == 2) || (state > 4)) {
    fprintf(stderr, "FICS: state screwed for con[%d], this is a bug.\n", who);
    state = 0;
  }
  s = start = con[who].inBuf;
  pending = con[who].numPending;
  fd = con[who].fd;

  howmany = recv(fd, start + pending, MAX_STRING_LENGTH - 1 - pending, 0);
  if (howmany == 0)		/* error: they've disconnected */
    return (-1);
  else if (howmany == -1) {
    if (errno != EWOULDBLOCK) {	/* some other error */
      return (-1);
    } else if (con[who].processed) {	/* nothing new and nothing old */
      return (0);
    } else {			/* nothing new, but some unprocessed old */
      howmany = 0;
    }
  }
  if (con[who].processed)
    s += pending;
  else
    howmany += pending;
  d = s;

  for (; howmany-- > 0; s++) {
    switch (state) {
    case 0:			/* Haven't skipped over any control chars or
				   telnet commands */
      if (*s == IAC) {
	d = s;
	state = 1;
      } else if (*s == '\n') {
	*s = '\0';
	strcpy(com, start);
	if (howmany)
	  bcopy(s + 1, start, howmany);
	con[who].state = 0;
	con[who].numPending = howmany;
	con[who].processed = 0;
	con[who].outPos = 0;
	return (1);
      } else if ((*s > (0xff - 0x20)) || (*s < 0x20)) {   
	d = s;
	state = 2;
      }
      break;
    case 1:			/* got telnet IAC */
      if (*s == IP)
	return (-1);		/* ^C = logout */
      else if (*s == DO)
	state = 4;
      else if ((*s == WILL) || (*s == DONT) || (*s == WONT))
	state = 3;		/* this is cheesy, but we aren't using em */
      else if (*s == AYT) {
	send(fd, (char *) ayt, strlen((char *) ayt), 0);
	state = 2;
      } else if (*s == EL) {	/* erase line */
	d = start;
	state = 2;
      } else			/* dunno what it is, so ignore it */
	state = 2;
      break;
    case 2:			/* we've skipped over something, need to
				   shuffle processed chars down */
      if (*s == IAC)
	state = 1;
      else if (*s == '\n') {
	*d = '\0';
	strcpy(com, start);
	if (howmany)
	  memmove(start, s + 1, howmany);
	con[who].state = 0;
	con[who].numPending = howmany;
	con[who].processed = 0;
	con[who].outPos = 0;
	return (1);
      } else if (*s >= ' ')
	*(d++) = *s;
      break;
    case 3:			/* some telnet junk we're ignoring */
      state = 2;
      break;
    case 4:			/* got IAC DO */
      if (*s == TELOPT_TM)
	send(fd, (char *) will_tm, strlen((char *) will_tm), 0);
      else if (*s == TELOPT_SGA)
	send(fd, (char *) will_sga, strlen((char *) will_sga), 0);
      state = 2;
      break;
    }
  }
  if (state == 0)
    d = s;
  else if (state == 2)
    state = 0;
  con[who].state = state;
  con[who].numPending = d - start;
  con[who].processed = 1;
  if (con[who].numPending == MAX_STRING_LENGTH - 1) {	/* buffer full */
    *d = '\0';
    strcpy(com, start);
    con[who].state = 0;
    con[who].numPending = 0;
    con[who].processed = 0;
    return (1);
  }
  return (0);
}

/*
PRIVATE int readline(int who)
{
  int e, fd = con[who].fd;
  char *t = con[who].inBuf;
  int recvCount;
  int totalCount = 0;
  unsigned char c;
  static unsigned char will_tm[] = {IAC, WILL, TELOPT_TM, '\0'};
  static unsigned char will_sga[] = {IAC, WILL, TELOPT_SGA, '\0'};

  t += con[who].numPending;

  while ((recvCount = recv(fd, (char *) &c, 1, 0)) == 1) {
    totalCount += recvCount;
    if (c == IAC) {
      recvCount = recv(fd, (char *) &c, 1, 0);
      if (recvCount == 1) {
	totalCount += recvCount;
	switch (c) {
	case IP:
	  c = '\3';
	  break;
	case DO:
	  recvCount = recv(fd, (char *) &c, 1, 0);
	  if (recvCount == 1) {
	    totalCount += recvCount;
	    if (c == TELOPT_TM) {
	      send(fd, (char *) will_tm, strlen((char *) will_tm), 0);
	    } else if (c == TELOPT_SGA) {
	      send(fd, (char *) will_sga, strlen((char *) will_sga), 0);
	    }
	  }
	  c = '\0';
	  break;
	case DONT:
	  recvCount = recv(fd, (char *) &c, 1, 0);
	  break;
	  c = '\0';
	default:
	  recvCount = recv(fd, (char *) &c, 1, 0);
	  c = '\0';
	  break;
	}
      }
    }
    if (c != '\r' && c > 2) {
      if (isprint(c) || (c == '\n')) {
	*t++ = c;
	con[who].numPending++;
      }
    }
    if (c == '\n' || con[who].numPending >= MAX_STRING_LENGTH - 1) {
      *--t = '\0';
      con[who].numPending = 0;
      con[fd].outPos = 0;
      return 1;
    }
  }

  *t = '\0';
  e = ((totalCount == 0) || (errno != EWOULDBLOCK)) ? -1 : 0;
  return (e);
}
*/

PUBLIC int net_init(int port)
{
  int i;
  int opt;
  struct sockaddr_in serv_addr;
  struct linger lingeropt;

/* Although we have 256 descriptors to work with for opening files,
 * we can only use 126 for sockets under SunOS 4.x.x socket libs.  Using
 * glibc can get you up to 256 again.  Many OS's can do more than that!
 *  Sparky  9/20/95
 */
  no_file = getdtablesize();
  if (no_file > MAX_PLAYER + 6)
    no_file = MAX_PLAYER + 6;
  max_connections = no_file - 6;
  for (i = 0; i < no_file; i++) {
    con[i].status = NETSTAT_EMPTY;
    con[i].sndbuf = NULL;
    con[i].sndbufsize = con[i].sndbufpos = 0;
  }
  /* Open a TCP socket (an Internet stream socket). */
  if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
    fprintf(stderr, "FICS: can't open stream socket\n");
    return -1;
  }
  /* Bind our local address so that the client can send to us */
  memset((char *) &serv_addr, 0, sizeof(serv_addr));
  serv_addr.sin_family = AF_INET;
  serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
  serv_addr.sin_port = htons(port);

  /** added in an attempt to allow rebinding to the port **/

  opt = 1;
  setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (char *) &opt, sizeof(opt));
  opt = 1;
  setsockopt(sockfd, SOL_SOCKET, SO_KEEPALIVE, (char *) &opt, sizeof(opt));
  lingeropt.l_onoff = 0;
  lingeropt.l_linger = 0;
  setsockopt(sockfd, SOL_SOCKET, SO_LINGER, (char *) &lingeropt, sizeof(lingeropt));

/*
#ifdef DEBUG
  opt = 1;
  setsockopt(sockfd, SOL_SOCKET, SO_DEBUG, (char *)&opt, sizeof(opt));
#endif
*/

  if (bind(sockfd, (struct sockaddr *) & serv_addr, sizeof(serv_addr)) < 0) {
    fprintf(stderr, "FICS: can't bind local address.  errno=%d\n", errno);
    return -1;
  }
  opt = 1;
  ioctl(sockfd, FIONBIO, &opt);
  listen(sockfd, 5);
  return 0;
}

PUBLIC void net_close(void)
{
  int i;
  for (i = 0; i < no_file; i++) {
    if (con[i].status != NETSTAT_EMPTY)
      net_close_connection(con[i].fd);
  }
}

PUBLIC void net_close_connection(int fd)
{
  if (con[fd].status == NETSTAT_CONNECTED)
    net_flush_connection(fd);
  if (!remConnection(fd)) {
    if (fd > 2)
      close(fd);
  }
}

PUBLIC void turn_echo_on(int fd)
{
  static unsigned char wont_echo[] = {IAC, WONT, TELOPT_ECHO, '\0'};

  send(fd, (char *) wont_echo, strlen((char *) wont_echo), 0);
}

PUBLIC void turn_echo_off(int fd)
{
  static unsigned char will_echo[] = {IAC, WILL, TELOPT_ECHO, '\0'};

  send(fd, (char *) will_echo, strlen((char *) will_echo), 0);
}

PUBLIC unsigned int net_connected_host(int fd)
{
  int which;

  if ((which = findConnection(fd)) < 0) {
    fprintf(stderr, "FICS: FD not in connection table!\n");
    return -1;
  }
  return con[which].fromHost;
}

PUBLIC void ngc2(char *com, int timeout)
{
  struct sockaddr_in cli_addr;
  int cli_len = (int) sizeof(struct sockaddr_in);

  int fd, loop, nfound, lineComplete;
  fd_set readfds;
  struct timeval to;
 
  while ((fd = accept(sockfd, (struct sockaddr *) & cli_addr, &cli_len)) != -1)
  {
    if (net_addConnection(fd, cli_addr.sin_addr.s_addr)) {
      fprintf(stderr, "FICS is full.  fd = %d.\n", fd);
      psend_raw_file(fd, mess_dir, MESS_FULL);
      close(fd);
    } else
      process_new_connection(fd, net_connected_host(fd));
  }
  
  if (errno != EWOULDBLOCK)
    fprintf(stderr, "FICS: Problem with accept().  errno=%d\n", errno);

  net_flush_all_connections();

  FD_ZERO(&readfds);
  for (loop = 0; loop < no_file; loop++)
    if (con[loop].status != NETSTAT_EMPTY)
      FD_SET(con[loop].fd, &readfds);

  to.tv_usec = 0;
  to.tv_sec = timeout;
  nfound = select(no_file, &readfds, NULL, NULL, &to);
  for (loop = 0; loop < no_file; loop++) {
    if (con[loop].status != NETSTAT_EMPTY) {
      fd = con[loop].fd;
      lineComplete = readline2(com, fd);
      if (lineComplete == 0)	/* partial line: do nothing with it */
	continue;
      if (lineComplete > 0) {	/* complete line: process it */
#ifdef TIMESEAL
        if (!(parseInput(com, &con[loop]))) continue;
#endif
	if (process_input(fd, com) != COM_LOGOUT) {
	  net_flush_connection(fd);
	  continue;
	}
      }
      /* Disconnect anyone who gets here */
      process_disconnection(fd);
      net_close_connection(fd);
    }
  }
}

PUBLIC int net_consize(void)
{
  int i, total;

  total = sizeof(con);
  for (i=0; i < no_file; i++)
    total += con[i].sndbufsize;

  return(total);
}