/******************************************************************************
 Copyright (c) 2000-2001 Richard Woolcock

 Permission is hereby granted, free of charge, to any person obtaining a copy 
 of this software and associated documentation files (the "Software"), to deal 
 in the Software without restriction, including without limitation the rights 
 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 
 copies of the Software, and to permit persons to whom the Software is 
 furnished to do so, subject to the following conditions:

 The above copyright notice and this permission notice shall be included in all 
 copies or substantial portions of the Software.

 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 
 SOFTWARE.
 ******************************************************************************/

/******************************************************************************
 File Name        : sockets.c
 ******************************************************************************
 Description      : Contains the socket handling code.
 ******************************************************************************/

/******************************************************************************
 Required library files.
 ******************************************************************************/

#include <stdlib.h>
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include <unistd.h>
#include <ctype.h>
#include <netinet/in.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/socket.h>
#include "sockets.h"
#include "commands.h"
#include "glad.h"
#include "text_io.h"
#include "string.h"
#include "help.h"

/******************************************************************************
 Required global variables.
 ******************************************************************************/

body_t  stBodyEmpty;         /* Empty body structure for initialisation */
conn_t  stConnEmpty;         /* Empty connection structure for initialisation */
conn_t *pstFirstConn = NULL; /* Connection list */
body_t *pstFirstBody = NULL; /* Body list */
fd_set  a_sFd;               /* Array of socket flags */
bool    bShutdown = FALSE;   /* When TRUE, shut mud down */
FILE   *pFile = NULL;        /* File pointer */

/******************************************************************************
 Required operations.
 ******************************************************************************/

/* Function: InitSocket
 *
 * This function creates the control socket to listen for incoming
 * connections.
 *
 * The function takes no parameters.
 *
 * This function returns an int: The descriptor of the socket created.
 */
int InitSocket()
{
   static struct sockaddr_in   s_stSaEmpty; /* Empty structure */
   struct sockaddr_in   stSa = s_stSaEmpty; /* Clear stSa */
   struct linger stLd;
   int iFd;
   int iOpt = 1;

   /* Initialise the control socket data */
   stSa.sin_family = AF_INET;
   stSa.sin_addr.s_addr = INADDR_ANY;
   stSa.sin_port = htons(PORT);
   stLd.l_onoff  = 0;
   stLd.l_linger = 0;

   /* Create the control socket and stores it's descriptor in iFd */
   iFd = socket(AF_INET,SOCK_STREAM,0);

   /* Set the socket options */
   setsockopt(iFd,SOL_SOCKET,SO_REUSEADDR,(char*)&iOpt,sizeof(iOpt));
   setsockopt(iFd,SOL_SOCKET,SO_LINGER,&stLd,sizeof(stLd));

   /* Assign the name (stSa) to the control socket (iFd) */
   bind(iFd,(struct sockaddr*)&stSa,sizeof(stSa));

   /* Allow iFd to accept incoming connections, with up to 3 pending */
   listen(iFd,3); /* Backlog 3, max is 5 in BSD, SOMAXCONN in Linux */

   /* Return the description of the control socket */
   return ( iFd );
}


/* Function: CloseSocket
 *
 * This function closes the specified socket.
 *
 * The function takes one parameter, as follows:
 *
 * iSocket: The socket number to be closed.
 *
 * This function has no return value.
 */
void CloseSocket(int iSocket)
{
   /* Close the iSocket descriptor */
   close(iSocket);

   /* Remove iSocket from the descriptor set */
   FD_CLR(iSocket,&a_sFd);
}


/* Function: GetInput
 *
 * This function reads the input from the connection's socket then calls
 * the ParseInput function to parse the command.
 *
 * The function takes one parameter, as follows:
 *
 * pstConn: The connection to read the input from.
 *
 * This function returns a bool: TRUE means the read was successful, FALSE 
 * means it was not.
 */
