/* exp_log.c - logging routines and other things common to both Expect
   program and library.  Note that this file must NOT have any
   references to Tcl except for including tclInt.h
*/

#include "expect_cf.h"
#include <stdio.h>
/*#include <varargs.h>		tclInt.h drags in varargs.h.  Since Pyramid */
/*				objects to including varargs.h twice, just */
/*				omit this one. */
#include "tclInt.h"
#ifdef NO_STDLIB_H
#include "../compat/stdlib.h"
#else
#include <stdlib.h>		/* for malloc */
#endif
#include <ctype.h>

#include "expect_comm.h"
#include "exp_int.h"
#include "exp_rename.h"
#include "exp_command.h"
#include "exp_log.h"

typedef struct ThreadSpecificData {
    Tcl_Channel diagChannel;
    Tcl_DString diagFilename;
    int diagToStderr;

    Tcl_Channel logChannel;
    Tcl_DString logFilename;	/* if no name, then it came from -open or -leaveopen */
    int logAppend;
    int logLeaveOpen;
    int logAll;			/* if TRUE, write log of all interactions
				 * despite value of logUser - i.e., even if
				 * user is not seeing it (via stdout)
				 */
    int logUser;		/* TRUE if user sees interactions on stdout */
} ThreadSpecificData;

static Tcl_ThreadDataKey dataKey;

/*
 * create a reasonably large buffer for the bulk of the output routines
 * that are not too large
 */
static char bigbuf[2000];

static void expDiagWriteCharsUni _ANSI_ARGS_((Tcl_UniChar *str,int len));

/*
 * Following this are several functions that log the conversation.  Some
 * general notes on all of them:
 */

/*
 * ignore sprintf return value ("character count") because it's not
 * defined in terms of UTF so it would be misinterpreted if we passed
 * it on.
 */

/*
 * if necessary, they could be made more efficient by skipping vsprintf based
 * on booleans
 */

/* Most of them have multiple calls to printf-style functions.  */
/* At first glance, it seems stupid to reformat the same arguments again */
/* but we have no way of telling how long the formatted output will be */
/* and hence cannot allocate a buffer to do so. */
/* Fortunately, in production code, most of the duplicate reformatting */
/* will be skipped, since it is due to handling errors and debugging. */

/*
 * Name: expWriteBytesAndLogIfTtyU
 *
 * Output to channel (and log if channel is stdout or devtty)
 *
 * Returns: TCL_OK or TCL_ERROR;
 */

int
expWriteBytesAndLogIfTtyU(esPtr,buf,lenChars)
    ExpState *esPtr;
    Tcl_UniChar *buf;
    int lenChars;
{
    int wc;
    ThreadSpecificData *tsdPtr = TCL_TSD_INIT(&dataKey);

    if (esPtr->valid)
	wc = expWriteCharsUni(esPtr,buf,lenChars);

    if (tsdPtr->logChannel && ((esPtr->fdout == 1) || expDevttyIs(esPtr))) {
      Tcl_DString ds;
      Tcl_DStringInit (&ds);
      Tcl_UniCharToUtfDString (buf,lenChars,&ds);
      Tcl_WriteChars(tsdPtr->logChannel,Tcl_DStringValue (&ds), Tcl_DStringLength (&ds));
      Tcl_DStringFree (&ds);
    }
    return wc;
}

/*
 * Name: expLogDiagU
 *
 * Send to the Log (and Diag if open).  This is for writing to the log.
 * (In contrast, expDiagLog... is for writing diagnostics.)
 */

void
expLogDiagU(buf)
char *buf;
{
    ThreadSpecificData *tsdPtr = TCL_TSD_INIT(&dataKey);

    expDiagWriteChars(buf,-1);
    if (tsdPtr->logChannel) {
	Tcl_WriteChars(tsdPtr->logChannel, buf, -1);
    }
}

/*
 * Name: expLogInteractionU
 *
 * Show chars to user if they've requested it, UNLESS they're seeing it
 * already because they're typing it and tty driver is echoing it.
 * Also send to Diag and Log if appropriate.
 */
