/*
 * Atom-4 network server
 * Implementation file
 *
 * $Id: server.cc,v 1.14 2003/04/15 20:39:28 hsteoh Exp hsteoh $
 * --------------------------------------------------------------------------
 * PROTOCOL
 *
 * Please see protocol.txt.
 */

#include <arpa/inet.h>
#include <errno.h>
#include <netdb.h>
#include <stdio.h>
#include <string.h>
#include <sys/time.h>
#include <unistd.h>

#include <exception.h>			// must be prog/lib version
#include "server.h"


// FIXME: temporary
#define GAME_VERSION		"2.1"
#define PROTOCOL_VERSION	"2.0"

// Max allowed pending output packets per client (note: this must also account
// for multiple packets sent between returns to the event loop)
#define SEND_LIMIT		24


/*
 * Internal tables
 */

// Map board cells to network representation
char cell2net[NUM_COLORS] = {
  'K', 'r', 'g', 'y', 'b', 'p', 'c', 'W'
};


/*
 *
 * Class clientconn
 *
 */

clientconn::dispatch clientconn::dispatch_tbl[] = {
  { "NAME", &clientconn::p_name },
  { "REQU", &clientconn::p_requ },
  { "MOVE", &clientconn::p_move },
  { "RSGN", &clientconn::p_rsgn },
  { "CHAT", &clientconn::p_chat },
  { "NEXT", &clientconn::p_next },
  { "PLAY", &clientconn::p_play },
  { "WATCH", &clientconn::p_watch },
  { "QUIT", &clientconn::p_quit },
  { "WHO?", &clientconn::p_who },
  { NULL, NULL }
};

void clientconn::p_name() {
  not_implemented();
}

void clientconn::p_requ() {
  // Client wants a board update
  send_board(serv->get_board());
}

void clientconn::p_move() {
  int x,y;

  try {
    x = next_number();
    y = next_number();
  } catch(exception &e) {		// catch syntax errors
    send_packet("GRR 802 %s", e.message());
    return;
  }

  if (playnum) {
    switch (serv->move(playnum,x,y)) {	// attempt move
    case 1:				// good move
      break;
    case 0:				// bad move
      send_packet("GRR 103 Illegal move");
      break;
    case -1:
      send_packet("GRR 102 Game hasn't started yet");
      break;
    case -2:
      send_packet("GRR 102 Not your turn to move");
      break;
    default:
      // FIXME: internal error
      break;
    }
  } else {
    send_packet("GRR 101 You are not a player in the current game");
  }
}

void clientconn::p_rsgn() {
  not_implemented();
}

void clientconn::p_chat() {
  not_implemented();
}

void clientconn::p_next() {
  not_implemented();
}

void clientconn::p_play() {
  // Sanity checks
  if (playnum != 0) {
    send_packet("GRR 104 Already playing");
    return;
  }
  // FIXME: should check for atom4::round_over() as well

  // Assign player number
  playnum = serv->assign_player(id);
  if (playnum) {
    send_packet("PLNUM %d", playnum);
  } else {
    send_packet("PLNUM W");
  }
}

void clientconn::p_watch() {
  if (playnum==0) {
    send_packet("PLNUM W");		// indicate client is a watcher
  } else {
    send_packet("GRR 104 Already playing, use RSGN to resign");
  }
}

void clientconn::p_quit() {
  disconnected();			// client wants to quit
}

void clientconn::p_who() {
  serv->send_names(id);			// request for name list
}

void clientconn::fatal_error(char *fmt, ...) {
  va_list args;

  // Transmit error message
  va_start(args, fmt);
  send_packet(fmt, args);
  va_end(args);

  // Schedule disconnection
  disconnected();
}

void clientconn::not_implemented() {
  send_packet("GRR 802 Not implemented yet");
}

int clientconn::next_number() {
  char *cp = parser.next_word();
  char *ep;
  int val;

  if (strlen(cp) > 0) {
    val = strtol(cp, &ep, 10);
    if (cp==ep || *ep!='\0')
      throw exception("@Malformed number: %s", cp);

    return val;
  } else {
    throw exception("Missing numerical argument");
  }
}

