/* Filename: TtySrc.c */
/* Author: Andy Lowry */
static char sccsid[] = "@(#)TtySrc.c	1.1 6/18/90";

/*
 * COMPONENT_NAME: X11
 *
 * FUNCTIONS: Tty
 *
 * ORIGINS: 27, 40
 *
 * (C) COPYRIGHT International Business Machines Corp. 1987, 1989
 * All Rights Reserved
 * Licensed Materials - Property of IBM
 *
 * US Government Users Restricted Rights - Use, duplication or
 * disclosure restricted by GSA ADP Schedule Contract with IBM Corp.
 *
 * Copyright 1987, 1988 by Digital Equipment Corporation, Maynard, Massachusetts,
*/


/***********************************************************
Copyright 1987, 1988 by Digital Equipment Corporation, Maynard, Massachusetts,
and the Massachusetts Institute of Technology, Cambridge, Massachusetts.

                        All Rights Reserved

Permission to use, copy, modify, and distribute this software and its 
documentation for any purpose and without fee is hereby granted, 
provided that the above copyright notice appear in all copies and that
both that copyright notice and this permission notice appear in 
supporting documentation, and that the names of Digital or MIT not be
used in advertising or publicity pertaining to distribution of the
software without specific, written prior permission.  

DIGITAL DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING
ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL
DIGITAL BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR
ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
SOFTWARE.

******************************************************************/

/*****************************************************************

This module implements a text source that acts like a computer
terminal.  Edits are allowed to the last line of the text only
(regardless of edit mode), and only until that line is terminated with
a newline.  A callback can be registered to allow the client to
retrieve each line as it is entered.  A new action routine can be used
to temporarily disable accumulation of new text as user input.  Any
text added to the source during such a period constitutes an edit
boundary, but will not affect the accumulation and reporting of user
typein.  As an example, suppose the following text appears, with
uppercase letters representing program output (e.g. text added
while typein accumulation is disabled):

	HELLO THERE, PLEASE jaTYPEc YOUR NAME
	k

[Jack answered the question with typeahead.]  Now Jack will be able to
delete the "k" but no further.  The client will receive a callback
with the text "jack\n" when Jack enters a newline.

A new resource, maxLines, determines how many lines of text will be
retained by the source.  The default, 0, means to keep all lines
forever.  Accumulated typein will be reported even if part of it has
been pruned by this mechanism (due to intervening program output).

The source text is managed internally, rather in the client-supplied
string as with the normal StringSrc.  Eventually, this will all be
moved to R4 where this enhancement already exists in a better
integrated fashion anyway.

Terminal emulation of escape sequences may be added in the future, but
don't hold your breath!

******************************************************************/
#include <sys/types.h>
#include <sys/stat.h>
#include <X11/IntrinsicP.h>
#include <X11/StringDefs.h>
#include <X11/Text.h>
#include <X11/TextP.h>
#include "TtySrc.h"

/* Private TtySource Definitions */

#define TEXTCHUNK 32
#define MINLINECHUNK 4
#define MAXLINECHUNK 128

typedef struct _TtySourceLine {
    char *text;
    int length, maxLength;
} TtySourceLine, *TtySourceLinePtr;

typedef struct _TtySourceData {
    TextWidget owner;
    TtySourceLinePtr lines;
    int lastLine, lookLine, lineCount, maxLines;
    XtTextPosition length, lastLinePos, lookPos;
    char *initText;
    int initLength;
    char *typein;
    Boolean accumulateTypein;
    XtTextPosition typeinLength, typeinMaxLength;
    XtTextPosition editBarrier;
    XtTextPosition left, right;		/* selection */
} TtySourceData, *TtySourcePtr;

#define Increment(data, position, direction)\
{\
    if (direction == XtsdLeft) {\
	if (position > 0) \
	    position -= 1;\
    }\
    else {\
	if (position < data->length)\
	    position += 1;\
    }\
}

static XtResource sourceResources[] = {
    {XtNeditType, XtCEditType, XtREditMode, sizeof(XtTextEditType), 
        XtOffset(XtTextSource, edit_mode), XtRString, "read"},
};

static XtResource ttyResources[] = {
    {XtNstring, XtCString, XtRString, sizeof (char *),
        XtOffset(TtySourcePtr, initText), XtRString, NULL},
    {XtNlength, XtCLength, XtRInt, sizeof (int),
        XtOffset(TtySourcePtr, initLength), XtRInt, 0},
    {XtNmaxLines, XtCMaxLines, XtRInt, sizeof (int),
	XtOffset(TtySourcePtr, maxLines), XtRInt, 0},
};