bool GetInput(conn_t*pstConn)
{
   char a_chBuf[MAX_BUF] = {'\0'}; /* Text storage buffer */
   int  iInput;
   bool bReadLine = FALSE;
   int  i;

   /* Read the input from the socket */
   if ( ( iInput = read(pstConn->iSocket,a_chBuf,MAX_BUF) ) <=0 )
   {
      return ( FALSE ); /* Connection was lost while reading */
   }

   /* Store the usable (printing) characters in the connection's input buffer */
   for ( i = 0; i < iInput && pstConn->iInLen < MAX_BUF-1 && !bReadLine; i++ )
   {
      /* Only store the printing characters */
      if ( isprint(a_chBuf[i]) )
      {
         pstConn->a_chInBuf[pstConn->iInLen++] = a_chBuf[i];
      }
      else if ( a_chBuf[i] == '\n' )
      {
         bReadLine = TRUE; /* A full input line has been read */
      }
   }

   /* Terminate the connection's input buffer */
   pstConn->a_chInBuf[pstConn->iInLen] = '\0';

   /* If a full line has been read in */
   if ( bReadLine )
   {
      /* Parse the line of input */
      ParseInput(pstConn);

      /* Display the prompt (unless there was no input) */
      if ( pstConn->iInLen )
      {
         PutPrompt(pstConn);
      }

      /* Reset the input buffer */
      pstConn->iInLen = 0;
   }

   /* Input was successfully read from the connection */
   return ( TRUE );
}


/* Function: ParseInput
 *
 * This function parses a line of input and performs the appropriate
 * response.
 *
 * The function takes one parameter, as follows:
 *
 * pstConn: The connection to parse the response command to.
 *
 * This function has no return value.
 */
void ParseInput(conn_t*pstConn)
{
   char *szTxt = pstConn->a_chInBuf;
   int  i;

   /* Loop through the connection's input buffer */
   while ( *szTxt )
   {
      /* If a space is found */
      if ( *szTxt == ' ' )
      {
         /* Set the space to a string terminator and break from the loop */
         *szTxt++ = '\0';
         break;
      }
      szTxt++;
   }

   /* Make sure the command is alphabetic-only (because of wildcard cmp) */
   if ( !StringAlpha(pstConn->a_chInBuf) )
   {
      PutOutput(pstConn,TO_USER,"Commands must contain only letters.\n\r",pstConn->a_chInBuf);
      return;
   }

   /* Process the command */
   for ( i = 0; kstCmdTable[i].szCommand != NULL; i++ )
   {
      if ( StringCompare(pstConn->a_chInBuf, kstCmdTable[i].szCommand) == CMP_EQUAL )
      {
         if ( ( GetStatus(pstConn) & kstCmdTable[i].eType ) != 0 )
         {
            /* The command was found, so perform the appropriate function */
            (*kstCmdTable[i].pfnFunction)(pstConn,pstConn->a_chInBuf,szTxt);
            return; /* No point carrying on */
         }
      }
   }

   /* Inform the user that there is no such command (unless they just hit enter) */
   if ( pstConn->a_chInBuf[0] != '\0' )
   {
      PutOutput(pstConn,TO_USER,"Unrecognised command '%s'.  Type 'commands' to list available options.\n\r",pstConn->a_chInBuf);
   }
   else /* If they just hit enter, send "nothing" (this is required for screen formatting) */
   {
      PutOutput(pstConn,TO_USER,"");
   }
}


/* Function: PutOutput
 *
 * This function sends the specified string to the specified connection.
 *
 * The function takes four parameters, as follows:
 *
 * pstConn: The connection sending the string.
 * szTxt:   The string to be sent.
 * eOut:    Who the string should be sent to.
 * ...:     One or more other arguments.
 *
 * This function has no return value.
 */
void PutOutput( conn_t* pstConn, out_t eOut, char* szTxt, ... )
{
   va_list pArg;             /* Pointer to the next argument */
   bool    bRePrompt = FALSE;
   conn_t *pstUser;
   char    a_chBuf[MAX_BUF];

   /* Variable arguments lists "start" macro */
   va_start(pArg,szTxt);

   /* Store szTxt in a_chBuf as per sprintf, using pArg for arguments */
   vsprintf(a_chBuf,szTxt,pArg);

   /* Loop through all connections */
   for ( pstUser = pstFirstConn; pstUser != NULL; pstUser = pstUser->pstNext )
   {
      /* Determine who should get sent the text */
      switch(eOut)
      {
         case TO_USER:/* Text goes to just the user */
            if ( pstUser == pstConn ) 
               break;
            else
               continue;
         case TO_ROOM:/* Text goes to all in users room - except user */
            if ( pstUser != pstConn && GetRoom(pstConn) == GetRoom(pstUser) )
               break;
            else
               continue;
         case TO_WORLD:/* Text goes to all in world - except user */
            if ( pstUser != pstConn )
               break;
            else
               continue;
         case TO_ALL:/* Text goes to all in world - including user */
            break;
      }

      /* Determine if their prompt needs to be redrawn first */
      if ( pstUser->iOutLen == 0 && pstUser->iInLen == 0 )
      {
         /* Redraw the prompt */
         pstUser->a_chOutBuf[pstUser->iOutLen++] = '\n';
         pstUser->a_chOutBuf[pstUser->iOutLen++] = '\r';
         bRePrompt = TRUE;
      }

      /* Set szTxt to points to the start of the reformated text */
      szTxt = a_chBuf;

      /* Display the text to the current connection */
      while ( *szTxt )
      {
         pstUser->a_chOutBuf[pstUser->iOutLen++] = *szTxt++;
         if ( pstUser->iOutLen >= MAX_BUF-1 )
         {
            FlushOutput(pstUser);
         }
      }

      /* Redraw the prompt if required */
      if ( bRePrompt )
      {
         PutPrompt(pstUser);
      }
   }

   /* Variable arguments lists "end" macro */
   va_end(pArg);
}


