
/* cli2.c */
/* Andrew Davison (ad@ratree.psu.ac.th) */
/* 20th January 1997 */

/* Usage:                                        
     cli2 (dotted-decimal-address | actual-name) [port]
   e.g.
     cli2 catsix  
     cli2 203.154.146.136
     cli2 catsix.coe.psu.ac.th 2002
*/
/* A simple client application for communicating with a
   *chat* server at a specified address and port.

   Information about the room currently occupied by the user is
   printed out as circumstances change (e.g. people enter/leave,
   talk, change their mood and/or body form). These details
   is extracted from the messages sent to the client by the 
   Chat server.

   Room messages are annotated output from the server.
   They have the form:
      <letter>$ text...
   The letters, and the meaning of the associated text:
      w     text contains information on the people in the room
      e     text is about another person entering the user's current room
      l     text is about another person leaving the user's current room
      m     text is about a person's new mood
      f     text is about a person's new body form
      u     text is about user's name -- appears only at start-up
      t     text is about another person speaking
*/
/* Extensions to cli.c:
    * Visitor and Room data structures
    * moods and forms
    * monitoring of room messages to update the Room d.s
*/

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>        /* inet_aton() */
#include <bstring.h>          /* bcopy() */
#include <unistd.h>           /* read(), write(), close() */
#include <time.h>

#define MAXLEN 120         /* max length of a string */
#define PORT 2001          /* default port number */

#define ROOM_SIZE 5        /* maximum number of people allowed in a room */
#define MAXMOODS 3         /* max number of moods */
#define MAXFORMS 7         /* max number of body forms*/

enum Mood {OK, HAPPY, SAD};
enum Form {NEWBIE, DANDY, HACKER, MANAGER, BOBBIE, WOOF, GRAD};

typedef struct {
  char name[MAXLEN];  /* visitor's name */
  enum Mood mood;
  enum Form form;
} Visitor;           /* information about a room visitor */

typedef struct {
  char rname[MAXLEN];       /* room name */
  int curr_speaker;         /* position of speaker in vs[] */
  int uposn;                /* position of user in vs[] */ 
  Visitor vs[ROOM_SIZE];    /* room visitors */
} Room;              /* room information */


struct sockaddr_in get_addr(int argc, char *argv[]);
int open_socket(struct sockaddr_in naddr);
void wait_contact(int sfd, fd_set *rmp);
void process_user(int sfd, Room *r);
void quit_cmd(int sfd);
void process_server(int sfd, Room *r);
int read_line(int sfd, char str[]);
void catch_interrupt(int signo);

/* Room Operations */
void init_room(Room *r);
void set_speaker(int spot, Room *r);
int room_message(char str[]);
void update_room(char str[], Room *r);
void who_details(char str[], Room *r);
void get_word_before(char c, char str[], int *posn, char word[]);
void get_visitor(char str[], int *posn, Room *r, int numv);
void print_status(Room *r);
void enter_details(char str[], Room *r);
int find_empty(Room *r);
void leave_details(char str[], Room *r);
int find_spot(char name[], Room *r);
void mood_details(char str[], Room *r);
enum Mood map_mood(char str[]);
void form_details(char str[], Room *r);
enum Form map_form(char str[]);
void user_details(char str[], Room *r);
void talk_details(char str[], Room *r);

/* Global Data */
int ssockfd;               /* global server socket descriptor;
                              used by catch_interrupt() */
/* Must correspond to the enumeration types above */
char *moods[MAXMOODS] = {"ok", "happy", "sad"};
char *forms[MAXFORMS] = {"newbie", "dandy", "hacker", "manager", "bobbie",
                         "woof", "grad"}; 


