/* Copyright (c) 1993 Stephen F. White */

#include "cool.h"
#include "string.h"
#include "netio.h"
#include "buf.h"
#include "netio_private.h"
#include "servers.h"

int server_running;

Player *players = 0;

short yo_port;
short player_port = PLAYER_PORT;
SOCKET yo_sock;
char *progfname;
char *dumpfname;
int max_age = MAX_AGE;
int max_ticks = MAX_TICKS;

static SOCKET player_sock;
static int nfds;
static int compiler = 0;        /* flag: command line compiler mode */
static int dumper = 0;          /* flag: dump flatfile mode */
static int do_init = 0;         /* flag: call init() in new objects */

static void init_fdsets (fd_set * inputfds, fd_set * outputfds,
  struct timeval *timeout);
static void process_fdsets (fd_set * inputfds, fd_set * outputfds);
static void init_signals (void);
static void close_sockets (void);
static SOCKET server_socket (int port, int type);
static void run_server (void);
static void server_input (void);
static int player_input (Player * p);
static Player *new_player (void);
static Player *init_player (SOCKET psock);
static void set_nonblock (SOCKET fd);
static int do_options (int argc, char *argv[]);
static long	 safe_gethostbyname(const char *hostname, int timeout);
static const char *shutdown_message = "*** The server has been shut down. ***\r\n";


typedef void (*PFVS) (int);
void sighandler (int sig);

static int do_options (int argc, char *argv[])
{
  int i, j, done_option, newargc = 0;

  for (i = 0; i < argc; i++) {
    if (argv[i][0] == '-') {
      done_option = 0;
      for (j = 1; j < (int)strlen (argv[i]) && !done_option; j++) {
        switch (argv[i][j]) {
        case 'p':
          promiscuous = 1;
          break;
        case 'c':
          corefile = 1;
          break;
        case 'i':
          do_init = 1;
          break;
        case 'f':
          compiler = 1;
          if (i >= argc - 1) {
            return 0;
          }
          progfname = argv[++i];
          done_option = 1;
          break;
        case 'd':
          dumper = 1;
          if (i >= argc - 1) {
            return 0;
          }
          dumpfname = argv[++i];
          done_option = 1;
          break;
        default:
          fprintf (stderr, "Unknown option:  -%c\n", argv[i][j]);
          return 0;
        }                       /* for i */
      }                         /* if */
    } else {
      argv[newargc++] = argv[i];
    }                           /* if */
  }                             /* for j */
  return newargc;
}                               /* do_options() */

int main (int argc, char *argv[])
{
  argc = do_options (argc, argv);
  if (argc != 2) {
    fprintf (stderr, "Usage: %s [-pc -f progfname -d dumpfname] dbfile\n",
      argv[0]);
    return (1);
  }
  WIN32STARTUP
  if (compiler) {
    cmdline_compile (argv[1], progfname, do_init);
    WIN32CLEANUP
    return (0);
  } else if (dumper) {
    write_flatfile (argv[1], dumpfname);
    WIN32CLEANUP
    return (0);
  }
  if (init (argv[1], 1, 1)) {
    WIN32CLEANUP
    return (2);
  }
  writelog ();
  fprintf (stderr, "Initializing sockets.\n");
  player_sock = server_socket (player_port, SOCK_STREAM);
  yo_sock = server_socket (yo_port, SOCK_DGRAM);
  if (player_sock == INVALID_SOCKET || yo_sock == INVALID_SOCKET) {
    WIN32CLEANUP
    return (3);
  }
  nfds = yo_sock + 1;
  writelog ();
  fprintf (stderr, "Connecting to remote servers.\n");
  connect_to_servers ();
  init_signals ();

/*
 * do it
 */
  writelog ();
  fprintf (stderr, "Server initialized.\n");

  run_server ();                /* there 'tis */

  close_sockets ();
  shutdown_server ();
  writelog ();
  fprintf (stderr, "Server exiting.\n");
  WIN32CLEANUP
  return (0);
}

struct timeval cur_time;        /* grabbed at start of loop */
static int totfds;