/* Function: SendToBody
 *
 * This function sends the specified string to the specified connection in the 
 * same way as PutOutput, except that it goes to the body rather than the 
 * connection and can also handle dynamic text_io variables.
 *
 * The function takes four parameters, as follows:
 *
 * pstBody: The body sending the string.
 * szTxt:   The string to be sent.
 * eOut:    Who the string should be sent to.
 * ...:     One or more other arguments.
 *
 * This function has no return value.
 */
void SendToBody( body_t* pstBody, out_t eOut, char* szTxt, ... )
{
   va_list pArg;             /* Pointer to the next argument */
   bool    bRePrompt = FALSE;
   body_t *pstUser;
   char    a_chBuf[MAX_BUF];
   char    a_chDynamic[MAX_BUF];

   /* Variable arguments lists "start" macro */
   va_start(pArg,szTxt);

   /* Store szTxt in a_chBuf as per sprintf, using pArg for arguments */
   vsprintf(a_chBuf,szTxt,pArg);

   /* Loop through all connections */
   for ( pstUser = pstFirstBody; pstUser != NULL; pstUser = pstUser->pstNext )
   {
      if ( pstUser->pstConn == NULL )
      {
         /* Ignore link-dead players */
         continue;
      }

      /* Determine who should get sent the text */
      switch(eOut)
      {
         case TO_USER:/* Text goes to just the user */
            if ( pstUser == pstBody ) 
               break;
            else
               continue;
         case TO_ROOM:/* Text goes to all in users room - except user */
            if ( pstUser != pstBody && pstBody->iRoom == pstUser->iRoom )
               break;
            else
               continue;
         case TO_WORLD:/* Text goes to all in world - except user */
            if ( pstUser != pstBody )
               break;
            else
               continue;
         case TO_ALL:/* Text goes to all in world - including user */
            break;
      }

      /* Determine if their prompt needs to be redrawn first */
      if ( pstUser->pstConn->iOutLen == 0 && pstUser->pstConn->iInLen == 0 )
      {
         /* Redraw the prompt */
         pstUser->pstConn->a_chOutBuf[pstUser->pstConn->iOutLen++] = '\n';
         pstUser->pstConn->a_chOutBuf[pstUser->pstConn->iOutLen++] = '\r';
         bRePrompt = TRUE;
      }

      /* Convert any dynamic description tags */
      TextParse( pstBody, a_chBuf, a_chDynamic );

      /* Set szTxt to points to the start of the reformated text */
      szTxt = a_chDynamic;

      /* Display the text to the current connection */
      while ( *szTxt )
      {
         pstUser->pstConn->a_chOutBuf[pstUser->pstConn->iOutLen++] = *szTxt++;
         if ( pstUser->pstConn->iOutLen >= MAX_BUF-1 )
         {
            FlushOutput(pstUser->pstConn);
         }
      }

      /* Redraw the prompt if required */
      if ( bRePrompt )
      {
         PutPrompt(pstUser->pstConn);
      }
   }

   /* Variable arguments lists "end" macro */
   va_end(pArg);
}


/* Function: FlushOutput
 *
 * This function flushes the output buffer for the specified connection.
 *
 * The function takes one parameter, as follows:
 *
 * pstConn: The connection to be flushed.
 *
 * This function has no return value.
 */
void FlushOutput( conn_t* pstConn )
{
   /* Check that the connection's output buffer isn't empty */
   if ( pstConn->iOutLen > 0 )
   {
      /* Write the content of the output buffer to the socket descriptor */
      write(pstConn->iSocket,pstConn->a_chOutBuf,pstConn->iOutLen);

      /* Indicate that the output buffer is now empty */
      pstConn->iOutLen = 0;
   }
}


/* Function: PutPrompt
 *
 * This function displays the prompt.
 *
 * The function takes one parameter, as follows:
 *
 * pstConn: The connection to send the prompt string to.
 *
 * This function has no return value.
 */