void main(int argc, char *argv[])
/* Create a socket link to the server and then repeatedly wait
   for user or server input. Ctrl-c's are dealt with by
   catch_interrupt()
*/
{
  Room r;
  struct sockaddr_in netaddr;
  fd_set readmask;

  signal(SIGINT, catch_interrupt);      /* catch ctrl-c */

  netaddr = get_addr(argc, argv);
  ssockfd = open_socket(netaddr);

  init_room(&r);
  while(1) {
    wait_contact(ssockfd, &readmask);

    if (FD_ISSET(ssockfd, &readmask))     /* data to read from server */
      process_server(ssockfd, &r);
    if (FD_ISSET(0, &readmask))           /* data to read from user */
      process_user(ssockfd, &r);
  }
  /* execution never reaches here */
}


struct sockaddr_in get_addr(int argc, char *argv[])
/* Create address structure using the given internet address
   (either in dotted decimal form or an actual name) and 
   an optional port number.
*/
/* Similar to an example in Stevens p.397-399 */
/* inet_aton() may have to be replace by inet_addr() 
   in some UNIX's */
{
  struct sockaddr_in addr;        /* described on p.264 */
  struct in_addr iaddr;           /* described on p.264 */
  struct hostent *hp;             /* p.393 */
  int port;

  if ((argc < 2) || (argc > 3)) {
    fprintf(stderr, "Usage: cli2 address [port]\n");
    exit(1);
  }

  if (argc == 2)
    port = PORT;      /* default value */
  else {      
    port = atoi(argv[2]);
    if (port < 0) {
      fprintf(stderr, "The port number cannot be < 0\n");
      exit(1);
    }
  }

  bzero(&addr, sizeof(addr));
  addr.sin_family = AF_INET;
  addr.sin_port = htons(port);          /* htons(): see Stevens p.276 */

  if (inet_aton(argv[1], &iaddr) != -1)       /* dotted-decimal or alias */
    bcopy(&iaddr, &addr.sin_addr, sizeof(iaddr));
  else if ((hp = gethostbyname(argv[1])) != NULL)  /* lookup name */
    bcopy(hp->h_addr, &addr.sin_addr.s_addr, hp->h_length);
  else {
    fprintf(stderr, "Unknown address\n");
    exit(1);
  }
  return addr;
}


