/* util.c
   Utility routines to maintain and process gopher data structures
   and events. */

     /*---------------------------------------------------------------*/
     /* Xgopher        version 1.1     20 April 1991                  */
     /*                version 1.0     04 March 1991                  */
     /* X window system client for the University of Minnesota        */
     /*                                Internet Gopher System.        */
     /* Allan Tuchman, University of Illinois at Urbana-Champaign     */
     /*                Computing Services Office                      */
     /* Copyright 1992 by                                             */
     /*           the Board of Trustees of the University of Illinois */
     /* Permission is granted to freely copy and redistribute this    */
     /* software with the copyright notice intact.                    */
     /*---------------------------------------------------------------*/


#include <stdio.h>

#include "conf.h"
#include "gopher.h"
#include "globals.h"
#include "list.h"
#include "util.h"
#include "misc.h"
#include "net.h"
#include "resources.h"
#include "prefixP.h"


/* makeItem
   Given the contents, allocate and build a new gopher item */

gopherItemP
makeItem(t, titleString, selectString, host, port)
char	t;
char	*titleString;
char	*selectString;
char	*host;
int	port;
{
	gopherItemP	gi = newItem();

	gi->type = t;
	strncpy(USER_STRING(gi), titleString, USER_STRING_LEN);
	vStringSet(&(gi->selector), selectString);
	strncpy(gi->host, host, HOST_STRING_LEN);
	gi->port = port;

	return gi;
}


/* getGopherItem
   receive a response from a gopher server, parsing it as it comes in.
   Return the item pointer for success, NULL for failure. */

gopherItemP
getGopherItem(gfd)
int	gfd;
{
	char *cPtr = NULL;
	char buffer[255];
	gopherItemP	gi;
     

	gi = newItem();
	(void) readField(gfd, buffer, 255);

	gi->type = buffer[0];

	/** Get the kind of file from the first character **/
	/** Filter out files that we can't deal with **/

	switch (gi->type) {
	    case A_FILE:
		PREFIX(gi, prefixFile);
		break;
	    case A_DIRECTORY:
		PREFIX(gi, prefixDir);
		break;
	    case A_CSO:
		PREFIX(gi, prefixCSO);
		break;
	    case A_INDEX:
		PREFIX(gi, prefixIndex);
		break;
	    case A_TELNET:
		PREFIX(gi, prefixTelnet);
		break;
	    case A_SOUND:
		PREFIX(gi, prefixSound);
		break;
	    case A_EOI:
		freeItem(gi);
		return NULL;
	    default:
		PREFIX(gi, prefixUnknown);
	}



	/* get the User Displayable name */

	strncpy(USER_STRING(gi), buffer + 1, USER_STRING_LEN);
	*(USER_STRING(gi)+(USER_STRING_LEN - 1)) = '\0';

	/* get the Pathname */

	{
		char temp[SELECTOR_STRING_MAX_LEN];
		readField(gfd, temp, SELECTOR_STRING_MAX_LEN);
		vStringSet(&(gi->selector), temp);
	}

	/* get the hostname */

	if (readField(gfd, gi->host, HOST_STRING_LEN) == 0) {
		gi->type = A_BADREAD;
		return(gi);
	}

	if (readLine(gfd, buffer, 255)==0)
		/* problem.  Should be a port number here */
		;

	gi->port = 0;

	/* Get the port number */

	gi->port = atoi(buffer);

	return(gi);
}


/* getGopherDir
   receive a directory from a gopher server.
   Return the directory pointer for success, NULL for failure. */

BOOLEAN
getGopherDir(gfd, d)
int	gfd;
gopherDirP	d;
{
	gopherItemP	gi;
	char		buffer[512];
	int  j;

	do {
		if ((gi = getGopherItem(gfd)) != NULL) {
			if (gi->type == A_FILE  ||
			    gi->type == A_DIRECTORY ||
			    gi->type == A_CSO ||
			    gi->type == A_INDEX ||
			    gi->type == A_TELNET ||
			    gi->type == A_SOUND) {
				appendItem(&(d->contents), gi);

			} else {
				/*
				fprintf (stderr,
				    "discarding type %c file: %s\n",
				    gi->type, USER_STRING(gi));
				*/
				freeItem(gi);

				if (gi->type == A_BADREAD) {
					/* read fail:
					   give up on this directory */
					break;
				}
			}
		}
	} while (gi != NULL);

	return  ( (emptyItemList(&d->contents)) ? FALSE : TRUE );
} 


/* getDirectory
   load a new gopher directory as indicated by the selected item */

