/*

 BabbleMud - A MUD server

 Author:  Nick Gammon 
          http://www.gammon.com.au/ 

(C) Copyright Nick Gammon 2004. Permission to copy, use, modify, sell and
distribute this software is granted provided this copyright notice appears
in all copies. This software is provided "as is" without express or implied
warranty, and with no claim as to its suitability for any purpose.
 
*/

#include <fcntl.h>
#include <signal.h>
#include <sys/types.h>

#ifdef WIN32
#include <time.h>
#include <winsock.h>
#else
#include <sys/time.h>
#include <sys/socket.h>
#include <sys/errno.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#endif

// standard library includes ...

#include <stdexcept>
#include <iostream>

using namespace std; 

#include "utils.h"
#include "constants.h"
#include "globals.h"
#include "exceptions.h"
#include "events.h"
#include "player.h"
#include "gameutils.h"

void MainEventHandler (Event & event);

// comms descriptors
static fd_set in_set;  
static fd_set out_set;
static fd_set exc_set;

/* Here when a signal is raised */

void bailout (int sig)
{
  cout << "**** Terminated by player on signal " << sig << " ****" << endl << endl;
  bStopNow = true;
} /* end of bailout */

void sighup (int sig)
{
  cout << "**** SIGHUP received ****" << endl << endl;
} /* end of sighup */

void SetUpListener (int & iControl, const int port)
  {
  struct sockaddr_in sa;
    
  // Create the control socket
  if ( (iControl = socket (AF_INET, SOCK_STREAM, 0)) == -1)
    throw msg ("comms_error_creating_control_socket");
  
  // make sure socket doesn't block
  if (fcntl( iControl, F_SETFL, FNDELAY ) == -1)
    throw msg ("comms_error_fcntl_control_socket");

  struct linger ld = linger ();  // zero it

  // Don't allow closed sockets to linger
  if (setsockopt( iControl, SOL_SOCKET, SO_LINGER,
                  (char *) &ld, sizeof ld ) == -1)
    throw msg ("comms_error_setsockopt_linger");

  int x = 1;

  // Allow address reuse 
  if (setsockopt( iControl, SOL_SOCKET, SO_REUSEADDR,
                 (char *) &x, sizeof x ) == -1)
    throw msg ("comms_error_setsockopt_reuseaddr");
  
  sa.sin_family       = AF_INET;
  sa.sin_port         = htons (port);
  sa.sin_addr.s_addr  = INADDR_ANY;   /* change to listen on a specific adapter */

  // bind the socket to our connection port
  if ( bind (iControl, (struct sockaddr *) &sa, sizeof sa) == -1)
    throw msg ("comms_error_bind");
  
  // listen for connections

  if (listen (iControl, SOMAXCONN) == -1)   // SOMAXCONN is the backlog count
    throw msg ("comms_error_listen");
  
  } // end of SetUpListener

/* set up comms - get ready to listen for connection */

int InitComms ()
  {
  int mudport = control ().geti ("port", 4000);    // player port, default to 4000
  int webport = control ().geti ("webport", 80);   // web client port, default to 80
    
  // set up both player port and web client port
  try
    {
    SetUpListener (iMudControl, mudport);
    SetUpListener (iWebControl, webport);
    }  // end of try block
    
  // problem?
  catch (msg & e)
    {
    cerr << "Cannot initialise comms ..." << endl;
    perror (convertErrorCode (e.what ()).c_str ());
    return 1;    
    }

  tLastMessage = time (NULL);
   
  // standard termination signals
  signal (SIGINT,  bailout);
  signal (SIGTERM, bailout);
  signal (SIGQUIT, bailout);
  signal (SIGHUP,  sighup);

  cout << "Accepting player connections from port " << mudport << endl;
  cout << "Accepting web client connections from port " << webport << endl;
  
  return 0;
  }   /* end of InitComms */


/* close listening port */