int open_socket(struct sockaddr_in netaddr)
/* Create a socket data structure, initialise it with the
   server address details, and then try to connect to it.
*/
/* See Stevens p.286 */
{
  int sd;

  if ((sd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
    fprintf(stderr, "socket() error\n");
    exit(1);
  }
  if (connect(sd, (struct sockaddr *)&netaddr, sizeof(netaddr)) < 0) {
    fprintf(stderr, "Can not connect\n");
    exit(1);
  }
  return sd;
}

void wait_contact(int sfd, fd_set *rmp)
/* Wait for input either from the user or the server */
{
  int maxfd, numfound;

  maxfd = sfd + 1;

  /* build read-mask for select(), see Stevens p.595 */
  FD_ZERO(rmp);
  FD_SET(0, rmp);        /* to monitor user input */
  FD_SET(sfd, rmp);      /* to monitor server */

  numfound = select(maxfd, rmp, (fd_set *)0, (fd_set *)0,
                                        (struct timeval *) 0);
  if (numfound < 0) {
    fprintf(stderr, "select() error\n");
    exit(1);
  }
}


void process_user(int sfd, Room *r)
/* Read input from the user and pass it unchanged to the
   server except when a ".quit" command is typed. In that   
   case, call quit_cmd().
*/
{
  int len;
  char str[MAXLEN];                                               

  len = read_line(0, str);   /* 0 is input file descriptor */

  if (len < 0) {
    fprintf(stderr, "Fatal user error\n");
    quit_cmd(sfd);
  }
  else if (len == 0)                           
    fprintf(stderr, "Empty user input being ignored\n");
  else {                             
    if (str[0] == '.') {        /* input is a command */
      if ((str[1] == 'q') || (str[1] == 'Q'))    /* a ".quit" command */
        quit_cmd(sfd);
      else
        write(sfd, str, len);   /* pass other cmds straight to server */
    }
    else {                      /* input is not a command */
      write(sfd, str, len);       /* pass straight to server */
      set_speaker(r->uposn, r);   /* NEW: the user is speaking */
    }
  }
}


void quit_cmd(int sfd)
/* Send a ".quit" message to the server and then terminate.
   Do not wait for any server response (e.g. "goodbye").
   cli.c uses quit_cmd() to terminate.
*/
{
  write(sfd, ".quit\n", 6);
  close(sfd);
  printf("Sent \".quit\" to server. Now terminating...\n");
  exit(0);
}


void process_server(int sfd, Room *r)
/* Read the server input and print it out unaltered. 
   Process any room messages in the input.
*/
{
  char str[MAXLEN];

  if (read_line(sfd, str) <= 0) {
    fprintf(stderr, "Server connection has been lost. Terminating...\n");
    exit(1);
  }

  if (room_message(str))    /* NEW: is str a room message? */
    update_room(str, r); 
  fputs(str, stdout); 
}


int read_line(int sfd, char str[])
/* Read up to and including the newline in the socket buffer
   by reading 1 character at a time. Return the line length.
*/
{
  int i = 0, len;

  while ((i < MAXLEN-1) && ((len = read(sfd, &str[i], 1)) == 1) &&
         (str[i] != '\n'))
    i++;

  if (len <= 0)
    return len;

  if (i == MAXLEN-1)
    fprintf(stderr, "Input string too long\n");
  else
    i++;      /* move past newline */
  str[i] = '\0';
  return i;
}


void catch_interrupt(int signo)
/* This function is called when a ctrl-c is typed.
   Further ctrl-c's are ignored and the client 'gracefully'
   terminates by calling quit_cmd().                               
   This function may have to return int in some UNIX's.       
   Signals are described in Stevens p.43-51            
*/                        
{
  signal(SIGINT, SIG_IGN);       /* ignore further ctrl-c's */
  quit_cmd(ssockfd);             /* uses global ssockfd */
}


/* Room operations */

void init_room(Room *r)
/* Initialise the room to be empty */
{
  int i;

  r->rname[0] = '\0';
  r->curr_speaker = -1;
  r->uposn = -1;
  for (i=0; i < ROOM_SIZE; i++) {
    r->vs[i].name[0] = '\0';
    r->vs[i].mood = (enum Mood)0;
    r->vs[i].form = (enum Form)0;
  }
}


void set_speaker(int spot, Room *r)
/* The if-test is used to stop the room details being updated
   when the user first communicates with the server to
   input his/her name. At that point. the user is not in Chat, 
   and so not in a room.
*/
{
  if (r->rname[0] != '\0') {    /* if in a room */
    r->curr_speaker = spot;
    printf("New speaker in %s: %s\n", r->rname, r->vs[spot].name);
  }
}


int room_message(char str[])
/* A room message starts a single line with:
     <letter>$ text...
*/
{
  if (str[0] == '\n')    /* the server message is only a newline */
    return 0;

  if ((str[1] == '$') && (str[2] == ' '))
    return 1;
  else
    return 0;
}


void update_room(char str[], Room *r)
/* str[] has the form:
     <letter>$ text...
   The text is processed by a different "_details" function
   depending on the letter value.
*/
{
  if (str[0] == 'w')
    who_details(str+3, r);
  else if (str[0] == 'e')
    enter_details(str+3, r);
  else if (str[0] == 'l')
    leave_details(str+3, r);
  else if (str[0] == 'm')
    mood_details(str+3, r);
  else if (str[0] == 'f')
    form_details(str+3, r);
  else if (str[0] == 'u')
    user_details(str+3, r);
  else if (str[0] == 't')
    talk_details(str+3, r);
  else
    fprintf(stderr, "Room message code \'%c\' not understood\n", str[0]);
}


void who_details(char str[], Room *r)
/* 
   When the user first enters a room (or when a ".who" command is 
   issued), the server sends the client a 'w' message which is 
   processed here. The room details in r are reinitialised. 
   However the visitors list in str[] may not match the one stored 
   in vs[]. Thus, the user's name is extracted from vs[] before r 
   is updated so that he can be identified afterwards. Also, the 
   current speaker is set to -1 (no one is talking).
   Format of str[] is:
      text... <room>:  <visitor-name> (<mood-digit>.<form-digit>)  ...\n
*/
{
  char uname[MAXLEN];
  int posn = 0, numv = 0, uposn = -1;

  strcpy(uname, r->vs[r->uposn].name);   /* remember user's name */
  init_room(r);

  get_word_before(':', str, &posn, r->rname);
  posn = posn + 3;        /* skip over ':' and 2 spaces */
  
  while ((numv < ROOM_SIZE) && (str[posn] != '\n')) {
    get_visitor(str, &posn, r, numv);
    if (strcmp(r->vs[numv].name, uname) == 0)
      uposn = numv;          /* remember user's new room position */
    numv++;
  }

  r->uposn = uposn;
  if ((numv == ROOM_SIZE) && (str[posn] != '\n'))
    fprintf(stderr, "Too many visitors in the room, ignoring some\n");

  print_status(r);
}


void get_word_before(char c, char str[], int *posn, char word[])
/* Get the word before character c in str[]. Start searching from
   str[*posn] */
{
  int i, j, left;

  while (str[*posn] != c)  /* find c */
    (*posn)++;
  
  left = *posn - 1;        /* one char before c */
  while ((left >= 0) && (str[left] != ' '))
    left--;

  i = left + 1;            /* one char after space */
  j = 0;
  while ((i < *posn) && (j < MAXLEN-1))
    word[j++] = str[i++];
  word[j] = '\0';
}


void get_visitor(char str[], int *posn, Room *r, int numv)
/* Format of this part of str[] is:
      <visitor-name> (<mood-digit>.<form-digit>)
   followed by two spaces
*/
{
  get_word_before(' ', str, posn, r->vs[numv].name);
  *posn = *posn + 2;           /* skip space and '(' */
  r->vs[numv].mood = (enum Mood)(str[*posn] - '0');
  *posn = *posn + 2;           /* skip mood-digit and '.' */
  r->vs[numv].form = (enum Form)(str[*posn] - '0');
  *posn = *posn + 4;           /* ship form-digit, ')' and 2 spaces */
}


void print_status(Room *r)
/* Print the contents of r. Useful for debugging. */
{
  int i;

  printf("----Room Status----\n");
  if (r->rname[0] == '\0')
    printf("No current room\n");
  else
    printf("Room: %s\n", r->rname);

  if (r->curr_speaker == -1)
    printf("No current speaker\n");
  else
    printf("Current Speaker: %s\n", r->vs[r->curr_speaker].name);

  printf("User position: %d\n", r->uposn);

  printf("Visitors:  ");
  for (i=0; i < ROOM_SIZE; i++)
    if (r->vs[i].name[0] != '\0')
      printf("%s (%d.%d)  ", r->vs[i].name, r->vs[i].mood, r->vs[i].form);
  printf("\n--------\n");
}


void enter_details(char str[], Room *r)
/* Record info about new person entering the user's room.
   Format of str[] is:
     <visitor-name> (<mood-digit>.<form-digit>) text...
*/
{
  int posn = 0, spot;

  if ((spot = find_empty(r)) == -1)
    fprintf(stderr, "Room too full; ignoring new person\n");
  else {
    printf("Adding new person to room details\n");
    get_visitor(str, &posn, r, spot);
    print_status(r);
  }
}


int find_empty(Room *r)
/* Find an empty spot in r->vs[], which is identified by
   the name field containing only '\0'. Return the index 
   position. If there is no free space then return -1.
*/
{
  int i = 0;

  while (i < ROOM_SIZE) {
    if (r->vs[i].name[0] == '\0')
      return i;
    i++;
  }
  return -1;
}


void leave_details(char str[], Room *r)
/* Record departure of a person from the user's room.
   Format of str[]:
     <name> text...
*/
{
  int posn = 0, spot;
  char name[MAXLEN];

  get_word_before(' ', str, &posn, name);

  if ((spot = find_spot(name, r)) == -1)
    fprintf(stderr, "%s is not recorded in the room details\n", name);
  else {
    printf("Removing person from room details\n");
    r->vs[spot].name[0] = '\0';
    print_status(r);
  }
}


int find_spot(char name[], Room *r)
/* Return the index position in r->vs[] which contains a name;
   return -1 if the name cannot be found. */
{
  int i = 0;

  while (i < ROOM_SIZE) {
    if (strcmp(r->vs[i].name, name) == 0)
      return i;
    i++;
  }
  return -1;
}



void mood_details(char str[], Room *r)
/* Record the new mood of a person in the room (which may be
   the user). Format of str[]:
     <name>'s mood: <mood-word>\n
*/
{
  int posn = 0, spot;
  char name[MAXLEN], moodstr[MAXLEN];

  get_word_before('\'', str, &posn, name);

  if ((spot = find_spot(name, r)) == -1)
    fprintf(stderr, "%s is not recorded in the room details\n", name);
  else {
    printf("Updating %s's mood information\n", name);
    get_word_before('\n', str, &posn, moodstr);
    r->vs[spot].mood = map_mood(moodstr);
    print_status(r);
  }
}


enum Mood map_mood(char str[])
/* str[] contains a mood word, which must be converted to
   an enum Mood value. */
{
  int i;
  for (i=0; i < MAXMOODS; i++)
     if (str[0] == moods[i][0])
       return (enum Mood) i;

  fprintf(stderr, "%s is not a recognised mood; setting to \"%s\"\n", 
                                    str, moods[0]);
  return (enum Mood) 0;        /* first mood is the default */
}


void form_details(char str[], Room *r)
/* Record the new body form of a person in the room (which may be
   the user). Format of str[]:
     <name>'s form: <form-word>\n
*/
{
  int posn = 0, spot;
  char name[MAXLEN], formstr[MAXLEN];

  get_word_before('\'', str, &posn, name);

  if ((spot = find_spot(name, r)) == -1)
    fprintf(stderr, "%s is not recorded in the room details\n", name);
  else {
    printf("Updating %s's form information\n", name);
    get_word_before('\n', str, &posn, formstr);
    r->vs[spot].form = map_form(formstr);
    print_status(r);
  }
}


enum Form map_form(char str[])
/* str[] contains a form word, which must be converted to
   an enum Form value. */
{
  int i;
  for (i=0; i < MAXFORMS; i++)
     if (str[0] == forms[i][0])
       return (enum Form) i;

  fprintf(stderr, "%s is not a recognised form; setting to \"%s\"\n", 
                                    str, forms[0]);
  return (enum Form) 0;        /* first body form is the default */
}


void user_details(char str[], Room *r)
/* This function is always called at the beginning of the user's
   session with Chat, and so it can assume that vs[0] is empty.
   Format of str[]:
     <name>, text...
*/
{
  int posn = 0;

  get_word_before(',', str, &posn, r->vs[0].name);
  printf("User's name: %s\n", r->vs[0].name);
  r->uposn = 0;
}


void talk_details(char str[], Room *r)
/* Someone (other than the user) has spoken.
   Format of str[]:
     <name>: text...
*/
{
  int posn = 0, spot;
  char name[MAXLEN];

  get_word_before(':', str, &posn, name);

  if ((spot = find_spot(name, r)) == -1)
    fprintf(stderr, "%s is not recorded in the room details\n", name);
  else
    set_speaker(spot, r);
}