BOOLEAN
getDirectory(gi)
gopherItemP	gi;
{
	int		s;
	gopherDirP	d;

	s = connectToSocket(gi->host, gi->port);
	if (s < 0) {
		/* could not make a network connection to receive directory */
		networkError(s, gi->host, gi->port);
		return FALSE;
	}

	writeString(s, vStringValue(&(gi->selector)));
	writeString(s, EOL_STRING);

	d = newDir();
	if (! getGopherDir(s, d)) {
		/* failure to load directory */
		freeDir(d);
		close(s);
		showError(
			"There seems to be no information in this directory\n");
		return FALSE;
	}

	setDirTime(d);

	close(s);

	d->selectorItem = copyItem(gi);
	d->indexSearchString = NULL;
	
	pushDir(d);

	displayCurrent();

	return TRUE;
}


/* updateDirectory
   reload an existing gopher directory (possibly an index search result) */

void
updateDirectory(d)
gopherDirP	d;
{
	int		s;
	gopherItemP	gi = d->selectorItem;

	showStatus("reloading requested directory", GSTAT_WAIT);

	s = connectToSocket(gi->host, gi->port);
	if (s < 0) {
		/* could not make a network connection to receive directory */
		networkError(s, gi->host, gi->port);
		showStatus("select an item from the list", GSTAT_SELECT);
		return;
	}

	writeString(s, vStringValue(&(gi->selector)));
	if (d->indexSearchString != NULL) {
		writen(s, "\t", 1);
		writeString(s, d->indexSearchString);
	}
	writeString(s, EOL_STRING);

	if (! getGopherDir(s, d)) {
		/* failure to re-load the directory */
		showError(
			"There seems to be no information in this directory\n");
	}

	setDirTime(d);

	close(s);
	showStatus("select an item from the list", GSTAT_SELECT);

	return;
}


/* getIndexDirectory
   load a new gopher directory resulting from an index search */

BOOLEAN
getIndexDirectory(gi, string)
gopherItemP	gi;
char		*string;
{
	int		s;
	gopherDirP	d;

	s = connectToSocket(gi->host, gi->port);
	if (s < 0) {
		/* could not make a network connection to receive index */
		networkError(s, gi->host, gi->port);
		return FALSE;
	}

	writeString(s, vStringValue(&(gi->selector)));
	writen(s, "\t", 1);
	writeString(s, string);
	writeString(s, EOL_STRING);

	d = newDir();
	if (! getGopherDir(s, d)) {
		/* failure to load directory */
		freeDir(d);
		close(s);
		showError(
			"There are no files selected by this index search\n");
		return FALSE;
	}

	setDirTime(d);

	close(s);

	d->selectorItem = copyItem(gi);
	d->indexSearchString = string;
	{
#define			INDEX_LABEL " - Index words: "
		char	*title = USER_STRING(d->selectorItem);
		int	len=strlen(title);
		int	remaining = USER_STRING_LEN - len;
		int	labelLen = strlen(INDEX_LABEL);

		title += len;
		strncpy(title, INDEX_LABEL, remaining);
		remaining -= labelLen;
		title += labelLen;
		strncpy(title, string, remaining);
	}
	
	pushDir(d);

	displayCurrent();

	return TRUE;
}


/* getFile
   access the file as required by the selected item */

BOOLEAN
getFile(gi, indexString)
gopherItemP	gi;
char		*indexString;
{
	char		errorString[MESSAGE_STRING_LEN];
	char		tempName[PATH_NAME_LEN];
	FILE		*tempFP;
	int		s;
	
	s = connectToSocket(gi->host, gi->port);
	if (s < 0) {
		/* could not make a network connection to receive file */
		networkError(s, gi->host, gi->port);
		return FALSE;
	}

	writeString(s, vStringValue(&(gi->selector)));
	writeString(s, EOL_STRING);

	getTempFile(tempName);
	if ((tempFP = fopen(tempName, "w")) == NULL) {
		sprintf(errorString,
			"Cannot open the temporary file %s", tempName);
		showError(errorString);
		return FALSE;
	}

	copyNetToFile(s, tempFP);

	close(s);
	fclose(tempFP);

	showFile(USER_STRING(gi), tempName, indexString);

	return TRUE;

}

/* getTempFile
   generate a temperary file name and return a pointer to it.
   The caller should check for a NULL file pointer.
   (The Unix routines mktemp(), tmpnam(), or tempnam() could be
   used as the body of this routine.) */