/* Return line structure containing given position */
int LineNoForPosition(data, pos, posInLine)
  TtySourcePtr data;
  XtTextPosition pos;
  XtTextPosition *posInLine;
{
    int delta, length;

    delta = pos - data->lookPos;
    while (data->lookLine < data->lastLine &&
	   delta >= (length = data->lines[data->lookLine].length)) {
	delta -= length;
	data->lookPos += length;
	data->lookLine++;
    }
    while (delta < 0) {
	data->lookLine--;
	data->lookPos -= (length = data->lines[data->lookLine].length);
	delta += length;
    }
    *posInLine = delta;
    return(data->lookLine);
}

char Look(data, position, direction)
  TtySourcePtr data;
  XtTextPosition position;
  XtTextScanDirection direction;
{

    int lineNo, posInLine;

/* Looking left at pos 0 or right at position data->length returns newline */
    if (direction == XtsdLeft) {
	if (position == 0)
	    return(0);
	else
	    position -= 1;
    }
    else {
	if (position == data->length)
	    return(0);
    }
    
    lineNo = LineNoForPosition(data, position, &posInLine);
    return(data->lines[lineNo].text[posInLine]);
}

static XtTextPosition TtyReadText (src, pos, text, maxRead)
  XtTextSource src;
  int pos;
  XtTextBlock *text;
  int maxRead;
{
    int     charsLeft;
    TtySourcePtr data;
    TtySourceLinePtr line;
    XtTextPosition posInLine;

    data = (TtySourcePtr) src->data;
    text->firstPos = pos;
    line = &data->lines[LineNoForPosition(data, pos, &posInLine)];
    text->ptr = line->text + posInLine;
    charsLeft = line->length - posInLine;
    text->length = (maxRead > charsLeft) ? charsLeft : maxRead;
    return pos + text->length;
}


/* Get rid of the first line of text, since we've reached the line */
/* count limit set for this widget */
void dropOneLine(data)
     TtySourcePtr data;
{
    /* todo: implement this routine */
}

/* Expand the capacity of our line table.  We grow it exponentially, */
/* within the bounds set by MINLINECHUNK and MAXLINECHUNK */

void expandLines(data)
     TtySourcePtr data;
{
    int chunksize, newCount;
    TtySourceLinePtr line;

    chunksize = data->lineCount/2;
    if (chunksize < MINLINECHUNK)
      chunksize = MINLINECHUNK;
    else if (chunksize > MAXLINECHUNK)
      chunksize = MAXLINECHUNK;
    newCount = data->lineCount + chunksize;
    data->lines =
      (TtySourceLinePtr) XtRealloc((char *) data->lines,
				   newCount*sizeof(TtySourceLine));
    for (line = &data->lines[data->lineCount]; data->lineCount < newCount;
	 data->lineCount++, line++) {
	line->text = (char *) XtMalloc(TEXTCHUNK);
	line->length = 0;
	line->maxLength = TEXTCHUNK;
    }
}

/* Add text for the given length from the given text block to the end */
/* of the source text. */

void addText(data, text, length)
     TtySourcePtr data;
     XtTextBlock *text;
     XtTextPosition length;
{
    TtySourceLinePtr line;
    int needed;

    line = &data->lines[data->lastLine];
    needed = length - (line->maxLength - line->length);
    if (needed > 0) {
	/* need to expand this line */
	needed += TEXTCHUNK - (needed % TEXTCHUNK);
	line->maxLength += needed;
	line->text = (char *) XtRealloc(line->text, line->maxLength);
    }
    (void) strncpy(&line->text[line->length], &text->ptr[text->firstPos],
		   length);
    line->length += length;
    data->length += length;
    if (line->text[line->length -1] == '\n') {
	data->editBarrier = data->length;
	/* need to move to the next line in the line table */
	if (data->lastLine == data->maxLines -1)
	  dropOneLine(data);
	if (data->lastLine == data->lineCount - 1)
	  expandLines(data);
	data->lastLine += 1;
	data->lines[data->lastLine].length = 0;
	data->lastLinePos = data->length;
    }
}

/* Add text from the given text block for the given length to the */
/* typein buffer.  If the added text ends with a newline, do callbacks */
/* and then clear the typein buffer */

void addTypein(data, text, length)
     TtySourcePtr data;
     XtTextBlock *text;
     XtTextPosition length;
{
    int needed;
    XtTextBlock callbackText;

    needed = length - (data->typeinMaxLength - data->typeinLength);
    if (needed > 0) {
	/* need to expand the typein buffer */
	needed += TEXTCHUNK - (needed % TEXTCHUNK);
	data->typeinMaxLength += needed;
	data->typein = (char *) XtRealloc(data->typein, data->typeinMaxLength);
    }
    (void) strncpy(&data->typein[data->typeinLength],
		   &text->ptr[text->firstPos], 
		   length);
    data->typeinLength += length;
    if (data->typein[data->typeinLength - 1] == '\n') {
	/* flush this line */
	callbackText.ptr = data->typein;
	callbackText.firstPos = 0;
	callbackText.length = data->typeinLength;
	callbackText.format = FMT8BIT;
	XtCallCallbacks((Widget)data->owner, XtNcallback,
			(caddr_t) &callbackText);
	data->typeinLength = 0;
    }
}