void PutPrompt( conn_t *pstConn )
{
   /* Call PutOutput, passing in the prompt text */
   if ( pstConn->pstBody != NULL && pstConn->pstBody->szPrompt != NULL )
   {
      SendToBody( pstConn->pstBody, TO_USER, pstConn->pstBody->szPrompt );
   }
   else if ( pstConn->pstBody != NULL && GetStatus(pstConn) == FIGHTING )
   {
      /* Combat prompt shows your remaining actions */
      PutOutput( pstConn, TO_USER, "(Actions: Left %d, Right %d, Eyes %d, Legs %d)> ",
         pstConn->pstBody->iActions[AP_LEFT_HAND],
         pstConn->pstBody->iActions[AP_RIGHT_HAND],
         pstConn->pstBody->iActions[AP_EYES],
         pstConn->pstBody->iActions[AP_LEGS] );
   }
   else
   {
      PutOutput( pstConn, TO_USER, "> " ); /* Default prompt is "> " */
   }
}


/* Function: NewConnection
 *
 * This function creates a new connection.
 *
 * The function takes one parameter, as follows:
 *
 * iSocket: The new connection's socket number.
 *
 * This function returns a bool: TRUE if the connection could be created,
 * and FALSE if it could not.
 */
bool NewConnection( int iSocket )
{
   struct sockaddr_in stSa;
   int    iSaSize;
   conn_t*pstConn;

   /* Attempt to allocate space for a new connection */
   if ( !(pstConn = (conn_t*) malloc(sizeof(conn_t)) ) )
   {
      /* If unable to malloc, tidy up and return FALSE */
      CloseSocket(iSocket);
      Log("BUG: Cannot malloc.\n");
      return ( FALSE );
   }

   /* Add the new connection to the active socket list */
   FD_SET(iSocket,&a_sFd);

   /* Clear the connection structure */
   *pstConn = stConnEmpty;

   /* Insert the new connection into the connection list */
   if ( pstFirstConn )
   {
      pstFirstConn->pstPrev = pstConn;
      pstConn->pstNext = pstFirstConn;
   }

   /* The new connection goes on the front of the list */
   pstFirstConn = pstConn;

   /* Initialise the new connection structure */
   pstConn->iSocket = iSocket;

   /* Calculate and store the ip address of the new connection */
   iSaSize = sizeof(stSa);
   getpeername(iSocket,(struct sockaddr*)&stSa,&iSaSize);
   pstConn->iIp = ntohl(stSa.sin_addr.s_addr);

   /* Log the connection */
   Log("Connection from %d.%d.%d.%d\n",
      (pstConn->iIp >> 24 ) & 255,   /* 11111111 00000000 00000000 00000000 */
      (pstConn->iIp >> 16 ) & 255,   /* 00000000 11111111 00000000 00000000 */
      (pstConn->iIp >> 8  ) & 255,   /* 00000000 00000000 11111111 00000000 */
      (pstConn->iIp       ) & 255 ); /* 00000000 00000000 00000000 11111111 */

   /* Greet the new connection */
   PutHelpText( pstConn, "@welcome" );

   /* New connection was successfully created, so return TRUE */
   return ( TRUE );
}


/* Function: CloseConnection
 *
 * This function closes the specified connection.
 *
 * The function takes one parameter, as follows:
 *
 * pstConn: The connection to be closed.
 *
 * This function has no return value.
 */
void CloseConnection( conn_t* pstConn )
{
   /* Must seperate them from their body, if they have one */
   if ( pstConn->pstBody != NULL )
   {
      Log("%s has dropped link.\n\r",pstConn->pstBody->a_chName);
      PutOutput(pstConn,TO_WORLD,"%s drops link.\n\r",pstConn->pstBody->a_chName);
      pstConn->pstBody->pstConn = NULL;
      pstConn->pstBody = NULL;
   }

   /* Have a quick flush in case there is any pending output */
   FlushOutput(pstConn);

   /* If the connection was first in the list, reset the start of list */
   if ( pstFirstConn == pstConn )
   {
      pstFirstConn = pstConn->pstNext;
   }

   /* Remove the connection from the list */
   if ( pstConn->pstPrev )
   {
      pstConn->pstPrev->pstNext = pstConn->pstNext;
   }

   if ( pstConn->pstNext )
   {
      pstConn->pstNext->pstPrev = pstConn->pstPrev;
   }

   /* Close and free the socket */
   CloseSocket(pstConn->iSocket);
   free(pstConn);
}