void CloseComms ()
  {

  cerr << "Closing all comms connections." << endl;

  // close listening sockets
  if (iMudControl != NO_SOCKET)
    close (iMudControl);
  if (iWebControl != NO_SOCKET)
    close (iWebControl);

  // get rid of all players and mobs in playing map
  playing ().clear ();
  
  // delete all players - this will close connections
  for_each (ISEQ (playerlist ()), DeleteObject ());

  // delete all web clients - this will close connections
  for_each (ISEQ (webclientlist ()), DeleteObject ());
  
  // delete all mobs
  for_each (ISEQ (moblist ()), DeleteObject ());

  } /* end of CloseComms */

// prepare for comms
struct setUpMudDescriptors
{
  int iMaxdesc;
  
  setUpMudDescriptors (const int i) : iMaxdesc (i) {}
    
  // check this player
  void operator() (const Player * p) 
    {
     /* don't bother if connection is closed */
      if (p->Connected ())
        {
        iMaxdesc = max (iMaxdesc, p->GetSocket ());
        // don't take input if they are closing_ down
        if (!p->closing_)
          {
          FD_SET( p->GetSocket (), &in_set  );
          FD_SET( p->GetSocket (), &exc_set );
          }
        
        /* we are only interested in writing to sockets we have something for */
        if (p->PendingOutput ())
          FD_SET( p->GetSocket (), &out_set );
        } /* end of active player */
    } // end of operator()  
    
  int GetMax () const { return iMaxdesc; }
  
};  // end of setUpDescriptors

// prepare for comms
struct setUpWebDescriptors
{
  int iMaxdesc;
  
  setUpWebDescriptors (const int i) : iMaxdesc (i) {}
    
  // check this player
  void operator() (const WebClient * p) 
    {
     /* don't bother if connection is closed */
      if (p->Connected ())
        {
        iMaxdesc = max (iMaxdesc, p->GetSocket ());
        // don't take input if they are closing_ down
        if (!p->closing_)
          {
          FD_SET( p->GetSocket (), &in_set  );
          FD_SET( p->GetSocket (), &exc_set );
          }
        
        /* we are only interested in writing to sockets we have something for */
        if (p->PendingOutput ())
          FD_SET( p->GetSocket (), &out_set );
        } /* end of active client */
    } // end of operator()  
    
  int GetMax () const { return iMaxdesc; }
  
};  // end of setUpWebDescriptors

// handle comms
struct processMudDescriptors
{
  
  // handle this player
  void operator() (Player * p) 
    {
      /* handle exceptions */
      if (p->Connected () && FD_ISSET (p->GetSocket (), &exc_set))
        p->ProcessException ();

      /* look for ones we can read from, provided they aren't closed */
      if (p->Connected () && FD_ISSET (p->GetSocket (), &in_set))
        p->ProcessRead ();

      /* look for ones we can write to, provided they aren't closed */
      if (p->Connected () && FD_ISSET (p->GetSocket (), &out_set))
        p->ProcessWrite ();
     } // end of operator()  
      
};  // end of processMudDescriptors

// handle comms
struct processWebDescriptors
{
  
  // handle this player
  void operator() (WebClient * p) 
    {
      /* handle exceptions */
      if (p->Connected () && FD_ISSET (p->GetSocket (), &exc_set))
        p->ProcessException ();

      /* look for ones we can read from, provided they aren't closed */
      if (p->Connected () && FD_ISSET (p->GetSocket (), &in_set))
        p->ProcessRead ();

      /* look for ones we can write to, provided they aren't closed */
      if (p->Connected () && FD_ISSET (p->GetSocket (), &out_set))
        p->ProcessWrite ();
     } // end of operator()  
      
};  // end of processWebDescriptors

/* new player has connected */

