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

/* Usage:
     cli3 (dotted-decimal-address | actual-name) [port]
   e.g.
     cli3 catsix
     cli3 203.154.146.136
     cli3 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
   represented as a changing image. This changes when:
     * the user changes room
     * someone enters/leaves the user's room
     * somebody changes their body form or mood
     * someone speaks

   The information is extracted from visualisation messages sent
   to the client by the server. The messages 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

   The image is created using the gd graphics library and
   displayed using the UNIX xv graphics library.
*/
/* Makes use of the gd graphics library, by
   Thomas Boutell and the Quest Protein Database Center
   at Cold Spring Harbor Labs.
   COPYRIGHT 1994,1995 BY THE QUEST PROTEIN DATABASE CENTER
   AT COLD SPRING HARBOR LABS.
*/
/* Extensions to cli2.c:
    * graphics operations
    * commented out print_status() calls, and other debugging printf()s
    * compilation: 
        \gcc -Wall cli3.c -o cli3 -L/home/ad/gd1.2 -lgd -lm
*/

#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>
#include "/home/ad/gd1.2/gd.h"
#include "/home/ad/gd1.2/gdfontl.h"   /* Large font */


#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*/

#define MAX_ROOMS 7         /* max number of rooms */
#define PICDIR "/home/ad/chat/client/pics/"   /* pics directory */
#define OPIC "room.gif"                        /* output picture */
#define BUBBLE "bubble.gif"                    /* speech bubble picture */
#define WIDTH 160          /* width of figure space in picture */
#define Y_BUBBLE 160       /* height of bubble in picture */
#define Y_BODY 300         /* height of figure in picture */
#define Y_NAME 40          /* height of name space in picture */


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


void start_client(int argc, char *argv[]);
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);

/* Graphic Operations */
void speak(int new_posn, Room *r);
gdImagePtr load_image(char nm[]);
int wipe_area(gdImagePtr im, int posn, int ystart, int ylength, Room *r);
void get_room_pic(char rnm[], char gifnm[]);
gdImagePtr load_room(char nm[]);
int draw_bubble(gdImagePtr im, int posn);
void store_image(gdImagePtr im);
void draw_scene(Room *r);
void draw_figure(int posn, Room *r);
void draw_name(gdImagePtr im, int posn, Room *r);
int draw_body(gdImagePtr im, int posn, Room *r);
void get_body_pic(enum Form f, enum Mood m, char gifnm[]);
void erase_figure(int posn, Room *r);
void change_body(int posn, Room *r);
void init_pic(void);


/* 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"}; 
char *rooms[MAX_ROOMS] = {"foyer", "garden", "road", "beach", "mountain",
                          "tvroom", "bedroom"};


void main(int argc, char *argv[])
/* Fork a child process to do the main client actions,
   and invoke xv in the parent. This allows the child to
   later terminate the parent via its process ID in quit_cmd().
   xv is put into polling mode (-poll) to reload the OPIC image
   whenever the file changes.
*/
{
  int pid;
  char pic_fnm[MAXLEN];

  init_pic();           /* clear PIC */
  pid = fork();
  if (pid == 0)         /* child process */
    start_client(argc, argv);
  else if (pid > 0) {   /* parent process */
    sprintf(pic_fnm, "%s%s", PICDIR, OPIC);
    printf("Parent: starting xv with %s ...\n", pic_fnm);
    if (execl("/usr/bin/X11/xv", "xv", "-viewonly", "-poll",
                            pic_fnm, (char *)0) == -1) {
      fprintf(stderr, "Parent: Cannot start xv\n");
      exit(1);
    }
  }
}


void start_client(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(). Uses the main() code in cli2.c.
*/
{
  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: cli3 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);     /* the user is speaking */
    }
  }
}


void quit_cmd(int sfd)
/* Terminate the parent process and xv, 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.
*/
{
  int ppid;

  printf("Terminating xv...\n");
  ppid = getppid();       /* get parent's process ID */
  kill(ppid, SIGINT);

  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))
    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 */
    speak(spot, r);
    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')    /* 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); */
  draw_scene(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)
{
  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); */
    draw_figure(spot, 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"); */
    erase_figure(spot, r);
    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); */
    change_body(spot, 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); */
    change_body(spot, 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);
}


/* Graphics Operations */

void speak(int new_posn, Room *r)
/* Remove old speech bubble; draw new one.
   Assume that the old speaker position is the one in r->curr_speaker.
*/
{
  gdImagePtr im;
  int old_posn = r->curr_speaker;

  if ((im = load_image(OPIC)) == NULL) {
    fprintf(stderr, "Cannot open output picture file: %s\n", OPIC);
    return;
  }
  if ((wipe_area(im, old_posn, 0, Y_BUBBLE, r)) &&   /* wipe old bubble */
      (draw_bubble(im, new_posn)) )                  /* draw new one */
    store_image(im);
  gdImageDestroy(im);
}


