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

#include <signal.h>
#include <stdio.h>
#ifdef SYSV
#include <string.h>
#else
#include <strings.h>
#endif
#include <sys/types.h>
#include <sys/uio.h>
#include <sys/file.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <sys/ioctl.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <sys/errno.h>
#include <ctype.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

#include "config.h"
#include "string.h"
#include "netio.h"
#include "buf.h"
#include "netio_private.h"
#include "sys_proto.h"
#include "socket_proto.h"
#include "servers.h"

int	server_running;

Player	*players = 0;

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

static int	 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 int	 server_socket(int port, int type);
static void	 run_server(void);
static void	 server_input(void);
static int	 player_input(Player *p);
static void	 send_output(int fd, Buf *output);
static Player	*new_player(void);
static Player	*init_player(int psock);
static void	 set_nonblock(int s);
static int	 do_options(int  argc, char  *argv[]);
static const char	*shutdown_message = "*** The server has been shut down. ***\r\n";
char		*welcome;

typedef void  (*PFV)();

void		 sighandler(int sig, int code);

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 < 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() */

void
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]);
	exit (1);
    }
    if (compiler) {
	cmdline_compile(argv[1], progfname, do_init);
	exit(0);
    } else if (dumper) {
	write_flatfile(argv[1], dumpfname);
	exit(0);
    }
    if (init(argv[1], 1, 1)) {
	exit(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 < 0 || yo_sock < 0) {
	exit(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");
    exit (0);
}

#ifdef PROTO
extern void		gettimeofday(struct timeval *, struct timezone *);
#endif /* PROTO */
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) < 0) {
	    if (errno != 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, (PFV) sighandler);
    signal(SIGQUIT, (PFV) sighandler);
    signal(SIGILL, (PFV) sighandler);
    signal(SIGTRAP, (PFV) sighandler);
    signal(SIGIOT, (PFV) sighandler);
    signal(SIGFPE, (PFV) sighandler);
#ifndef linux
    signal(SIGBUS, (PFV) sighandler);
    signal(SIGEMT, (PFV) sighandler);
    signal(SIGSYS, (PFV) sighandler);
#endif /* !linux */
    signal(SIGSEGV, (PFV) sighandler);
    signal(SIGPIPE, SIG_IGN);		/* ignore this */
    signal(SIGTERM, (PFV) sighandler);
    signal(SIGURG, SIG_IGN);		/* and this */
    signal(SIGTSTP, SIG_IGN);
    signal(SIGCONT, SIG_IGN);
    signal(SIGWINCH, (PFV) SIG_IGN);
}

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

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

    sprintf (message, "Caught signal %d, code %d", sig, code);
    panic(message);
    close_sockets();
    if (corefile) {
	writelog();
	fprintf(stderr, "Dumping corefile.\n");
	(void) abort();
    }
    writelog();
    fprintf(stderr, "Server aborting.\n");
    _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 */
    if (nfds < totfds) {
	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->parsing && p->quota >= 0) {
	    timeout->tv_sec = timeout->tv_usec = 0;
	}
	if (p->output.head) {
	    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()) && p->fd >= nfds) {
	    nfds = p->fd + 1;
	}
    }
    for (p = players; p; p = pnext) {		/* for all players */
	pnext = p->next;
	if (FD_ISSET(p->fd, outputfds)) {	/* player has output pending */
	    send_output(p->fd, &p->output);	/* send it */
	}
	if (FD_ISSET(p->fd, inputfds)) {	/* player has input pending */
	    if (player_input(p)) {		/* 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 = 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)
{
    int			psock;
    struct sockaddr_in	from;
    int			fromlen;
    Player		*p;

    fromlen = sizeof(from);
    psock = accept(player_sock, (struct sockaddr *) &from, &fromlen);
    if (psock < 0) {
	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);
	write(psock, welcome, strlen(welcome));
	return p;
    }
}

void
remove_player(Player *p)
{
    if (p->connected) {
	if (p->isprogramming) {
	    fclose(p->progfile);
	    unlink(p->progfilename);
	    FREE(p->progwhat);
	}
	writelog();
	fprintf(stderr, "Player #%d disconnected from fd %d\n", p->id, 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(int psock)
{
    Player	*newp = MALLOC(Player, 1);

    set_nonblock(psock);
    newp->fd = psock;
    newp->connected = 0;
    newp->isprogramming = 0;
    newp->parsing = 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 int
server_socket(int port, int type)
{
    struct sockaddr_in	to;
    int			servsock, opt;

    servsock = socket(AF_INET, type, 0);
    if (servsock < 0) {
	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)) < 0) {
	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)) < 0) {
	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) < 0) {
	    perror ("Couldn't listen()");
	    return -4;
	}
    }
    return servsock;
}

static void
send_output(int 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 = write(fd, line->ptr, len);
	if (written < 0) {	/* error, including EWOULDBLOCK */
	    break;
	} else if (written < len) {
	    line->ptr += written;
	    break;
	} else {
	    buf_delhead(output);
	}
    }
}

static void
set_nonblock(int s)
{
    char	buf[128];

#ifndef linux
    if (fcntl(s, F_SETFL, FNDELAY) == -1) {
#else /* linux */
    if (fcntl(s, F_SETFL, O_NONBLOCK) == -1) {
#endif
	sprintf(buf, "Couldn't set descriptor %d nonblocking", s);
	writelog();
	perror(buf);
    }
}

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

    got = read (fd, buf, BLOCK_SIZE); 
    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 = read (fd, buf, BLOCK_SIZE); 
    } 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 (isascii (*r) && isprint (*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;
	write(p->fd, shutdown_message, strlen(shutdown_message));
	(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);
}