void clientconn::handshake() {
  if (strcmp(parser.packet_type(), "ATOM4")!=0) {
    fatal_error("ERR 902 Bad response to handshake");
    return;				// abort
  }
  if (strcmp(parser.next_word(), "CLNT")!=0) {
    fatal_error("ERR 902 Handshake expecting CLNT");
    return;
  }
  if (strcmp(parser.next_word(), GAME_VERSION)!=0) {
    fatal_error("ERR 901 Incompatible game version, server runs %s",
                GAME_VERSION);
    return;
  }
  if (strcmp(parser.next_word(), PROTOCOL_VERSION)!=0) {
    fatal_error("ERR 901 Incompatible protocol version");
    return;
  }

  // Handshake complete, enter connected state
  send_packet("ATOM4 CONN Welcome to Atom-4 (%s) server (%s)", GAME_VERSION,
              PROTOCOL_VERSION);
  enter_connected_state();
}

void clientconn::enter_connected_state() {
  fprintf(stderr, "Entering connected state\n");

  connstate=CONNECTED;
  playnum=0;				// assume watcher mode by default
  serv->send_names(id);
  send_board(serv->get_board());	// send initial game board

  if (serv->need_players()) {
    send_packet("POW?");		// offer client to play or watch
  }
}

void clientconn::connected_state() {
  dispatch *dp;

  for (dp=&dispatch_tbl[0]; dp->packet_type; dp++) {
    if (!strcasecmp(dp->packet_type, parser.packet_type())) {
      (this->*(dp->handler))();		// invoke handler
      return;
    }
  }

  // Unable to find handler for request
  send_packet("GRR 802 Unknown request: %s", parser.packet_type());
}

// Notes:
// - we should not directly invoke server::disconnect_client() here because it
//   will return to an invalid (dealloc'd) object context. So we should just
//   enter DISCONN state, drop any additional incoming packet, and schedule
//   for disconnect_client() to be called afterwards.
void clientconn::process_packet(char *packet) {
  parser.parse(packet);

  switch (connstate) {
  case HANDSHAKE:
    handshake();
    break;
  case CONNECTED:
    connected_state();
    break;
  case DISCONN:
    break;				// ignore all incoming packets
  default:
    throw exception("@Inconsistent clientconn state %d\n", connstate);
  }
}

// Notes:
// - server::disconnect_client() doesn't actually disconnect until the event
//   loop has finished dispatching; so we temporarily enter the DISCONN state
//   to avoid doing anything more until the actual disconnect happens.
void clientconn::disconnected() {
  if (connstate != DISCONN) {
    connstate=DISCONN;			// ignore all further communications
    serv->disconnect_client(id);	// schedule disconnect
  }
}

clientconn::clientconn(server *s, int cid, int sock, char *hostname,
                       eventloop *loop, int sendlimit) :
	netconn(sock, loop, sendlimit), serv(s), id(cid), hostname(hostname),
	connstate(HANDSHAKE), playnum(0) {

//fprintf(stderr, "Client connection initialized\n");

  // Initiate handshake
  if (!send_packet("ATOM4 SERV %s %s", GAME_VERSION, PROTOCOL_VERSION)) {
    throw exception("@Error while sending handshake to client on fd %d\n",
                    sockfd());
  }

//fprintf(stderr, "Sent handshake, waiting for response\n");
}

clientconn::~clientconn() {
  fprintf(stderr, "Client (fd %d) disconnected\n", sockfd());

  delete [] hostname;
}