static int TtyReplaceText (src, startPos, endPos, text)
  XtTextSource src;
  XtTextPosition startPos, endPos;
  XtTextBlock *text;
{
    TtySourcePtr data;
    TtySourceLinePtr line;
    XtTextPosition typeinPos, posInLine, nlPos;
    int     i, length;
    char *tail;
    int tailLength;
    XtTextBlock textCopy;

    data = (TtySourcePtr) src->data;
    if (startPos < data->editBarrier)
      return(PositionError);
    
    length = endPos - startPos;
    typeinPos = data->typeinLength - (data->length - startPos);
    /* Edit barrier is guaranteed to be in the last line, so that's */
    /* where we must be replacing */
    line = &data->lines[data->lastLine];
    posInLine = startPos - data->lastLinePos;

    /* First remove characters being replaced */
    for (i = posInLine+length ; i < line->length; i++)
      line->text[i-length] = line->text[i];
    line->length -= length;
    data->length -= length;

    /* Remove them from the typein buffer too (they're guaranteed to */
    /* be there) */
    for (i = typeinPos+length ; i < data->typeinLength; i++)
      data->typein[i-length] = data->typein[i];
    data->typeinLength -= length;

    /* Get a copy of whatever remains on the current last line, so we */
    /* can reinsert it after we're finished inserting the given text */
    tailLength = line->length - posInLine;
    if (tailLength > 0) {
	tail = (char *) XtMalloc((unsigned) tailLength);
	(void) strncpy(tail, &line->text[posInLine], tailLength);
	data->length -= tailLength;
	line->length -= tailLength;
	data->typeinLength -= tailLength;
    }
    else
      tail = NULL;
    
    /* Now keep inserting up to a newline until we've gotten through */
    /* the entire insertion text */
    textCopy = *text;
    while (textCopy.firstPos < textCopy.length) {
	for (nlPos = textCopy.firstPos; nlPos < textCopy.length; nlPos++)
	  if (textCopy.ptr[nlPos] == '\n')
	    break;
	if (nlPos < textCopy.length)
	  nlPos++;
	length = nlPos - textCopy.firstPos;
	addText(data, &textCopy, length);
	if (data->accumulateTypein)
	  addTypein(data, &textCopy, length);
	else
	  data->editBarrier = data->length;
	textCopy.firstPos = nlPos;
    }

    /* Now put back the tail if any */
    if (tailLength > 0) {
	textCopy.ptr = tail;
	textCopy.firstPos = 0;
	textCopy.length = tailLength;
	addText(data, &textCopy, tailLength);
	addTypein(data, &textCopy, tailLength);
	XtFree(tail);
    }
    return(EditDone);
}
    
/* N.B. If the following causes the text to shrink past the edit */
/* barrier, all accumulated typein will be discarded!!! */

static TtySetLastPos (src, lastPos)
  XtTextSource src;
  XtTextPosition lastPos;
{
    int delta;
    TtySourcePtr data;

    data = (TtySourcePtr) src->data;

    /* See how much text is being discarded */
    delta = data->length - lastPos;

    /* Handle any full line discards */
    while (data->lastLinePos > lastPos) {
	delta -= data->lines[data->lastLine].length;
	data->lastLine--;
	data->lastLinePos -= data->lines[data->lastLine].length;
    }
    /* Now do final partial discard */
    data->lines[data->lastLine].length -= delta;

    /* Set new data length */
    data->length = lastPos;

    /* Fix the typein buffer and edit barrier */
    if (lastPos < data->editBarrier) {
	data->typeinLength = 0;
	data->editBarrier = data->length;
    }
    else
      /* the while loop above is guaranteed not to have had any */
      /* iterations in this case, so delta remains as originally */
      /* computed */
      data->typeinLength -= delta;
}