void ProcessNewMudConnection ()
  {
  static struct sockaddr_in sa;
  socklen_t sa_len = sizeof sa;   

  int s;    /* incoming socket */

  /* loop until all outstanding connections are accepted */
  while (true)
    {
    s = accept ( iMudControl, (struct sockaddr *) &sa, &sa_len);

    /* a bad socket probably means no more connections are outstanding */
    if (s == NO_SOCKET)
      {

      /* blocking is OK - we have accepted all outstanding connections */
      #ifdef WIN32
      if ( errno == WSAEWOULDBLOCK )
      #else
      if ( errno == EWOULDBLOCK )
      #endif
        return;

      perror ("accept");
      return;
      }
        
    /* here on successful accept - make sure socket doesn't block */
    
    if (fcntl (s, F_SETFL, FNDELAY) == -1)
      {
      perror ("fcntl on player socket");
      return;
      }

    string address = inet_ntoa ( sa.sin_addr);
    int port = ntohs (sa.sin_port);
            
    set<string> blockedIP;
    stringToContainer (control ().gets ("blocked_ip"), blockedIP);
    
    // immediately close connections from blocked IP addresses
    if (blockedIP.find (address) != blockedIP.end () ||
      badPlayerPasswords () [address] >= control ().geti ("password_attempts_maximum_this_ip", 20))
      {
      cerr << "Rejected connection from " << address << endl;
      close (s);
      continue;      
      }
      
    Player * p = new Player (s);
    playerlist ().push_back (p);
         
    cout << "New player accepted on socket " << s << 
            ", from address " << address << 
            ", port " << port << endl;
    
    serverInfo ().seti ("last_incoming_port", port);          // for random token generation
    serverInfo ().sets ("last_incoming_address", address);    // for random token generation
    
    p->address_ = address;
    p->send ("\nWelcome to " + MUDNAME + " server version " + VERSION + "\n"); 
    p->send ("welcome", messagemap ());   // message from message file
    p->send (p->prompt_);    // initial prompt_ (Enter your name ...)
    
    } /* end of processing *all* new connections */

  } /* end of ProcessNewMudConnection */
  
void ProcessNewWebConnection ()
  {
  static struct sockaddr_in sa;
  socklen_t sa_len = sizeof sa;   

  int s;    /* incoming socket */

  /* loop until all outstanding connections are accepted */
  while (true)
    {
    s = accept ( iWebControl, (struct sockaddr *) &sa, &sa_len);

    /* a bad socket probably means no more connections are outstanding */
    if (s == NO_SOCKET)
      {

      /* blocking is OK - we have accepted all outstanding connections */
      #ifdef WIN32
      if ( errno == WSAEWOULDBLOCK )
      #else
      if ( errno == EWOULDBLOCK )
      #endif
        return;

      perror ("accept");
      return;
      }
        
    /* here on successful accept - make sure socket doesn't block */
    
    if (fcntl (s, F_SETFL, FNDELAY) == -1)
      {
      perror ("fcntl on web client socket");
      return;
      }

    string address = inet_ntoa ( sa.sin_addr);
    int port = ntohs (sa.sin_port);
            
    set<string> blockedIP;
    stringToContainer (control ().gets ("blocked_ip"), blockedIP);
    
    // immediately close connections from blocked IP addresses
    if (blockedIP.find (address) != blockedIP.end () ||
        badWebPasswords () [address] >= control ().geti ("password_attempts_maximum_this_ip", 20))
      {
      webserverlog ("Rejected connection from " + address);
      close (s);
      continue;      
      }
      
    WebClient * p = new WebClient (s);
    webclientlist ().push_back (p);
         
    webserverlog (MAKE_STRING ("New web client accepted on socket " << s << 
            ", from address " << address << 
            ", port " << port));
    
    p->address_ = address;
    
    serverInfo ().seti ("web_server_hits", serverInfo ().geti ("web_server_hits") + 1);
    serverInfo ().seti ("web_last_incoming_port", port);          // for random token generation
    serverInfo ().sets ("web_last_incoming_address", address);    // for random token generation
    
    } /* end of processing *all* new connections */

  } /* end of ProcessNewWebConnection */