void
expLogInteractionU(esPtr,buf,buflen)
    ExpState *esPtr;
    Tcl_UniChar *buf;
    int buflen;
{
    ThreadSpecificData *tsdPtr = TCL_TSD_INIT(&dataKey);

    if (tsdPtr->logAll || (tsdPtr->logUser && tsdPtr->logChannel)) {
      Tcl_DString ds;
      Tcl_DStringInit (&ds);
      Tcl_UniCharToUtfDString (buf,buflen,&ds);
      Tcl_WriteChars(tsdPtr->logChannel,Tcl_DStringValue (&ds), Tcl_DStringLength (&ds));
      Tcl_DStringFree (&ds);
    }

    /* hmm.... if stdout is closed such as by disconnect, loguser
       should be forced FALSE */

    /* don't write to user if they're seeing it already, i.e., typing it! */
    if (tsdPtr->logUser && (!expStdinoutIs(esPtr)) && (!expDevttyIs(esPtr))) {
	ExpState *stdinout = expStdinoutGet();
	if (stdinout->valid) {
	    (void) expWriteCharsUni(stdinout,buf,buflen);
	}
    }
    expDiagWriteCharsUni(buf,buflen);
}

/* send to log if open */
/* send to stderr if debugging enabled */
/* use this for logging everything but the parent/child conversation */
/* (this turns out to be almost nothing) */
/* uppercase L differentiates if from math function of same name */
#define LOGUSER		(tsdPtr->logUser || force_stdout)
/*VARARGS*/
void
expStdoutLog TCL_VARARGS_DEF(int,arg1)
{
    ThreadSpecificData *tsdPtr = TCL_TSD_INIT(&dataKey);
    int force_stdout;
    char *fmt;
    va_list args;

    force_stdout = TCL_VARARGS_START(int,arg1,args);
    fmt = va_arg(args,char *);

    if ((!tsdPtr->logUser) && (!force_stdout) && (!tsdPtr->logAll)) return;

    (void) vsprintf(bigbuf,fmt,args);
    expDiagWriteBytes(bigbuf,-1);
    if (tsdPtr->logAll || (LOGUSER && tsdPtr->logChannel)) Tcl_WriteChars(tsdPtr->logChannel,bigbuf,-1);
    if (LOGUSER) fprintf(stdout,"%s",bigbuf);
    va_end(args);
}

/* just like log but does no formatting */
/* send to log if open */
/* use this function for logging the parent/child conversation */
void
expStdoutLogU(buf,force_stdout)
char *buf;
int force_stdout;	/* override value of logUser */
{
    ThreadSpecificData *tsdPtr = TCL_TSD_INIT(&dataKey);
    int length;

    if ((!tsdPtr->logUser) && (!force_stdout) && (!tsdPtr->logAll)) return;

    length = strlen(buf);
    expDiagWriteBytes(buf,length);
    if (tsdPtr->logAll || (LOGUSER && tsdPtr->logChannel)) Tcl_WriteChars(tsdPtr->logChannel,buf,-1);
    if (LOGUSER) {
#if (TCL_MAJOR_VERSION > 8) || ((TCL_MAJOR_VERSION == 8) && (TCL_MINOR_VERSION >= 1))
      Tcl_WriteChars (Tcl_GetStdChannel (TCL_STDOUT), buf, length);
      Tcl_Flush      (Tcl_GetStdChannel (TCL_STDOUT));
#else
      fwrite(buf,1,length,stdout);
#endif
    }
}

/* send to log if open */
/* send to stderr */
/* use this function for error conditions */
/*VARARGS*/
void
expErrorLog TCL_VARARGS_DEF(char *,arg1)
{
    ThreadSpecificData *tsdPtr = TCL_TSD_INIT(&dataKey);

    char *fmt;
    va_list args;

    fmt = TCL_VARARGS_START(char *,arg1,args);
    (void) vsprintf(bigbuf,fmt,args);

    expDiagWriteChars(bigbuf,-1);
    fprintf(stderr,"%s",bigbuf);
    if (tsdPtr->logChannel) Tcl_WriteChars(tsdPtr->logChannel,bigbuf,-1);
    
    va_end(args);
}