static void run_server (void)
{
  fd_set inputfds, outputfds;
  struct timeval timeout;

  /* reserve 9 fd's: 3 for stdio, 2 for server sockets, 3 for db, 1 for misc */
  totfds = getdtablesize () - 9;

  server_running = 1;
  while (server_running) {
    gettimeofday (&cur_time, 0);
    timeout.tv_sec = 60;
    timeout.tv_usec = 0;

    queue_player_commands (cur_time, &timeout);
    process_queues (cur_time, &timeout);

    if (!server_running) {
      break;
    }

    init_fdsets (&inputfds, &outputfds, &timeout);

    if (select (nfds, &inputfds, &outputfds, (fd_set *) 0, &timeout) == SOCKET_ERROR) {
      if (GETERROR != EINTR) {
        writelog ();
        perror ("select failed");
        server_running = 0;
      }
    } else {
      gettimeofday (&cur_time, 0);
      process_fdsets (&inputfds, &outputfds);
    }
  }                             /* while */
}

static void init_signals (void)
{
  signal (SIGINT, (PFVS) sighandler);
  signal (SIGTERM, (PFVS) sighandler);
  signal (SIGFPE, (PFVS) sighandler);
  signal (SIGSEGV, (PFVS) sighandler);
#if !defined linux && !defined WIN32
  signal (SIGEMT, (PFVS) sighandler);
#endif
#if !defined __CYGWIN__ && !defined WIN32
  signal (SIGIOT, (PFVS) sighandler);
#endif
#ifndef WIN32
  signal (SIGQUIT, (PFVS) sighandler);
  signal (SIGILL, (PFVS) sighandler);
  signal (SIGTRAP, (PFVS) sighandler);
  signal (SIGBUS, (PFVS) sighandler);
  signal (SIGSYS, (PFVS) sighandler);
  signal (SIGPIPE, SIG_IGN);    /* ignore this */
  signal (SIGURG, SIG_IGN);     /* and this */
  signal (SIGTSTP, SIG_IGN);
  signal (SIGCONT, SIG_IGN);
  signal (SIGALRM, SIG_IGN);
  signal (SIGWINCH, SIG_IGN);
#endif
}

void sighandler (int sig)
{
  char message[80];
  int i;

/*
 * restore all signals to default handling
 */
  for (i = 1; i < NSIG; i++) {
    signal (i, SIG_DFL);
  }

  sprintf (message, "Caught signal %d", sig);
  panic (message);
  close_sockets ();
  if (corefile) {
    writelog ();
    fprintf (stderr, "Dumping corefile.\n");
    WIN32CLEANUP
    (void) abort ();
  }
  writelog ();
  fprintf (stderr, "Server aborting.\n");
  WIN32CLEANUP
  _exit (10);
}

static void
init_fdsets (fd_set * inputfds, fd_set * outputfds, struct timeval *timeout)
{
  Player *p;

  FD_ZERO (inputfds);
  FD_ZERO (outputfds);
  /* only wait for new players if there are free descriptors */
#ifndef WIN32
  if (nfds < totfds)
#endif
  {
    FD_SET (player_sock, inputfds);
  }

  FD_SET (yo_sock, inputfds);   /* always wait for UDP input */
  /*
   * always wait for input from players; wait for output if there's some
   * output to send
   */
  for (p = players; p; p = p->next) {
    FD_SET (p->fd, inputfds);
	if (p->input.head && p->quota >= 0) {
      timeout->tv_sec = timeout->tv_usec = 0;
    }
	if (p->output.head || (p->outbound && !p->connected)) {
      FD_SET (p->fd, outputfds);
    }
  }
}

static void process_fdsets (fd_set * inputfds, fd_set * outputfds)
{
  Player *p, *pnext;

  if (FD_ISSET (yo_sock, inputfds)) {   /* if there's UDP yo input */
    server_input ();            /* handle it */
  }
  if (FD_ISSET (player_sock, inputfds)) {       /* new player wants connect */
    if ((p = new_player ())
#ifndef WIN32
    && p->fd >= nfds
#endif
    ) {
      nfds = p->fd + 1;
    }
  }
  for (p = players; p; p = pnext) {     /* for all players */
    pnext = p->next;
	if (FD_ISSET(p->fd, outputfds)) {	/* socket ready for writing */
	    if (p->outbound && !p->connected) {	/* if it's outbound connect, */
		p->connected = 1;		/* it just connected */
		fprintf(stderr, "Outbound connect #%d, fd %d to %s(%d) eventually succeeded\n",
		        p->id, p->fd, addr_htoa(p->addr), p->port);
		new_connect(p->id, p->fd);
	    } else {
		send_output(p->fd, &p->output);	/* player output, send it */
	    }
    }
    if (FD_ISSET (p->fd, inputfds)) {   /* player has input pending */
	    if (player_input(p) && !p->input.head) {	/* handle it; if conn dies, */
        if (p->connected) {
          disconnect_player (p->id);
        }
        remove_player (p);      /* remove player */
      }
    }
  }
}