gdImagePtr load_image(char nm[])
/* Extract the image from the file nm, located in PICDIR. */
{
  char fnm[MAXLEN];
  FILE *fp;
  gdImagePtr im;

  sprintf(fnm, "%s%s", PICDIR, nm);
  if ((fp = fopen(fnm, "rb")) == NULL)
    return NULL;
  else {
    im = gdImageCreateFromGif(fp);
    fclose(fp);
    return im;
  }
}


int wipe_area(gdImagePtr im, int posn, int ystart, int ylength, Room *r)
/* Erase the image at posn starting from ystart, down ylength pixels */
{
  gdImagePtr roomim;
  char roompic[MAXLEN];
  int xcoord = posn*WIDTH;

  get_room_pic(r->rname, roompic);
  if ((roomim = load_room(roompic)) == NULL) {
    fprintf(stderr, "Cannot open room picture file: %s\n", roompic);
    return 0;
  }
  gdImageCopy(im, roomim, xcoord, ystart, xcoord, ystart, WIDTH, ylength);
  gdImageDestroy(roomim);
  return 1;
}


void get_room_pic(char rname[], char gifnm[])
/* Get the GIF filename corresponing to the room name. This
   does *not* include the directory location.
*/
{
  int i;

  for (i=0; i < MAX_ROOMS; i++)
    if (strcmp(rname, rooms[i]) == 0)
      break;
  if (i < MAX_ROOMS)
    strcpy(gifnm, rooms[i]);
  else
    strcpy(gifnm, rooms[0]);     /* first room is default */
  strcat(gifnm, ".gif");
}


gdImagePtr load_room(char nm[])
/* Extract the room image from the file nm, located in PICDIR.
   This differs from load_image() in that the image is scaled to 
   the size expected by OPIC.
*/
{
  char fnm[MAXLEN];
  FILE *fp;
  gdImagePtr im, roomim;
  int xlen, ylen, white;

  sprintf(fnm, "%s%s", PICDIR, nm);
  if ((fp = fopen(fnm, "rb")) == NULL)
    return NULL;
  else {
    im = gdImageCreateFromGif(fp);
    fclose(fp);
    xlen = ROOM_SIZE*WIDTH;             /* width of OPIC */
    ylen = Y_BUBBLE + Y_BODY + Y_NAME;  /* height of OPIC */
    roomim = gdImageCreate(xlen, ylen);
    white = gdImageColorAllocate(roomim, 255, 255, 255);
    gdImageCopyResized(roomim, im, 0, 0, 0, 0, xlen, ylen, im->sx, im->sy);
    gdImageDestroy(im);
    return roomim;
  } 
}


int draw_bubble(gdImagePtr im, int posn)
/* Draw a speech bubble, resized to fit the space */
{
  gdImagePtr bubbleim;
  int xcoord = posn*WIDTH;

  if ((bubbleim = load_image(BUBBLE)) == NULL) {
    fprintf(stderr, "Cannot open bubble picture file: %s\n", BUBBLE);
    return 0;
  }
  gdImageCopyResized(im, bubbleim, xcoord, 0, 0, 0, WIDTH, Y_BUBBLE,
                                 bubbleim->sx, bubbleim->sy);
  gdImageDestroy(bubbleim);
  return 1;
}


void store_image(gdImagePtr im)
/* Store the image in OPIC, the output picture file,
   located in the PICDIR directory. */
{
  char fnm[MAXLEN];
  FILE *fp;

  sprintf(fnm, "%s%s", PICDIR, OPIC);
  if ((fp = fopen(fnm, "wb")) == NULL)
    fprintf(stderr, "Cannot write to \"%s\"\n", fnm);
  else {
    gdImageGif(im, fp);
    fclose(fp);
  }
}


void draw_scene(Room *r)
/* Draw a complete scene: the background and all the figures */
{
  char roompic[MAXLEN];
  gdImagePtr im;
  int i;
  
  get_room_pic(r->rname, roompic);
  if ((im = load_room(roompic)) == NULL)
    fprintf(stderr, "Cannot load background image in \"%s\"\n", roompic);
  else {
    store_image(im);                   /* store background in output pic */
    gdImageDestroy(im);
    for (i=0; i < ROOM_SIZE; i++)
      if (r->vs[i].name[0] != '\0')    /* there is someone in the ith spot */
        draw_figure(i, r);
  }
}


void draw_figure(int posn, Room *r)
/* Draw a figure in OPIC (i.e. a body image and name). The function
   assumes that OPIC already contains a background, and that there
   is no figure already in that position.
   The vistor's name is written underneath the body image.
*/
{
  gdImagePtr im;

  if ((im = load_image(OPIC)) == NULL) {
    fprintf(stderr, "Cannot open output picture file: %s\n", OPIC);
    return;
  }
  if (draw_body(im, posn, r)) {
    draw_name(im, posn, r);
    store_image(im);
  }
  gdImageDestroy(im);
}