static XtTextPosition TtyScan (src, pos, sType, dir, count, include)
  XtTextSource	 src;
  XtTextPosition pos;
  XtTextScanType sType;
  XtTextScanDirection dir;
  int		 count;
  Boolean	 include;
{
    TtySourcePtr data;
    TtySourceLinePtr line;
    XtTextPosition position, posInLine;
    int     i, whiteSpace, lineNo;
    char    c;
    int ddir = (dir == XtsdRight) ? 1 : -1;

    data = (TtySourcePtr) src->data;
    position = pos;
    switch (sType) {
	case XtstPositions: 
	    if (!include && count > 0)
		count -= 1;
	    for (i = 0; i < count; i++) {
		Increment(data, position, dir);
	    }
	    break;
	case XtstWhiteSpace: 

	    for (i = 0; i < count; i++) {
		whiteSpace = -1;
		while (position >= 0 && position <= data->length) {
		    c = Look(data, position, dir);
		    if ((c == ' ') || (c == '\t') || (c == '\n')){
		        if (whiteSpace < 0) whiteSpace = position;
		    } else if (whiteSpace >= 0)
			break;
		    position += ddir;
		}
	    }
	    if (!include) {
		if(whiteSpace < 0 && dir == XtsdRight) whiteSpace = data->length;
		position = whiteSpace;
	    }
	    break;

	case XtstEOL: 
	    /* get to beginning of line containing position */
	    lineNo = LineNoForPosition(data, position, &posInLine);
	    if (count != 0)
	      position -= posInLine;
	    for (i = 0; i < count; i++)
	      if (dir == XtsdRight) {
		  if (lineNo <= data->lastLine)
		    position += data->lines[lineNo++].length;
	      }
	      else {
		  if (lineNo > 0 && i > 0)
		    position -= data->lines[--lineNo].length;
	      }
	    /* above leaves us immediately following the located */
	    /* newline, if it existed */
	    if (include) {
		if (dir == XtsdLeft && position > 0)
		  position -= 1;
	    }
	    else {
		if (dir == XtsdRight && position > 0) {
		    line = &data->lines[lineNo-1];
		    if (line->text[line->length - 1] == '\n')
		      position -= 1;
		}
	    }
	    break;
	case XtstAll: 
	    if (dir == XtsdLeft)
		position = 0;
	    else
		position = data->length;
    }
    if (position < 0) position = 0;
    if (position > data->length) position = data->length;
    return(position);
}

static void SetSelection(src, left, right, selection)
    XtTextSource src;
    Atom selection;
    XtTextPosition left, right;
{
    ((TtySourcePtr)src->data)->left = left;
    ((TtySourcePtr)src->data)->right = right;
}

/***** Public routines *****/

XtTextSource XtTtySourceCreate (parent, args, argCount)
    Widget parent;
    ArgList args;
    Cardinal argCount;
{
    XtTextSource src;
    TtySourcePtr data;
    TtySourceLinePtr line;
    int i;
    XtTextBlock initText;

    src = XtNew(XtTextSourceRec);

    XtGetSubresources (parent, (caddr_t)src, XtNtextSource, XtCTextSource,
        sourceResources, XtNumber(sourceResources), args, argCount);

    src->Read = TtyReadText;
    src->Replace = TtyReplaceText;
    src->SetLastPos = TtySetLastPos;
    src->Scan = TtyScan;
    src->SetSelection = SetSelection;
    src->ConvertSelection = NULL;
    data = XtNew(TtySourceData);
    src->data = (caddr_t)data;
    data->owner = (TextWidget) parent;
    data->lines = 
      (TtySourceLinePtr) XtMalloc(MINLINECHUNK*sizeof(TtySourceLine));
    for (i = 0; i < MINLINECHUNK; i++) {
	line = &data->lines[i];
	line->text = (char *) XtMalloc(TEXTCHUNK);
	line->maxLength = TEXTCHUNK;
	line->length = 0;
    }
    data->lastLine = 0;
    data->lookLine = 0;
    data->lineCount = MINLINECHUNK;
    data->length = 0;
    data->lastLinePos = 0;
    data->lookPos = 0;
    data->typein = (char *) XtMalloc(TEXTCHUNK);
    data->accumulateTypein = False;
    data->typeinLength = 0;
    data->typeinMaxLength = TEXTCHUNK;
    data->editBarrier = 0;
    data->left = data->right = 0;

    XtGetSubresources (parent, (caddr_t)data, XtNtextSource, XtCTextSource,
        ttyResources, XtNumber(ttyResources), args, argCount);

    if (data->initText != NULL) {
	if (data->initLength == 0)
	  data->initLength = strlen(data->initText);
	initText.ptr = data->initText;
	initText.firstPos = 0;
	initText.length = data->initLength;
	initText.format = FMT8BIT;
	TtyReplaceText(src, 0, 0, &initText);
    }
    data->accumulateTypein = True;
    return src;
}

void XtTtySourceDestroy (src)
  XtTextSource src;
{
    TtySourcePtr data;
    int i;

    data = (TtySourcePtr) src->data;
    for (i = 0; i < data->lineCount; i++)
      XtFree(data->lines[i].text);
    XtFree((char *) data->lines);
    XtFree(data->typein);
    XtFree((char *) data);
    XtFree((char *) src);
}

Boolean TtyAccumulateTypein(src, flag)
     XtTextSource src;
     Boolean flag;
{
    TtySourcePtr data;
    Boolean oldflag;
    
    data = (TtySourcePtr) src->data;
    oldflag = data->accumulateTypein;
    data->accumulateTypein = flag;
    return(oldflag);
}

  
