/*
  telnet-chat protocol
  25feb2001 Junichi Uekawa

  copyright 2001 Junichi Uekawa
  see GPL version 2 or later for license
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/poll.h>
#include <netinet/in.h>
#include <netdb.h>
#include <pthread.h>

#define MAXHOSTNAMELEN 256	/* max hostname len per manpage  */
#define BACKLOG 5		/* max backlog to listen to */

#define CHARACTERBUFSIZE 80	/* character buffer size  with "[user]: \r\n"*/
#define USERNAMELENGTH 10	/* user name length */
#define INCOMINGCHARACTERBUFSIZE (CHARACTERBUFSIZE - (USERNAMELENGTH + 3 + 2 ) ) 

struct handle_incoming_params
{
  int t ;
  struct sockaddr_in *tmpisap;
  char * incomingname ;		/* the name of the user.*/
  char * currentbuffer ;
  pthread_mutex_t currentbuffer_used ; 
  int counter ;			/* position of currentbuffer */
} handle_incoming_params ;

#define malloc_with_threadexit(A,BYTES) if (NULL==((A)=malloc(BYTES)))\
{fprintf(stderr, "out of memory in allocating %i bytes\n", BYTES);pthread_exit(NULL);}
				/* the following is paired. */
static char messagesenttoothers [CHARACTERBUFSIZE] ;
static pthread_mutex_t sendmessagetoothers_mutex = PTHREAD_MUTEX_INITIALIZER;
				/* get this for writing */
static pthread_cond_t sendmessagetoothers_broadcast = PTHREAD_COND_INITIALIZER;
				/* get this for waiting */
static pthread_mutex_t waitingsendmessagetoothers_mutex = PTHREAD_MUTEX_INITIALIZER;

				/* lock this for printf/puts to stdout*/
static pthread_mutex_t printf_mutex = PTHREAD_MUTEX_INITIALIZER;


				/* lock this for reading (and for cond) */

static void sendmessagetoothers ( char * buf, char * incomingname)
{
				/* send message to others */
  pthread_mutex_lock ( &waitingsendmessagetoothers_mutex);/* other processes are not trying to read this. (i.e. the data is already read) */
  pthread_mutex_lock ( &sendmessagetoothers_mutex); /* I am the only sendmessagetoothers process */
  sprintf(messagesenttoothers, "[%s]:%s\r\n", incomingname, buf);
  pthread_mutex_unlock (&waitingsendmessagetoothers_mutex);
  pthread_cond_broadcast (&sendmessagetoothers_broadcast);
  pthread_mutex_unlock (&sendmessagetoothers_mutex);
}


/*
 * Handle commands specified.
 * when this function is called, the string is already locked.
 * handles  /nick {nickname}  command
 */
static void read_processcommands(struct handle_incoming_params * parameter)
{
				/* accept /nick  command */
  if (!strncmp("/nick ", parameter->currentbuffer, 6))
    {
      pthread_mutex_lock(&printf_mutex);
      printf ("[%s] changed the nick to ", parameter->incomingname);
      
      memset (parameter->incomingname, 0, USERNAMELENGTH+1);
      strncpy(parameter->incomingname, parameter->currentbuffer + 6, USERNAMELENGTH);
      printf ("[%s] \n", parameter->incomingname);
      pthread_mutex_unlock(&printf_mutex);
    }  
}


/*
 * This thread reads from each user connected via TCP/IP.
 */
static void handle_read ( struct handle_incoming_params * parameter)
{				/* handle reading from user */
				/* wait for input from the user
				 and when something comes in, signal 
				 everyone that something has come in. */
  char  incomingbuf [2] ;	/* reads one char */
  
  pthread_mutex_lock(&parameter->currentbuffer_used);
  parameter->counter=0;  
  malloc_with_threadexit(parameter->currentbuffer, CHARACTERBUFSIZE + 1);
  pthread_mutex_unlock(&parameter->currentbuffer_used);

  sendmessagetoothers("--- start connection", parameter->incomingname);
  pthread_mutex_lock(&printf_mutex);
  printf ("%s connected\n", parameter->incomingname);
  pthread_mutex_unlock(&printf_mutex);  

  while (1)
    {
      if (read (parameter->t, incomingbuf, 1) == 1 )
	{
	  incomingbuf[1]=0;
	  if (incomingbuf[0]=='\r')
	    continue ;
	  if (incomingbuf[0]=='\n') 
	    {
	      pthread_mutex_lock(&parameter->currentbuffer_used);
	      parameter->currentbuffer [ parameter->counter ] = 0 ;
	      sendmessagetoothers(parameter->currentbuffer, parameter->incomingname);
	      if (parameter->currentbuffer[0] == '/')
		read_processcommands(parameter);
	      parameter->counter = 0 ;
	      pthread_mutex_unlock(&parameter->currentbuffer_used);
	    }
	  else
	    {			
	      pthread_mutex_lock(&parameter->currentbuffer_used);
	      parameter->currentbuffer [ parameter->counter ++ ] = incomingbuf[0] ;
	      if ( parameter->counter > INCOMINGCHARACTERBUFSIZE )
		{
		  parameter->currentbuffer [ parameter->counter ] = 0 ;
		  sendmessagetoothers(parameter->currentbuffer, parameter->incomingname);
		  parameter->counter =  0;
		}
	      pthread_mutex_unlock(&parameter->currentbuffer_used);
	    }
	} 
      else 
	{
				/* when something wrong, die */
	  sendmessagetoothers("--- lost connection", parameter->incomingname);
	  pthread_mutex_lock(&printf_mutex);
	  printf("%s has quit\n", parameter->incomingname);	  
	  pthread_mutex_unlock(&printf_mutex);
	  goto cleanup;	  
	}
      
    }
 cleanup:
  free (parameter->currentbuffer);
  parameter->currentbuffer=0;  
}
 