void draw_name(gdImagePtr im, int posn, Room *r)
/* Draw the name in a large black font centered in the
   name area of the image for posn. If the name is too
   wide then print noname instead. The name is printed
   with a white background to increase its visibility.
*/
{
  int black, white;                  /* colours used */
  int xposn, yposn, xright, yright;  /* coordinates */
  int msglen, nmsglen;               /* length of message and noname */
  char *noname = "***";

  black = gdImageColorAllocate(im, 0, 0, 0);
  white = gdImageColorAllocate(im, 255, 255, 255);

  yposn = Y_BUBBLE + Y_BODY + Y_NAME/2 - gdFontLarge->h/2;
  yright = yposn + gdFontLarge->h;

  msglen = strlen(r->vs[posn].name)*gdFontLarge->w;
  nmsglen = strlen(noname)*gdFontLarge->w;

  if (msglen > WIDTH) {     /* name is too wide */
    fprintf(stderr, "%s too wide for figure width\n", r->vs[posn].name);
    xposn = posn*WIDTH + WIDTH/2 - nmsglen/2;
    xright = xposn + nmsglen;
    gdImageFilledRectangle(im, xposn, yposn, xright, yright, white);
    gdImageString(im, gdFontLarge, xposn, yposn, noname, black);
  }
  else {    /* print name */
    xposn = posn*WIDTH + WIDTH/2 - msglen/2;
    xright = xposn + msglen;
    gdImageFilledRectangle(im, xposn, yposn, xright, yright, white);
    gdImageString(im, gdFontLarge, xposn, yposn, r->vs[posn].name, black);
  }
}


int draw_body(gdImagePtr im, int posn, Room *r)
/* Draw a body, resized to fit the space */
{
  gdImagePtr bodyim;
  char bodypic[MAXLEN];
  int xcoord = posn*WIDTH;

  get_body_pic(r->vs[posn].form, r->vs[posn].mood, bodypic);
  if ((bodyim = load_image(bodypic)) == NULL) {
    fprintf(stderr, "Cannot open body picture file: %s\n", bodypic);
    return 0;
  }
  gdImageCopyResized(im, bodyim, xcoord, Y_BUBBLE, 0, 0, WIDTH, Y_BODY,
                                bodyim->sx, bodyim->sy);
  gdImageDestroy(bodyim);
  return 1;
}


void get_body_pic(enum Form f, enum Mood m, char gifnm[])
/* Get the GIF filename for a given body form and mood.
   It has the form:
      <body-form><first-letter-of-mood>.gif
   The directory prefix is *NOT* added here.
*/
{
  int i, j, len;
  i = (int)f;
  j = (int)m;

  if ((i >= 0) && (i < MAXFORMS))
    strcpy(gifnm, forms[i]);
  else
    strcpy(gifnm, forms[0]);   /* default form is the first */
 
  len = strlen(gifnm);
  if ((j >= 0) && (j < MAXMOODS))
    gifnm[len] = moods[j][0];
  else
    gifnm[len] = moods[0][0];   /* default mood is the first */
  gifnm[len+1] = '\0';

  strcat(gifnm, ".gif");
}


void erase_figure(int posn, Room *r)
/* Erase the body, name and (any) speech bubble from the posn
   spot of OPIC. This is carried out by copying the relevant
   rectangle of the backgrouns into that figure area. */
{
  gdImagePtr im;

  if ((im = load_image(OPIC)) == NULL) {
    fprintf(stderr, "Cannot open picture file: %s\n", OPIC);
    return;
  }
  if (wipe_area(im, posn, 0, Y_BUBBLE+Y_BODY+Y_NAME, r))
    store_image(im);
  gdImageDestroy(im);
}


void change_body(int posn, Room *r)
/* Wipe old body; draw new one */
{
  gdImagePtr im;

  if ((im = load_image(OPIC)) == NULL) {
    fprintf(stderr, "Cannot open picture file: %s\n", OPIC);
    return;
  }
  if ((wipe_area(im, posn, Y_BUBBLE, Y_BODY, r)) &&    /* wipe old body */
      (draw_body(im, posn, r)) )                       /* draw new body */
    store_image(im);
  gdImageDestroy(im);
}


void init_pic(void)
/* The picture in OPIC is initialised as a white rectangle with 
   "Graphical Chat Interface" in a large black font in the middle.
*/
{
  gdImagePtr im;
  int white, black;
  char *msg = "Graphical Chat Interface";

  im = gdImageCreate(ROOM_SIZE*WIDTH, Y_BUBBLE+Y_BODY+Y_NAME);
  white = gdImageColorAllocate(im, 255, 255, 255);
  black = gdImageColorAllocate(im, 0, 0, 0);

  gdImageString(im, gdFontLarge, 
                   (im->sx/2) - (strlen(msg)*(gdFontLarge->w/2)),
                   (im->sy/2) - (gdFontLarge->h/2), msg, black);
  store_image(im);
  gdImageDestroy(im);
}