void
getTempFile(tempName)
char	*tempName;
{
	static int	fileCount = 0;
	static int	thisPID = 0;

	if (thisPID == 0) thisPID = getpid();
	sprintf (tempName, "%s/gop%d.%d",
			appResources->tempDirectory, thisPID, fileCount++);

	return;
}


/* copyNetToFile
   copy characters from an open socket connection to an open file. */

void
copyNetToFile(s, fp)
int	s;
FILE	*fp;
{
	char	line[FILE_LINE_LEN];

	if (readLine(s, line, FILE_LINE_LEN) <= 0) return;
	zapCRLF(line);

	while ( line[0] != '.'  ||  line[1] != '\0') {

		fputs(line, fp);
		fputc(NL, fp);

		if (readLine(s, line, FILE_LINE_LEN) <= 0) break;
		zapCRLF(line);
	}

	return;
}


extern errno;

#include <sys/wait.h>
#include <sys/time.h>
#include <sys/resource.h>

/* getSound
   get a sound file and play it.  Since sound will not conflict with
   visuals, this can be done asynchronously as a new process.

   X is run in a distributed environment.  As such, we take some pains
   to ensure that the requestor of the sound is the one who will actually
   hear the sound.  To wit, we strive to determine that the Xgopher
   client host and X display host are one and the same.  

   Note that this does not eliminate mischief, as the sound command
   specified in the resources can still be an "rsh" type of command. */

BOOLEAN
getSound(gi)
gopherItemP	gi;
{
	static BOOLEAN	firstTime = TRUE;
	static BOOLEAN	soundCommandOK = TRUE;
	static char	*soundCmd;
	char		message[MESSAGE_STRING_LEN];
	FILE		*soundCmdFile;
	int		s;

#if    	!defined(SYSV) && !defined(__convex__) && !defined(__bsdi__)
	static int     childPID = 0;
#else
	static pid_t   childPID = 0;
#endif                          /* !defined(SYSV) */

	/* this is supposed to be defined in <stdio.h>, but
	   it turns up missing on some machines. */

	FILE		*popen();

	/* see if sound command is turned on */

	if (! appResources->hasSound) {
		showError("This machine does not support sounds.");
		return FALSE;
	}

	/* see if sound command is an executable file */

	if (firstTime) {
		firstTime = FALSE;
		soundCmd = cmdPath(appResources->soundCommand);
		if (soundCmd == NULL) {
			soundCommandOK = FALSE;
		}

		/* check here for X display (later) */
	}

	if (! soundCommandOK) {
		sprintf (message,
			"Unable to execute the sound play command: \'%s\'\n",
			soundCmd);
		showError(message);
		return FALSE;
				
	}

	if (childPID != 0) {	/* sound may still be active */

#if     	!defined (linux) && !defined(SYSV) && !defined(__convex__) && !defined(__bsdi__)
		union wait	status;
		int		pid;
#else
		int		status;
		pid_t		pid;
#endif                          /* !defined(SYSV) */

		struct rusage	rusage;

		pid = wait3(&status, WNOHANG, &rusage);
		if (pid == 0) {
		    showError(
		    "Another sound file is still executing.  Try again later."
		        );
		    return FALSE;
		} else if (pid == childPID) {
			childPID = 0;
		} else {
			showError(
			    "It doesn't look good for playing any more sounds."
			    );
			return FALSE;
		}
	}

	if ((childPID = fork()) < 0) {

		/* we should immediately do a 
		      close (ConnectionNumber(XtDisplay(widget)));
		   here.
		*/
		soundCommandOK = FALSE;
		sprintf(message,
		    "Unable to start playing the sound (error %d)\n",
		    errno);
		showError(message);
		return FALSE;

	} else if (childPID != 0) {
		return TRUE;

	} else {

		s = connectToSocket(gi->host, gi->port);
		if (s < 0) {
			networkError(s, gi->host, gi->port);
			return FALSE;
		}

		writeString(s, vStringValue(&(gi->selector)));
		writeString(s, EOL_STRING);

		soundCmdFile = popen(soundCmd, "w");

		if (soundCmdFile == NULL) {
		    sprintf(message,
			"Unable to start the sound command (\'%s\')\n",
			    soundCmd);
			showError(message);
			close (s);
			exit(-1);
		}

		copyNetUntilEOF(s, soundCmdFile);

		close(s);
		pclose(soundCmdFile);
		exit(0);
	}
}


#define NET_BUFFER_SIZE	1400

/* copyNetUntilEOF
   copy chunks of data from a network file descriptor to a file pointer.
   No termination condition is expected except EOF on the input fd.
   This is a blocking read. */