/* just like errorlog but does no formatting */
/* send to log if open */
/* use this function for logging the parent/child conversation */
/*ARGSUSED*/
void
expErrorLogU(buf)
char *buf;
{
    ThreadSpecificData *tsdPtr = TCL_TSD_INIT(&dataKey);

    int length = strlen(buf);
    fwrite(buf,1,length,stderr);
    expDiagWriteChars(buf,-1);
    if (tsdPtr->logChannel) Tcl_WriteChars(tsdPtr->logChannel,buf,-1);
}



/* send diagnostics to Diag, Log, and stderr */
/* use this function for recording unusual things in the log */
/*VARARGS*/
void
expDiagLog TCL_VARARGS_DEF(char *,arg1)
{
    char *fmt;
    va_list args;

    ThreadSpecificData *tsdPtr = TCL_TSD_INIT(&dataKey);

    if ((tsdPtr->diagToStderr == 0) && (tsdPtr->diagChannel == 0)) return;

    fmt = TCL_VARARGS_START(char *,arg1,args);

    (void) vsprintf(bigbuf,fmt,args);

    expDiagWriteBytes(bigbuf,-1);
    if (tsdPtr->diagToStderr) {
	fprintf(stderr,"%s",bigbuf);
	if (tsdPtr->logChannel) Tcl_WriteChars(tsdPtr->logChannel,bigbuf,-1);
    }

    va_end(args);
}


/* expDiagLog for unformatted strings
   this also takes care of arbitrary large strings */
void
expDiagLogU(str)
char *str;
{
    ThreadSpecificData *tsdPtr = TCL_TSD_INIT(&dataKey);

    if ((tsdPtr->diagToStderr == 0) && (tsdPtr->diagChannel == 0)) return;

    expDiagWriteBytes(str,-1);

    if (tsdPtr->diagToStderr) {
      fprintf(stderr,"%s",str);
      if (tsdPtr->logChannel) Tcl_WriteChars(tsdPtr->logChannel,str,-1);
    }
}

/* expPrintf prints to stderr.  It's just a utility for making
   debugging easier. */

/*VARARGS*/
void
expPrintf TCL_VARARGS_DEF(char *,arg1)
{
  char *fmt;
  va_list args;
  char bigbuf[2000];
  int len, rc;

  fmt = TCL_VARARGS_START(char *,arg1,args);
  len = vsprintf(bigbuf,arg1,args);
 retry:
  rc = write(2,bigbuf,len);
  if ((rc == -1) && (errno == EAGAIN)) goto retry;

  va_end(args);
}


void
expDiagToStderrSet(val)
    int val;
{
    ThreadSpecificData *tsdPtr = TCL_TSD_INIT(&dataKey);

    tsdPtr->diagToStderr = val;
}
    

int
expDiagToStderrGet() {
    ThreadSpecificData *tsdPtr = TCL_TSD_INIT(&dataKey);
    return tsdPtr->diagToStderr;
}

Tcl_Channel
expDiagChannelGet()
{
    ThreadSpecificData *tsdPtr = TCL_TSD_INIT(&dataKey);
    return tsdPtr->diagChannel;
}

void
expDiagChannelClose(interp)
    Tcl_Interp *interp;
{
    ThreadSpecificData *tsdPtr = TCL_TSD_INIT(&dataKey);

    if (!tsdPtr->diagChannel) return;
    Tcl_UnregisterChannel(interp,tsdPtr->diagChannel);
    Tcl_DStringFree(&tsdPtr->diagFilename);
    tsdPtr->diagChannel = 0;
}

/* currently this registers the channel, however the exp_internal
   command doesn't currently give the channel name to the user so
   this is kind of useless - but we might change this someday */