static void handle_write ( struct handle_incoming_params * parameter)
{				/* handle writing to users */
  char buf [CHARACTERBUFSIZE];
  const char * new_message = "\r                                                                             \r"; /* display this to clear a line. */
  
  pthread_mutex_lock (&waitingsendmessagetoothers_mutex);
  while (1)
    {
      pthread_cond_wait (&sendmessagetoothers_broadcast, &waitingsendmessagetoothers_mutex); /* mutex is unlocked on waiting time only. */
      strcpy (buf, messagesenttoothers ) ;
      pthread_mutex_unlock (&waitingsendmessagetoothers_mutex);
      write (parameter->t, new_message, strlen(new_message));
      write (parameter->t, buf, strlen(buf));
      pthread_mutex_lock (&parameter->currentbuffer_used);
      write (parameter->t, parameter->currentbuffer, parameter->counter);
      pthread_mutex_unlock(&parameter->currentbuffer_used);      
      pthread_mutex_lock (&waitingsendmessagetoothers_mutex);
    }
  pthread_mutex_unlock (&waitingsendmessagetoothers_mutex);
}


static void handle_incoming(struct handle_incoming_params * parameter)	/* on exit, goto cleanup */
{
  pthread_t thread_reader=0;
  pthread_t thread_writer=0;

  if (pthread_create (&thread_reader, NULL, (void*)handle_read, parameter))
    {				/* error  */
      goto cleanup;      
    }
  if (pthread_create (&thread_writer, NULL, (void*)handle_write, parameter))
    {				/* error  */
      pthread_cancel(thread_reader);
      goto cleanup;
    }

				/* I die when the reader dies. */
  pthread_join (thread_reader, NULL);
  pthread_cancel(thread_writer);

  /* cleanup routine */
 cleanup:
  close(parameter->t);  
  free (parameter->tmpisap);  
  free (parameter->incomingname);
 
  free (parameter);
}


static char * gethostname_versatile(void)
{
  const int unithostnamelen = 256;
  int unit = 1;
  char * localhostname = malloc (unithostnamelen + 1) ;
  if (!localhostname) return NULL;
  
  while (gethostname (localhostname, unithostnamelen * unit) == -1)
    {
      char * newlocalhostname = realloc(localhostname, unithostnamelen * (++ unit) + 1);
      if (newlocalhostname == NULL)
	{
	  free(localhostname);
	  return NULL;
	}
      else 
	localhostname = newlocalhostname ;
    }
  return localhostname;
}

#define malloc_with_error(A,BYTES) if (NULL==((A)=malloc(BYTES)))\
{fprintf(stderr, "out of memory in allocating %i bytes\n", BYTES);exit(1);}

int main (int ac, char** av)
{
  char *localhostname ;
  struct sockaddr_in sa;
  struct handle_incoming_params * params;
  
  int incoming_socket;		/* the main waiting socket */
  int temporal_loginid = 0;	/* the temporal serial id name for login */  
  struct hostent *hp;
  int i;
  pthread_t newthread_id ;
  pthread_attr_t pthread_attrib ;

  puts ("Telnet chat server version 0.0.2\n"
	"copyright 2001 Junichi Uekawa\n");

  if (ac != 2)
    {
      fprintf(stderr, "Please specify the port as the command-line parameter\n");
      exit(1);
    }

				/* creating a pthread attribute */
  pthread_attr_init(&pthread_attrib);
  pthread_attr_setdetachstate(&pthread_attrib,1);

  if (NULL==(localhostname = gethostname_versatile()))
    {
      fprintf(stderr, "Cannot get hostname of localhost\n");
      exit (1);
    }
  if ((hp = gethostbyname(localhostname)) == NULL)
    {
      fprintf(stderr, "%s: cannot get localhost info\n", av[0]);
      exit (1);
    }
  if (hp->h_addrtype != AF_INET)
    {
      fprintf(stderr, "hp->h_addrtype != AF_INET\n");
      exit(1);
    }
  
  if ((incoming_socket = socket (hp->h_addrtype, SOCK_STREAM, 0 )) == -1 )
    {
      perror  ("socket");
      exit (1);      
    }

  sa.sin_port = htons(atoi(av[1]));	/* this should hopefully be portable */
  sa.sin_addr.s_addr=INADDR_ANY;
  sa.sin_family = AF_INET;
  
  if (bind(incoming_socket, (struct sockaddr*)&sa, sizeof (sa) ) < 0  ) 
    {
      perror ("bind");
      exit (1);
    }

  listen (incoming_socket, BACKLOG);


				/* the main routine getting incoming connections */
  while (1)
    {
      i = sizeof (struct sockaddr_in);
      malloc_with_error(params, sizeof(struct handle_incoming_params));
      malloc_with_error(params->tmpisap, i);
      malloc_with_error(params->incomingname, USERNAMELENGTH + 1 );
      pthread_mutex_init ( &params->currentbuffer_used, NULL);
      params->currentbuffer = NULL ;
      params->counter=0;      
      
      sprintf ( params->incomingname , "%i", temporal_loginid ++ );
      				/* end memory allocation */
				/* accept incoming connections and start threads */
      if ((params->t = accept (incoming_socket, (struct sockaddr*)params->tmpisap, &i )) == -1)
	{
	  perror ("accept");
	  exit (1);
	}
      
      pthread_create (&newthread_id, &pthread_attrib, (void*)handle_incoming, params);
    }
}