static void server_input (void)
{
  char buf[UDP_BLOCK_SIZE + 1];
  int got;
  struct sockaddr_in from;
  int fromlen;

  fromlen = (int)sizeof (from);
  got = recvfrom (yo_sock, buf, UDP_BLOCK_SIZE, 0, (struct sockaddr *) &from,
    &fromlen);
  if (got <= 0) {
    return;
  }
  buf[got] = '\0';
  server_command (&from, buf);
}

static Player *new_player (void)
{
  SOCKET psock;
  struct sockaddr_in from;
  int fromlen;
  Player *p;

  fromlen = (int)sizeof (from);
  psock = accept (player_sock, (struct sockaddr *) &from, &fromlen);
  if (psock == INVALID_SOCKET) {
    return 0;
  } else {
    p = init_player (psock);
    p->connected = 0;
    p->addr = ntohl (from.sin_addr.s_addr);
    p->port = ntohs (from.sin_port);
    writelog ();
    fprintf (stderr, "Accepted player from %s(%d)\n",
      addr_htoa (p->addr), p->port);
    new_connect(p->id, p->fd);
    return p;
  }
}

void remove_player (Player * p)
{
  if (p->connected) {
    writelog ();
    fprintf (stderr, "Player #%d disconnected from fd %d\n", p->id, p->fd);
    } else {
	writelog();
	fprintf(stderr, "Player disconnected from fd %d, never connected\n",
		p->fd);
  }
  buf_free (&p->input);
  buf_free (&p->output);
  shutdown (p->fd, 2);
  close (p->fd);
  if (p->prev) {
    p->prev->next = p->next;
  } else {
    players = p->next;
  }
  if (p->next) {
    p->next->prev = p->prev;
  }
  FREE (p);
}

static Player *init_player (SOCKET psock)
{
  Player *newp = MALLOC (Player, 1);

  set_nonblock (psock);
  newp->fd = psock;
  newp->connected = 0;
  newp->id = SYS_OBJ;
  newp->outbound = 0;
  buf_init (&newp->input);
  buf_init (&newp->output);
  newp->dangling_input = 0;
  newp->quota = MAX_CMDS * MSEC_PER_CMD;
  if (players) {
    players->prev = newp;
  }
  newp->next = players;
  newp->prev = 0;
  players = newp;

  return newp;
}

static SOCKET server_socket (int port, int type)
{
  struct sockaddr_in to;
  SOCKET servsock;
  int opt;

  servsock = socket (AF_INET, type, 0);
  if (servsock == INVALID_SOCKET) {
    writelog ();
    perror ("Couldn't make server socket");
    return -1;
  }
  set_nonblock (servsock);
  opt = 1;
  if (setsockopt (servsock, SOL_SOCKET, SO_REUSEADDR,
      (char *) &opt, sizeof (opt)) == SOCKET_ERROR) {
    writelog ();
    perror ("Couldn't set socket options");
    return -2;
  }
  to.sin_family = AF_INET;
  to.sin_addr.s_addr = INADDR_ANY;
  to.sin_port = htons (port);
  if (bind (servsock, (struct sockaddr *) &to, sizeof (to)) == SOCKET_ERROR) {
    writelog ();
    fprintf (stderr, "%s port %d:  ",
      (type == SOCK_STREAM ? "TCP" : "UDP"), port);
    perror ("couldn't bind()");
    close (servsock);
    return -3;
  }
  if (type == SOCK_STREAM) {
    if (listen (servsock, 5) == SOCKET_ERROR) {
      perror ("Couldn't listen()");
      return -4;
    }
  }
  return servsock;
}

void send_output (SOCKET fd, Buf * output)
{
  Line *line, *next;
  int len, written;

  for (line = output->head; line; line = next) {
    next = line->next;
    len = line->str->len - (line->ptr - line->str->str);
    written = send (fd, line->ptr, len, 0);
    if (written < 0) {          /* error, including EWOULDBLOCK */
      break;
    } else if (written < len) {
      line->ptr += written;
      break;
    } else {
      buf_delhead (output);
    }
  }
}

static void set_nonblock (SOCKET fd)
{
  char buf[128];
#ifdef WIN32
  unsigned long flags = 1;
  ioctlsocket (fd, FIONBIO, &flags);
#else
  int flags;
  flags = fcntl (fd, F_GETFL);
  if (flags < 0 || fcntl (fd, F_SETFL, flags | O_NONBLOCK) == -1) {
    sprintf (buf, "Couldn't set descriptor %d nonblocking", fd);
    writelog ();
    perror (buf);
  }
#endif /* WIN32 */
}