int
expDiagChannelOpen(interp,filename)
    Tcl_Interp *interp;
    char *filename;
{
    ThreadSpecificData *tsdPtr = TCL_TSD_INIT(&dataKey);
    char *newfilename;

    Tcl_ResetResult(interp);
    newfilename = Tcl_TranslateFileName(interp,filename,&tsdPtr->diagFilename);
    if (!newfilename) return TCL_ERROR;

    /* Tcl_TildeSubst doesn't store into dstring */
    /* if no ~, so force string into dstring */
    /* this is only needed so that next time around */
    /* we can get dstring for -info if necessary */
    if (Tcl_DStringValue(&tsdPtr->diagFilename)[0] == '\0') {
	Tcl_DStringAppend(&tsdPtr->diagFilename,filename,-1);
    }

    tsdPtr->diagChannel = Tcl_OpenFileChannel(interp,newfilename,"a",0777);
    if (!tsdPtr->diagChannel) {
	Tcl_DStringFree(&tsdPtr->diagFilename);
	return TCL_ERROR;
    }
    Tcl_RegisterChannel(interp,tsdPtr->diagChannel);
    Tcl_SetChannelOption(interp,tsdPtr->diagChannel,"-buffering","none");
    return TCL_OK;
}

void
expDiagWriteObj(obj)
    Tcl_Obj *obj;
{
    ThreadSpecificData *tsdPtr = TCL_TSD_INIT(&dataKey);

    if (!tsdPtr->diagChannel) return;

    Tcl_WriteObj(tsdPtr->diagChannel,obj);
}

/* write 8-bit bytes */
void
expDiagWriteBytes(str,len)
char *str;
int len;
{
    ThreadSpecificData *tsdPtr = TCL_TSD_INIT(&dataKey);

    if (!tsdPtr->diagChannel) return;

    Tcl_Write(tsdPtr->diagChannel,str,len);
}

/* write UTF chars */
void
expDiagWriteChars(str,len)
char *str;
int len;
{
    ThreadSpecificData *tsdPtr = TCL_TSD_INIT(&dataKey);

    if (!tsdPtr->diagChannel) return;

    Tcl_WriteChars(tsdPtr->diagChannel,str,len);
}

/* write Unicode chars */
static void
expDiagWriteCharsUni(str,len)
Tcl_UniChar *str;
int len;
{
    Tcl_DString ds;
    ThreadSpecificData *tsdPtr = TCL_TSD_INIT(&dataKey);

    if (!tsdPtr->diagChannel) return;

    Tcl_DStringInit (&ds);
    Tcl_UniCharToUtfDString (str,len,&ds);
    Tcl_WriteChars(tsdPtr->diagChannel,Tcl_DStringValue (&ds), Tcl_DStringLength (&ds));
    Tcl_DStringFree (&ds);
}

char *
expDiagFilename()
{
    ThreadSpecificData *tsdPtr = TCL_TSD_INIT(&dataKey);

    return Tcl_DStringValue(&tsdPtr->diagFilename);
}

void
expLogChannelClose(interp)
    Tcl_Interp *interp;
{
    ThreadSpecificData *tsdPtr = TCL_TSD_INIT(&dataKey);

    if (!tsdPtr->logChannel) return;

    if (Tcl_DStringLength(&tsdPtr->logFilename)) {
	/* it's a channel that we created */
	Tcl_UnregisterChannel(interp,tsdPtr->logChannel);
	Tcl_DStringFree(&tsdPtr->logFilename);
    } else {
	/* it's a channel that tcl::open created */
	if (!tsdPtr->logLeaveOpen) {
	    Tcl_UnregisterChannel(interp,tsdPtr->logChannel);
	}
    }
    tsdPtr->logChannel = 0;
    tsdPtr->logAll = 0; /* can't write to log if none open! */
}

/* currently this registers the channel, however the exp_log_file
   command doesn't currently give the channel name to the user so
   this is kind of useless - but we might change this someday */