void clientconn::send_board(board4 *b) {
  int w,h;
  int x,y;

  w = b->width();
  h = b->height();

  // Send board dimensions
  send_packet("BDIM %d %d", w, h);

  char *rowbuf = new char[w*2 + 2];
  for (y=0; y<h; y++) {
    // Format row data

    if (y%2) {				// be nice to telnet clients :-)
      rowbuf[0] = ' ';			// pad start of row
    } else {
      rowbuf[w*2] = ' ';		// pad end of row
    }

    for (x=0; x<w; x++) {
      color4 cell = b->getcell(x,y);

      if (cell==EMPTY_CELL) {
        rowbuf[x*2 + y%2] = '.';
      } else if (cell>='a' && cell<='h') {
        rowbuf[x*2 + y%2] = cell2net[(cell-'a')];
      } else {
        delete rowbuf;
        throw exception("@Internal error: unknown cell value %d\n",
                        (char)cell);
      }
      rowbuf[x*2 + y%2 + 1] = ' ';	// delimit cells with spaces
    } // endfor(x)
    rowbuf[w*2 + 1] = '\0';		// make sure result is null-terminated

    // Transmit row
    if (!send_packet("BROW %2d %s", y, rowbuf))
      throw exception("@Unable to send board row \"%s\"", rowbuf);
  } // endfor(y)

  delete rowbuf;			// discard row buffer
  send_packet("BEND");			// indicate end of board data
}

void clientconn::round_over() {
  playnum=0;
}



/*
 *
 * Class server
 *
 */

void server::serversock::read_ready(eventloop *src, int fd) {
  serv->new_client();			// accept connection
}

void server::serversock::write_ready(eventloop *src, int fd) {}

void server::disconnector::tick(eventloop *src, timeval t) {
  if (serv->clients[cid]) {
//fprintf(stderr, "Disconnecting client %d\n", cid);

    delete serv->clients[cid];		// disconnect client
    serv->clients[cid] = NULL;

    // TBD: clean up if client was a player
  } else {
    // Not sure about this; we might need to check for repeated disconnect
    // requests for the same client and ignore subsequent disconnect timers.
    throw exception("@Attempt to disconnect non-existent client %d", cid);
  }
}

/* FIXME: this function, as well as other, general, server-related stuff,
 * really should be in a base class which we can reuse for future servers!
 */
/* Creates a socket for listening to connections on the given port.
 * Returns socket fd if successful, otherwise returns -1.
 * On return sockname contains the structure for socket name.
 */
int server::create_servsock(int port, struct sockaddr_in *sockname,
                            int backlog) {
  int sock;

  /* Create inet-domain socket, stream-type connection, default (0) protocol */
  sock = socket(PF_INET, SOCK_STREAM, 0);
  if (sock<0) {
    perror("Socket error");
    return -1;
  }

  /* Bind the socket address. The htons() and htonl() calls are to convert host
   * byte order to network byte order. */
  sockname->sin_family = AF_INET;
  sockname->sin_port = htons(port);
  sockname->sin_addr.s_addr = htonl(INADDR_ANY);
  if (bind(sock, (struct sockaddr*)sockname, sizeof(*sockname)) < 0) {
    perror("Socket bind error");
    close(sock);
    return -1;
  }

  /* Listen for connections */
  if (listen(sock, backlog)) {
    perror("Cannot listen to socket");
    close(sock);                        /* discard socket: can't listen */
    return -1;
  }
  return sock;
}

int server::new_client() {
  struct sockaddr_in adrs;		// client's address
  socklen_t adrs_len=sizeof(adrs);
  struct hostent *hostinfo;		// for obtaining client's DNS name
  int csock, i;
  char *hostname;

//fprintf(stderr, "Got client connection\n");

  // Accept the connection
  csock = accept(sock, (struct sockaddr *)&adrs, &adrs_len);
  if (csock==-1) return 0;		// Error occurred

//fprintf(stderr, "Client socket is %d\n", csock);

  // Find out where the client is connecting from
  hostinfo = gethostbyaddr((char*)&adrs.sin_addr, adrs_len, AF_INET);
  if (hostinfo) {
    hostname = new char[strlen(hostinfo->h_name)+1];
    strcpy(hostname, hostinfo->h_name);
  } else {
    // Unable to determine hostname; use numeric IP instead
    char *ipadrs = inet_ntoa(adrs.sin_addr);
    hostname = new char[strlen(ipadrs)+1];
    strcpy(hostname, ipadrs);
  }

fprintf(stderr, "New client from %s\n", hostname);

  // Find available client ID
  for (i=0; i<MAX_CLIENTS; i++) {
    if (!clients[i]) {
      clients[i] = new clientconn(this, i, csock, hostname, loop, SEND_LIMIT);
      return 1;
    }
  }

  // No more available ID's; send error to client and abort.
  char *errormsg = "ERR 903 Server is full\n";
  send(csock, errormsg, strlen(errormsg)+1, MSG_NOSIGNAL);
  close(csock);

  return 0;
}