static String *grab_input (SOCKET fd, char **dangling_input)
{
  char buf[BLOCK_SIZE + 1];
  int got;
  String *str;

  got = recv (fd, buf, BLOCK_SIZE, 0);
  if (got <= 0) {
    return 0;
  }
  str = string_new (0);
  if (*dangling_input) {        /* some input left from last time */
    str = string_cat (str, *dangling_input);    /* prepend it */
    FREE (*dangling_input);
    *dangling_input = 0;
  }
  do {
    buf[got] = '\0';
    str = string_cat (str, buf);
    got = recv (fd, buf, BLOCK_SIZE, 0);
  } while (got > 0);
  return str;
}

static int player_input (Player * p)
{
  String *str, *cmd;
  char *q, *r;

  if (!(str = grab_input (p->fd, &p->dangling_input))) {
    return -1;
  }
  for (q = r = str->str; r < str->str + str->len; r++) {
    if (*r == '\n') {           /* save command */
      *q = '\0';
      cmd = string_new (0);
      cmd = string_cat (cmd, str->str);
      buf_add (&p->input, cmd);
      q = str->str;             /* restart at beginning of buf */
    } else if (*r == '\t') {    /* change tabs to spaces */
      *q++ = ' ';
	} else if (*r == '\b') {	/* Handle visible ^h's */
	    q--;
    } else if (isascii (*r) && isprint ((int)*r)) {  /* store printable */
      *q++ = *r;
    }
  }
  if (q > str->str) {           /* there's half a command here */
    *q = '\0';                  /* terminate it */
    p->dangling_input = str_dup (str->str);     /* copy it */
  }
  string_free (str);
  return 0;
}

static void close_sockets (void)
{
  Player *p, *pnext;

  for (p = players; p; p = pnext) {
    pnext = p->next;
    send (p->fd, shutdown_message, strlen (shutdown_message), 0);
    (void) shutdown (p->fd, 2);
    close (p->fd);
  }
  close (player_sock);
  writelog ();
  fprintf (stderr, "Disconnecting from remote servers.\n");
  disconnect_from_servers ();
  close (yo_sock);
}

/*
 * safe_gethostbyname(hostname, timeout)
 *
 * Do a gethostbyname(), with a timer set to expire in _timeout_ seconds
 */

long safe_gethostbyname(const char *hostname, int timeout)
{
    struct hostent      *h = 0;

#ifndef WIN32
    alarm(timeout);
#endif
    h = gethostbyname(hostname);
    if (h) {
	return ntohl(*( (unsigned long *) h->h_addr_list[0]));
    } else {
	return 0;
    }
}

/*
 * net_open_connect(id, hostname, port)
 */

SOCKET net_open_connect(Playerid id, const char *hostname, short port)
{
    SOCKET 	newsock;
    struct sockaddr_in	dest;
    Player	*p, *pnext;
    int err;
    long	addr = safe_gethostbyname(hostname, OUTBOUND_TIMEOUT);

    if (!addr) {
	  return -1;
    }
    for (p = players; p; p = pnext) {
        pnext = p->next;
        if (p->id == id) {
            disconnect_player(p->id);
            remove_player(p);
	}
    }
    newsock = socket(AF_INET, SOCK_STREAM, 0);
    if (newsock == INVALID_SOCKET) {
		return -2;
    }
    if (newsock >= nfds) {
		nfds = newsock + 1;
    }
    p = init_player(newsock);
    p->addr = addr;
    p->port = port;
    p->id = id;
    p->outbound = 1;
    dest.sin_family = AF_INET;
    dest.sin_addr.s_addr = htonl(addr);
    dest.sin_port = htons(port);
    writelog();
    fprintf(stderr, "Outbound connect #%d, fd %d to %s/%s(%d)",
	     id, newsock, hostname, addr_htoa(p->addr), p->port);
    err = connect(newsock, (struct sockaddr *) &dest, sizeof(dest));
    if (err == SOCKET_ERROR && GETERROR != EINPROGRESS && GETERROR != EWOULDBLOCK) {
		perror("failed:  ");
		return -3;
    } else if (err == SOCKET_ERROR && (GETERROR == EINPROGRESS || GETERROR == EWOULDBLOCK)) {
		p->connected = 0;
		fprintf(stderr, " in progress\n");
		return -4;
    } else {
		p->connected = 1;
		fprintf(stderr, " succeeded\n");
		new_connect(p->id, p->fd);
		return newsock;
    }
}