void RemoveInactivePlayers ()
{
  for (PlayerListIterator i = playerlist ().begin (); i != playerlist ().end (); )
    {
    if (!(*i)->Connected () ||        // no longer connected
         (*i)->closing_)               // or about to leave us
      {
     // no longer in playing map
      PlayingMap::iterator playingIter = playing ().find ((*i)->gets ("id"));
      if (playingIter != playing ().end ())
        playing ().erase (playingIter);
    
      // delete player
      delete *i;
      // not in active player list any more
      playerlist ().erase (i++);
      }
    else
      ++i;
    } /* end of looping through players */
} // end of RemoveInactivePlayers

void RemoveInactiveWebClients ()
{
  for (WebClientListIterator i = webclientlist ().begin (); i != webclientlist ().end (); )
    {
      // close him when all is sent
    if ((*i)->connstate_ == eAwaitingFinish && !((*i)->PendingOutput ()))
        (*i)->CloseClient ();
    
    if (!(*i)->Connected () ||        // no longer connected
         (*i)->closing_)               // or about to leave us
      {
      // delete client
      delete *i;
      // not in active player list any more
      webclientlist ().erase (i++);
      }
    else
      ++i;
    } /* end of looping through web clients */
} // end of RemoveInactiveWebClients

/* process player input - check connection state, and act accordingly */

void ProcessPlayerInput (Player * p, const string & s)
{
static map<tConnectionStates, tStateHandler> & statemap = getStateMap ();

  try
    {
    istringstream is (s);
              
    // look up what to do in state map  
    map<tConnectionStates, tStateHandler>::iterator si = statemap.find (p->connstate_);
  
    if (si != statemap.end ())
      si->second (p, is);  // execute command (eg. ProcessCommand) 
    } // end of try block

  // all errors during input processing will be caught here
  catch (exception & e)
    {
    string msg; 
    // if playing the command handler has already looked up the message (I hope)
    if (p->connstate_ == ePlaying)
      msg = e.what ();
    else
      msg = FixMessage (p->prefix (), p->error (e.what ()), p->sub_);
    
    p->send (msg);
    }
  
  p->send (p->prompt_);   // re-prompt them

} /* end of ProcessPlayerInput */

// main processing loop
void MainLoop ()
{
  // loop processing input, output, events
  do
    {

    // We will go through this loop roughly every COMMS_WAIT_SEC/COMMS_WAIT_USEC
    // seconds (at present 0.5 seconds).
    ProcessEvents (eventQueue (), MainEventHandler); 
      
    // delete players who have closed their comms - have to do it outside other loops to avoid 
    // access violations (iterating loops that have had items removed)
    RemoveInactivePlayers ();
    RemoveInactiveWebClients ();
      
    // get ready for "select" function ... 
    FD_ZERO (&in_set);
    FD_ZERO (&out_set);
    FD_ZERO (&exc_set);

    // add our control sockets (for new connections)
    FD_SET (iMudControl, &in_set);
    FD_SET (iWebControl, &in_set);
    int iMaxdesc = max<int> (iMudControl, iWebControl);
    
    // set bits in in_set, out_set etc. for each connected players and web clients
    iMaxdesc = for_each (ISEQ (playerlist ()), setUpMudDescriptors (iMaxdesc)).GetMax ();
    iMaxdesc = for_each (ISEQ (webclientlist ()), setUpWebDescriptors (iMaxdesc)).GetMax ();

    // set up timeout interval
    struct timeval timeout;
    timeout.tv_sec = COMMS_WAIT_SEC;    // seconds
    timeout.tv_usec = COMMS_WAIT_USEC;  // + 1000th. of second

    // check for activity, timeout after 'timeout' seconds
    if (select (iMaxdesc + 1, &in_set, &out_set, &exc_set, &timeout) > 0)
      {
      // New connection on MUD control port?
      if (FD_ISSET (iMudControl, &in_set))
        ProcessNewMudConnection ();
      // New connection on web client control port?
      if (FD_ISSET (iWebControl, &in_set))
        ProcessNewWebConnection ();
      
      // handle all player input/output
      for_each (ISEQ (playerlist ()), processMudDescriptors ());      
      for_each (ISEQ (webclientlist ()), processWebDescriptors ());      
      } // end of something happened
  
    }  while (!bStopNow);   // end of looping processing input

}   // end of MainLoop