void server::broadcast(char *fmt, ...) {
  va_list args;
  int i;

  for (i=0; i<MAX_CLIENTS; i++) {
    if (clients[i]) {
      int rc;

      va_start(args, fmt);
      rc=clients[i]->vsend_packet(fmt, args);
      va_end(args);

      if (!rc)
        throw exception("Error while broadcasting packet: %s ...", fmt);
    }
  }
}

void server::reset_players() {
  int i;

  for (i=0; i<NUM_PLAYERS; i++) {
    if (players[i]) {
      players[i]->round_over();
      players[i] = NULL;
    }
  }
}

server::server(atom4 *engine, eventloop *eloop, int listenport, int opts) :
	loop(eloop), listener(this) {
  int i;

  game = engine;
  port = listenport;
  options = opts;

  // Init client table
  for (i=0; i<MAX_CLIENTS; i++) {
    clients[i] = NULL;
  }

  // Init player table
  for (i=0; i<NUM_PLAYERS; i++) {
    players[i] = NULL;
  }

  // Open server socket
  sock = create_servsock(port, &sockname, SERVER_BACKLOG);
  if (sock < 0)
    throw exception("Unable to create server socket");

  // Register with event loop
  loop->register_handler(eventloop::READER, sock, &listener);
}

server::~server() {
  int i;

  for (i=0; i<MAX_CLIENTS; i++) {
    // clientconn dtor should take care of actually closing the socket
    if (clients[i]) delete clients[i];
  }

  close(sock);
}

void server::disconnect_client(int cid) {
  // Disconnect client, but only after eventloop dispatch is over; otherwise
  // we may return into an invalid object context.
  loop->schedule(0,0, new disconnector(this, cid));
}

void server::send_names(int cid) {
  int i;

  if (!clients[cid])
    throw exception("@Illegal client ID %s", cid);

  for (i=0; i<MAX_CLIENTS; i++) {
    if (clients[i]) {
      clientconn *c = clients[i];
      clients[cid]->send_packet("NAME %d:%s %d client%d %s",
	i, c->hostname, c->playnum, i,
	(c->playnum==game->current_player()) ? "[current turn]" : ""
      );
    }
  }
}

int server::need_players() {
  int i;

  for (i=0; i<NUM_PLAYERS; i++) {
    if (!players[i]) return i+1;	// found empty slot
  }
  return 0;				// no slot available
}

int server::assign_player(int cid) {
  int plnum = need_players();

  // Sanity check
  if (!clients[cid])
    throw exception("@Illegal client ID %d", cid);

  if (plnum==0) return 0;		// No more slots available

  // Assign client to player number
  players[plnum-1] = clients[cid];

  // Start game if all player slots are now filled
  if (!need_players()) {
    int curpl = game->current_player();

    if (curpl != -1) {
      broadcast("TURN %d %c", curpl, cell2net[game->current_tile()-'a']);
    }
  }

  return plnum;
}

int server::move(int player, int x, int y) {
  // Sanity checks
  if (player<1 || player>2)
    throw exception("@move(): bad player number %d", player);
  if (!players[player-1])
    throw exception("@No such player: %d", player);
  if (game->current_player()==-1 || need_players())
    return -1;
  if (game->current_player()!=player)
    return -2;

  // Make the move
  if (!game->move(player,x,y)) {
    return 0;				// bad move
  } else {				// good move

    // FIXME: broadcast board changes

    if (game->round_over()) {
      int winner = game->winner();

      if (winner==STALEMATE) {
        broadcast("DRAW");
      } else if (winner!=-1) {
        broadcast("WIN %d", winner);
      } else {
        // FIXME: unexpected state, internal error
      }
      reset_players();
    } else {
      int curpl = game->current_player();
      if (curpl!=-1) {
        broadcast("TURN %d %c", curpl, cell2net[game->current_tile()-'a']);
      }
    }
    return 1;
  }
}