int
expLogChannelOpen(interp,filename,append)
    Tcl_Interp *interp;
    char *filename;
    int append;
{
    ThreadSpecificData *tsdPtr = TCL_TSD_INIT(&dataKey);
    char *newfilename;
    char mode[2];

    if (append) {
      strcpy(mode,"a");
    } else {
      strcpy(mode,"w");
    }

    Tcl_ResetResult(interp);
    newfilename = Tcl_TranslateFileName(interp,filename,&tsdPtr->logFilename);
    if (!newfilename) return TCL_ERROR;

    /* Tcl_TildeSubst doesn't store into dstring */
    /* if no ~, so force string into dstring */
    /* this is only needed so that next time around */
    /* we can get dstring for -info if necessary */
    if (Tcl_DStringValue(&tsdPtr->logFilename)[0] == '\0') {
	Tcl_DStringAppend(&tsdPtr->logFilename,filename,-1);
    }

    tsdPtr->logChannel = Tcl_OpenFileChannel(interp,newfilename,mode,0666);
    if (!tsdPtr->logChannel) {
	Tcl_DStringFree(&tsdPtr->logFilename);
	return TCL_ERROR;
    }
    Tcl_RegisterChannel(interp,tsdPtr->logChannel);
    Tcl_SetChannelOption(interp,tsdPtr->logChannel,"-buffering","none");
    expLogAppendSet(append);
    return TCL_OK;
}

int
expLogAppendGet()
{
    ThreadSpecificData *tsdPtr = TCL_TSD_INIT(&dataKey);
    return tsdPtr->logAppend;
}

void
expLogAppendSet(app)
    int app;
{
    ThreadSpecificData *tsdPtr = TCL_TSD_INIT(&dataKey);
    tsdPtr->logAppend = app;
}

int
expLogAllGet()
{
    ThreadSpecificData *tsdPtr = TCL_TSD_INIT(&dataKey);
    return tsdPtr->logAll;
}

void
expLogAllSet(app)
    int app;
{
    ThreadSpecificData *tsdPtr = TCL_TSD_INIT(&dataKey);
    tsdPtr->logAll = app;
    /* should probably confirm logChannel != 0 */
}

int
expLogToStdoutGet()
{
    ThreadSpecificData *tsdPtr = TCL_TSD_INIT(&dataKey);
    return tsdPtr->logUser;
}

void
expLogToStdoutSet(app)
    int app;
{
    ThreadSpecificData *tsdPtr = TCL_TSD_INIT(&dataKey);
    tsdPtr->logUser = app;
}

int
expLogLeaveOpenGet()
{
    ThreadSpecificData *tsdPtr = TCL_TSD_INIT(&dataKey);
    return tsdPtr->logLeaveOpen;
}

void
expLogLeaveOpenSet(app)
    int app;
{
    ThreadSpecificData *tsdPtr = TCL_TSD_INIT(&dataKey);
    tsdPtr->logLeaveOpen = app;
}

Tcl_Channel
expLogChannelGet()
{
    ThreadSpecificData *tsdPtr = TCL_TSD_INIT(&dataKey);
    return tsdPtr->logChannel;
}

/* to set to a pre-opened channel (presumably by tcl::open) */
int
expLogChannelSet(interp,name)
    Tcl_Interp *interp;
    char *name;
{
    ThreadSpecificData *tsdPtr = TCL_TSD_INIT(&dataKey);

    int mode;
    
    if (0 == (tsdPtr->logChannel = Tcl_GetChannel(interp,name,&mode))) {
	return TCL_ERROR;
    }
    if (!(mode & TCL_WRITABLE)) {
	tsdPtr->logChannel = 0;
	Tcl_SetResult(interp,"channel is not writable",TCL_VOLATILE);
	return TCL_ERROR;
    }
    return TCL_OK;
}

char *
expLogFilenameGet()
{
    ThreadSpecificData *tsdPtr = TCL_TSD_INIT(&dataKey);

    return Tcl_DStringValue(&tsdPtr->logFilename);
}

int
expLogUserGet()
{
    ThreadSpecificData *tsdPtr = TCL_TSD_INIT(&dataKey);

    return tsdPtr->logUser;
}

void
expLogUserSet(logUser)
    int logUser;
{
    ThreadSpecificData *tsdPtr = TCL_TSD_INIT(&dataKey);

    tsdPtr->logUser = logUser;
}