void
copyNetUntilEOF(s, fp)
int	s;
FILE	*fp;
{
	char	buf[NET_BUFFER_SIZE];
	int	j;

	while (True) {
		j = read(s, buf, NET_BUFFER_SIZE);


		if (j <= 0) {
			if (j == -1)
			    fprintf (stderr,
				"Error (%d) copying data from the network\n",
					errno);
			break;
		}
		
		if (fwrite(buf, 1, j, fp) != j) {
			return;
		}
	}
}


/* giveCsoNameServer
   provide access to a CSO name server */

BOOLEAN
giveCsoNameServer(gi)
gopherItemP	gi;
{
	int		s;
	
	s = connectToSocket(gi->host, gi->port);
	if (s < 0) {
		/* could not make a network connection to receive file */
		networkError(s, gi->host, gi->port);
		return FALSE;
	}

	showNameServer(USER_STRING(gi), s);

	/* important: descriptor s will be closed by the name server
	   panel when it is done.  This will allow that panel to be
	   left up and operate asychronously with the rest of xgopher. */
}


/* getIndexSearchString
   provide access to an Index Search */

BOOLEAN
getIndexSearchString(gi)
gopherItemP	gi;
{
	int		s;

	/* This call will put up the panel with a grabNonexclusive, so
	   that no other gopher activity can proceed until the user 
	   has taken some action on this panel (done/cancel/etc).
	   Any action will result in a callback that eventually invokes
	   processIndexSelection (below). */
	
	showIndex(gi);
}


/* processIndexSelection
   process the index transaction from the user's selected keyword string */

void
processIndexSelection(gi, string)
gopherItemP	gi;
char		*string;
{
	/* this function is similar to a "getDirectory" operation */


	showStatus("Awaiting search results", GSTAT_WAIT);
	getIndexDirectory(gi, string);
	showStatus("select an item from the list", GSTAT_SELECT);
}


/* giveTelnetCommand
   provide an Xterm session running a telnet command */

BOOLEAN
giveTelnetSession(gi)
gopherItemP	gi;
{
	char	telnetCmd[PATH_NAME_LEN];	
	char	message[MESSAGE_STRING_LEN];

	if (! appResources->allowTelnet ) {
		showError("Telnet sessions are not enabled for this session");
		return FALSE;
	}

	if (strlen ( appResources->telnetCommand ) <= 0 ) {
		showError("Cannot execute the telnet command");
		return FALSE;
	}

	if (gi->host == NULL || strlen ( gi->host ) <= 0 ) {
		showError("There is no host to telnet to.");
		return FALSE;
	}

	if (gi->port == 0) 
		sprintf (telnetCmd, "%s %s &\n",
			appResources->telnetCommand, gi->host);
	else 
		sprintf (telnetCmd, "%s %s %d &\n",
			appResources->telnetCommand, gi->host, gi->port);
	
	system(telnetCmd);

	if (strlen(vStringValue(&(gi->selector))) > 0) {
	    sprintf(message,
		"If a login name is required, try:\n\'%s\'",
		vStringValue(&(gi->selector)));
	    showInfo(message);
	}

	return TRUE;
}


/* processSelection
   process the gopher transaction of the user's selected item */

BOOLEAN
processSelection(gi)
gopherItemP	gi;
{
	BOOLEAN	result=FALSE;

	switch (gi->type) {
	case A_FILE:
		showStatus("getting text file", GSTAT_WAIT);

		/* index string will be null for text file in normal dir */

		result = getFile(gi, getCurrentDirIndex());

		break;

	case A_DIRECTORY:
		showStatus("getting requested directory", GSTAT_WAIT);
		result = getDirectory(gi);
		
		break;

	case A_CSO:
		/* display panel.  Since the panel can operate independent
		   and asynchronous of the rest of gopher, no special
		   grabs are used. */

		showStatus("starting name server", GSTAT_WAIT);
		result = giveCsoNameServer(gi);
		break;

	case A_ERROR:
		
		break;

	case A_INDEX:
		/* display panel; after string is selected, it is
		   passed to processIndexSelection (above). */

		showStatus("Enter an index search string", GSTAT_WAIT);
		result = getIndexSearchString(gi);

		break;

	case A_TELNET:
		result = giveTelnetSession(gi);

		break;

	case A_SOUND:
		/* once it looks like we can really do sounds, the
		   sound process is forked off and executed
		   as a separate process.  However, only one sound
		   process may be active at a time.  This is enforced
		   by getSound. */

		result = getSound(gi);
		
		break;

	case A_EOI:
		
		break;

	default:

		break;
	}

	showStatus("select an item from the list", GSTAT_SELECT);
	return result;
}