/* generate printable versions of random ASCII strings.  Primarily used */
/* in diagnostic mode, "expect -d" */
static char *
expPrintifyReal(s)
char *s;
{
	static int destlen = 0;
	static char *dest = 0;
	char *d;		/* ptr into dest */
	unsigned int need;
	Tcl_UniChar ch;

	if (s == 0) return("<null>");

	/* worst case is every character takes 4 to printify */
	need = strlen(s)*6 + 1;
	if (need > destlen) {
		if (dest) ckfree(dest);
		dest = ckalloc(need);
		destlen = need;
	}

	for (d = dest;*s;) {
	    s += Tcl_UtfToUniChar(s, &ch);
	    if (ch == '\r') {
		strcpy(d,"\\r");		d += 2;
	    } else if (ch == '\n') {
		strcpy(d,"\\n");		d += 2;
	    } else if (ch == '\t') {
		strcpy(d,"\\t");		d += 2;
	    } else if ((ch < 0x80) && isprint(UCHAR(ch))) {
		*d = (char)ch;			d += 1;
	    } else {
		sprintf(d,"\\u%04x",ch);	d += 6;
	    }
	}
	*d = '\0';
	return(dest);
}

/* generate printable versions of random ASCII strings.  Primarily used */
/* in diagnostic mode, "expect -d" */
static char *
expPrintifyRealUni(s,numchars)
Tcl_UniChar *s;
int numchars;
{
  static int destlen = 0;
  static char *dest = 0;
  char *d;		/* ptr into dest */
  unsigned int need;
  Tcl_UniChar ch;

  if (s == 0) return("<null>");
  if (numchars == 0) return("");

  /* worst case is every character takes 6 to printify */
  need = numchars*6 + 1;
  if (need > destlen) {
    if (dest) ckfree(dest);
    dest = ckalloc(need);
    destlen = need;
  }

  for (d = dest;numchars > 0;numchars--) {
    ch = *s; s++;

    if (ch == '\r') {
      strcpy(d,"\\r");		d += 2;
    } else if (ch == '\n') {
      strcpy(d,"\\n");		d += 2;
    } else if (ch == '\t') {
      strcpy(d,"\\t");		d += 2;
    } else if ((ch < 0x80) && isprint(UCHAR(ch))) {
      *d = (char)ch;			d += 1;
    } else {
      sprintf(d,"\\u%04x",ch);	d += 6;
    }
  }
  *d = '\0';
  return(dest);
}

char *
expPrintifyObj(obj)
    Tcl_Obj *obj;
{
    ThreadSpecificData *tsdPtr = TCL_TSD_INIT(&dataKey);

    /* don't bother writing into bigbuf if we're not going to ever use it */
    if ((!tsdPtr->diagToStderr) && (!tsdPtr->diagChannel)) return((char *)0);
    
    return expPrintifyReal(Tcl_GetString(obj));
}

char *
expPrintify(s) /* INTL */
char *s;
{
    ThreadSpecificData *tsdPtr = TCL_TSD_INIT(&dataKey);

    /* don't bother writing into bigbuf if we're not going to ever use it */
    if ((!tsdPtr->diagToStderr) && (!tsdPtr->diagChannel)) return((char *)0);

    return expPrintifyReal(s);
}
 
char *
expPrintifyUni(s,numchars) /* INTL */
Tcl_UniChar *s;
int numchars;
{
    ThreadSpecificData *tsdPtr = TCL_TSD_INIT(&dataKey);

    /* don't bother writing into bigbuf if we're not going to ever use it */
    if ((!tsdPtr->diagToStderr) && (!tsdPtr->diagChannel)) return((char *)0);

    return expPrintifyRealUni(s,numchars);
}
 
void
expDiagInit()
{
    ThreadSpecificData *tsdPtr = TCL_TSD_INIT(&dataKey);

    Tcl_DStringInit(&tsdPtr->diagFilename);
    tsdPtr->diagChannel = 0;
    tsdPtr->diagToStderr = 0;
}

void
expLogInit()
{
    ThreadSpecificData *tsdPtr = TCL_TSD_INIT(&dataKey);

    Tcl_DStringInit(&tsdPtr->logFilename);
    tsdPtr->logChannel = 0;
    tsdPtr->logAll = FALSE;
    tsdPtr->logUser = TRUE;
}
