/* 
 * mxUndo.c --
 *
 *	This file provides undo-ing capabilities for the Mx editor.
 *	It does so by latching onto the file database as a spy.
 *
 * Copyright (C) 1986, 1987, 1988 Regents of the University of California
 * 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.  The University of California
 * makes no representations about the suitability of this
 * software for any purpose.  It is provided "as is" without
 * express or implied warranty.
 */

#ifndef lint
static char rcsid[] = "$Header: /sprite/src/lib/mx/RCS/mxUndo.c,v 1.9 88/11/02 16:23:40 ouster Exp $ SPRITE (Berkeley)";
#endif not lint


#include <X11/Xlib.h>
#include <sys/dir.h>
#include <sys/file.h>
#include <sys/types.h>
#include <sys/stat.h>

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "mxInt.h"

/*
 * Library imports:
 */

char *getenv();

/*
 * One structure of the type defined below is kept in main memory for
 * each undo log that is currently open.  The insertCount and insertNext
 * fields are used to allow many consecutive inserts to be combined into
 * a single insert record in the log.
 */

#define MAX_BYTES 1024

typedef struct {
    Mx_File file;		/* Token for file being monitored. */
    MxFileInfo *fileInfoPtr;	/* Other Mx information about file. */
    Mx_Spy spy;			/* Token for spy used to monitor file. */
    FILE *writeStream;		/* Used to write log. */
    FILE *readStream;		/* Used to read log during undo operations. */
    char *buffer;		/* Buffer for readStream.  Must free when
				 * closing stream. */
    char *logName;		/* Name of log file:  used to delete it. */
    int prevPos;		/* Index of previous entry into log (-1 for
				 * first entry). */
    int lastWritePos;		/* Position in the log of the last place
				 * where the file was written to disk. */
    int insertCount;		/* If we're in the middle of outputting
				 * insert information, this tells how many
				 * characters of insert data have been
				 * output so far.  0 means no insert is
				 * in progress.*/
    Mx_Position insertNext;	/* If an insert is in progress, this gives
				 * the file position where the next inserted
				 * character will go if it's combined with
				 * the inserts so far. */
    int undoMorePos;		/* Where to start undo-ing in Undo_More.  -2
				 * means start at end, and -1 means nothing
				 * more to undo. */
    int flags;			/* Miscellaneous flags;  see below. */
} Log;

/*
 * Flag values:
 *
 * UNDO_IN_PROGRESS		1 means an undo is in progress for this log.
 * DISABLED			1 means some sort of error occurred in
 *				reading or writing the undo log, so undo-ing
 *				has been disabled.
 * MARK_NEEDED			1 means an undo mark should be written out
 *				before the next modification is logged.
 * CLEAN			1 means that the file is currently "clean"
 *				(i.e. identical to the copy of the file that's
 *				on disk).  Used to write out special marks
 *				in the log.
 */

#define UNDO_IN_PROGRESS	1
#define DISABLED		2
#define MARK_NEEDED		4
#define CLEAN			8

/*
 * The log file itself is stored on disk in ASCII.  Besides making this
 * code easier to debug, the ASCII representation avoids any potential
 * representation incompatibility problems in a heterogeneous network:
 * a machine of one type may be used to edit a file and then a machine
 * of a different type may be used to recover after a crash.  The entries
 * in the log have the form
 *		type prev extra
 * Type: single character giving type of entry, e.g. "i".
 * Prev: decimal number giving position of first byte of previous entry.
 *	Used for stepping backwards through the file while processing
 *	undos.
 * Extra: type-specific additional information, always terminated by
 *	newline character.
 *
 * The types of entries are:
 *
 * File id:	"f prev name id modTime index"
 *	This is the first entry in the log file, and ONLY appears at the
 *	beginning.  It identifies the file uniquely, and is use to
 *	locate the correct log file during recovery.  The fields are all
 *	fixed-format, so that the record can be re-written to update it
 *	if the file is written to disk during an edit session.
 *	Name:	Exactly 16 chars of file name, space-padded.
 *	Id:	8 octal chars of unique file id.
 *	ModTime:8 octal chars of time when file was last modified.
 *	Index:	Offset of first record in log file pertaining to this
 *		version.  If the file is written out during an edit
 *		session, the file id line is rewritten so that index
 *		points to the first change made AFTER the file was
 *		written.
 *
 * Note: after the file id line comes a short human-readable message
 * identifying what the file is used for (in case people discover these
 * files lying around in their directories).
 *
 * Insert:	"i prev line char bytes"
 *	Generated when bytes are inserted into the file:
 *	Line:	line index in file of first character inserted.
 *	Char:	char index within line of first character inserted.
 *	Bytes:	the information that was inserted.
 *
 * Delete:	"d prev line char bytes"
 *	Generated when bytes are deleted from the file:
 *	Line:	line index in file of first character deleted.
 *	Char:	char index within line of first character deleted.
 *	Bytes:	a copy of the bytes that were deleted.  Not null-terminated.
 *
 * Mark:	"m|M prev caretLine caretChar selLeftLine selLeftChar
		 selRightLine selRightChar"
 *	Delineates the beginning of a group of changes that must be undone
 *	together.  CaretLine and CaretChar give the location of the caret
 *	just before the change, and selLeftLine ... selRightChar give
 *	the location of the selection before the change.  If selLeftLine
 *	is negative, it means that there was not a selection on this file
 *	at the time of the change.  If the type character is 'M' instead
 *	of 'm', it means that undo-ing back to this mark has restored the
 *	file to its initial state.
 *
 * To avoid problems with control characters in the undo file, the "bytes"
 * parts of insert and delete records are converted to/from printable
 * ASCII before writing and after reading, using the procedures BytesOut
 * and BytesIn.  No insert or delete record contains more than MAX_BYTES
 * characters in the bytes section (before output conversion).
 */

/*
 * Forward references for internal procedures:
 */

extern int		BytesIn();
extern void		BytesOut();
extern void		CheckError();
extern int		DoUndo();
extern void		FlushInserts();
extern void		ReadProc();
extern void		SetReadPosition();
extern void		UndoSpyProc();

/*
 *----------------------------------------------------------------------
 *
 * Undo_LogCreate --
 *
 *	Set up undo-ing for a file.
 *
 * Results:
 *	A token for the undo log is returned.  This token is used in
 *	calls to other procedures in this module, like Undo_LogDelete.
 *	If the log file couldn't be opened, NULL is returned.
 *
 * Side effects:
 *	All future changes to file will be logged.  This servers two
 *	functions:  a) changes can be undone, arbitrarily far back;
 *	and b) if a crash occurs while editing, the edit session can
 *	be recovered by re-reading the log.
 *
 *----------------------------------------------------------------------
 */

Undo_Log
Undo_LogCreate(fileInfoPtr, logFile, idLine)
    MxFileInfo *fileInfoPtr;	/* Mx information about file whose
				 * modifications are to be tracked. */
    char *logFile;		/* Name of file to use to hold log of
				 * changes. */
    char *idLine;		/* Initial id line to use for log file (as
				 * returned by Undo_FindLog), or NULL. */
{
    register Log *logPtr;
    FILE *writeStream, *readStream;
    char msg[600];
#define READ_BUFFER_SIZE 100;
    static char *info =
"This file contains a log of changes made to the file\n\
\"%.50s\".  It is kept around during Mx edit\n\
sessions for use in undo-ing, and also as a backup in\n\
case a crash occurs.  If you see this log, it means\n\
that either someone's editing the file, or Mx crashed\n\
during an edit.  If a crash occurred, you can recover\n\
all the changes by re-running Mx on the above-named\n\
file, and selecting the \"Recover and Delete Log\" option.\n\
If you don't care about recovery, you can just delete\n\
the log.\n";

    writeStream = fopen(logFile, "w");
    if (writeStream == NULL) {
	return (Undo_Log) NULL;
    }
    readStream = fopen(logFile, "r");
    if (readStream == NULL) {
	fclose(writeStream);
	return (Undo_Log) NULL;
    }
    if (idLine == NULL) {
	idLine = "f -1 1234567812345678 00000000 00000000";
    }
    sprintf(msg, info, fileInfoPtr->name);
    fprintf(writeStream, "%s %8x\n", idLine,
            strlen(idLine) + strlen(msg) + 10);
    fputs(msg, writeStream);
    fflush(writeStream);

    logPtr = (Log *) malloc(sizeof(Log));
    logPtr->file = fileInfoPtr->file;
    logPtr->fileInfoPtr = fileInfoPtr;
    logPtr->spy = Mx_SpyCreate(logPtr->file, MX_AFTER|MX_BEFORE,
	    UndoSpyProc, (ClientData) logPtr);
    logPtr->writeStream = writeStream;
    logPtr->readStream = readStream;

    /*
     * Use small buffers for reading the undo log:  since the log is
     * read backwards, there's a seek after processing each record,
     * which trashes the buffers.
     */

    logPtr->buffer = malloc(512);
    setbuffer(logPtr->readStream, logPtr->buffer, 512);
    logPtr->logName = (char *) malloc((unsigned) (strlen(logFile) + 1));
    strcpy(logPtr->logName, logFile);
    logPtr->prevPos = 0;
    logPtr->lastWritePos = 0;
    logPtr->insertCount = 0;
    logPtr->undoMorePos = -1;
    logPtr->flags = MARK_NEEDED|CLEAN;
    CheckError(logPtr);
    return (Undo_Log) logPtr;
}

/*
 *----------------------------------------------------------------------
 *
 * Undo_LogDelete --
 *
 *	Delete an undo log file, and stop logging changes.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	After this call, changes to log's file will not be monitored
 *	anymore, and the log file will be deleted.  The caller should
 *	not use the log token anymore.
 *
 *----------------------------------------------------------------------
 */

void
Undo_LogDelete(log)
    Undo_Log log;		/* Token for the undo log.  If NULL, do
				 * nothing. */
{
    register Log *logPtr = (Log *) log;

    if (logPtr == NULL) {
	return;
    }
    Mx_SpyDelete(logPtr->spy);
    fclose(logPtr->writeStream);
    fclose(logPtr->readStream);
    unlink(logPtr->logName);
    free((char *) logPtr->buffer);
    free((char *) logPtr->logName);
    free((char *) logPtr);
}

/*
 *----------------------------------------------------------------------
 *
 * Undo_Mark --
 *
 *	This procedure arranges for a marker to be placed in the
 *	undo log.  Markers separate groups of operations that should
 *	be undone together.  When undoing, all operations between
 *	markers are always undone together.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	A mark is placed in the undo log, unless the last thing in
 *	the log is already a marker.
 *
 *----------------------------------------------------------------------
 */

void
Undo_Mark(log)
    Undo_Log log;		/* Token for undo log. If NULL, do nothing. */
{
    register Log *logPtr = (Log *) log;

    if ((logPtr == NULL) || (logPtr->flags & DISABLED)
	    || (logPtr->flags & MARK_NEEDED)) {
	return;
    }
    FlushInserts(logPtr);
    fflush(logPtr->writeStream);
    logPtr->flags |= MARK_NEEDED;
    CheckError(logPtr);
}

/*
 *----------------------------------------------------------------------
 *
 * Undo_FindLog --
 *
 *	This procedure is normally called before setting up undo-ing.
 *	It figures out what name to use for the log file, and sees if
 *	there's already a log file around from some other edit session.
 *
 * Results:
 *	The return value is normally TCL_OK.  If an error occurred, then
 *	the return value is TCL_ERROR and interp->result will point to an
 *	error message describing the problem.  The area pointed to by
 *	logName will be filled in with the name of a log file to use for
 *	this edit session.  The log file will exist with zero length.
 *	The area pointed to by idLine will be filled in with an initial
 *	id line to be passed to Undo_LogCreate.  If oldLog isn't NULL,
 *	this procedure checks to see if there already exists a valid log
 *	file for fileName. If so, the name of that log file is filled in
 *	at oldLog.  If not, an empty string is left at oldLog.
 *
 * Side effects:
 *	A new log file is created.  If possible, the log file is created
 *	in the same directory as the file being logged.  If this directory
 *	isn't writable, then the log file is created in the home directory.
 *
 *----------------------------------------------------------------------
 */

int
Undo_FindLog(fileName, logName, idLine, oldLog, interp)
    char  *fileName;		/* Name of file for which undo-ing info
				 * is going to be set up. */
    char *logName;		/* Filled in with complete log name.  Caller
				 * must make sure this area has at least
				 * UNDO_NAME_LENGTH more bytes than logDir. */
    char *idLine;		/* Filled in with id line to pass to
				 * Undo_LogCreate.  Must have room for
				 * UNDO_ID_LENGTH chars. */
    char *oldLog;		/* If not NULL, filled in with name of existing
				 * log (if any), or empty string if none. */
    Tcl_Interp *interp;		/* Place to store error message, if any. */
{
    /*
     * The constant below defines the number of characters in the last name
     * in the path for an undo log.
     */

#define LAST_EL_LENGTH 12
    struct stat atts;
    int id, time, x;
    char name[20];
    char logDir[UNDO_NAME_LENGTH];
    char *lastInPath;
    DIR *dirStream;
    struct direct *entryPtr;

    /*
     * Find the last element of the file's path name, which will be
     * used for generating the log file name and id line.
     */

    lastInPath = strrchr(fileName, '/');
    if (lastInPath == NULL) {
	lastInPath = fileName;
    } else {
	lastInPath++;
    }

    /*
     * Pick a directory to hold the log file, which is either the
     * directory containing the file (if possible) or else our
     * user's home directory.
     */

    if (lastInPath == fileName) {
	strcpy(logDir, ".");
    } else {
	int count;
	count = (lastInPath - fileName);
	if (count >= UNDO_NAME_LENGTH - LAST_EL_LENGTH) {
	    count = UNDO_NAME_LENGTH - LAST_EL_LENGTH - 1;
	}
	strncpy(logDir, fileName, count);
	logDir[count] = 0;
    }
    if (access(logDir, R_OK|W_OK|X_OK) != 0) {
	char *home;

	home = getenv("HOME");
	if (home == NULL) {
	    interp->result = "Couldn't read HOME environment variable for setting up undo log.";
	    return TCL_ERROR;
	}
	strncpy(logDir, home, UNDO_NAME_LENGTH);
	logDir[UNDO_NAME_LENGTH-1] = 0;
	if (access(logDir, R_OK|W_OK|X_OK) != 0) {
	    sprintf(interp->result, "%s %s",
		"couldn't access either the file's directory ",
		"or your home directory to set up an undo log");
	    return TCL_ERROR;
	}
    }

    /*
     * Get status information about the file, and generate the log file
     * name and id line.
     */

    if (stat(fileName, &atts) == 0) {
	id = atts.st_ino;
	time = atts.st_mtime;
    } else {
	id = 0;
	time = 0;
    }
    sprintf(name, "Mx.%.5s.", lastInPath);
    sprintf(idLine, "f -1 %16.16s %8x %8x", lastInPath, id, time);

    /*
     * Now search the target directory for the log to see if there
     * are any existing logs whose names have the form nnnnnnii.x,
     * where nnnnnnii matches name, and x is an additional number
     * used to distinguish log files that hash to the same name but
     * really refer to different files.  For each name match, open
     * the log file and see if it corresponds EXACTLY to this file.
     * If a match is found, remember its name.
     */

    if (oldLog != NULL) {
	int nameLength;

	nameLength = strlen(name);
	dirStream = opendir(logDir);
	if (dirStream == NULL) {
	    sprintf(interp->result,
		    "couldn't open undo log directory \"%.50s\"", logDir);
	    return TCL_ERROR;
	}
	while (1) {
	    FILE *logStream;
	    char idFromLog[UNDO_ID_LENGTH];
	    int count, size;

	    *oldLog = 0 ;
	    entryPtr = readdir(dirStream);
	    if (entryPtr == NULL) {
		break;
	    }
	    if (strncmp(name, entryPtr->d_name, nameLength) != 0) {
		continue;
	    }
	    sprintf(oldLog, "%s/%s", logDir, entryPtr->d_name);
	    logStream = fopen(oldLog, "r");
	    if (logStream == NULL) {
		continue;
	    }
	    size = strlen(idLine);
	    count = fread(idFromLog, 1, size, logStream);
	    fclose(logStream);
	    if (count <= 0) {
		continue;
	    }
	    idFromLog[count] = 0;
	    if (strcmp(idFromLog, idLine) == 0) {
		break;
	    }
	}
	closedir(dirStream);
    }

    /*
     * Now find the name of a new log to use.  Try several file names
     * if necessary in order to find a name that isn't in use.
     */
    
    for (x = 1; ; x++) {
	int id;

	sprintf(logName, "%s/%s%d", logDir, name, x);
	id = open(logName, O_RDWR|O_CREAT|O_EXCL, 0600);
	if (id >= 0) {
	    close(id);
	    return TCL_OK;
	}
	if ((errno == EEXIST) || (errno == EACCES)) {
	    continue;
	}
	sprintf(interp->result, "%s \"%.50s\": %.100s.",
		"Problem in creating undo log file",
		logName, strerror(errno));
	return TCL_ERROR;
    }
}

/*
 *----------------------------------------------------------------------
 *
 * Undo_SetVersion --
 *
 *	This procedure is called after a file is written out.  It
 *	adds information to the log to describe the file's current
 *	version (this information is needed by the Undo_Recover
 *	procedure so that it can ignore all changes to the file
 *	except those that occurred after the last time the file
 *	was written).
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	The initial record in the log is modified.
 *
 *----------------------------------------------------------------------
 */


void
Undo_SetVersion(log, fileName)
    Undo_Log log;		/* Token for undo log.  If NULL, do nothing. */
    char *fileName;		/* Name to which log's file was just
				 * written. */
{
    register Log *logPtr = (Log *) log;
    struct stat atts;
    int id, time, pos;

    if ((logPtr == NULL) || (logPtr->flags & DISABLED)) {
	return;
    }
    FlushInserts(logPtr);

    /*
     * Get status information about the file.
     */

    if (stat(fileName, &atts) == 0) {
	id = atts.st_ino;
	time = atts.st_mtime;
    } else {
	id = 0;
	time = 0;
    }

    pos = ftell(logPtr->writeStream);
    fseek(logPtr->writeStream, 22, 0);
    fprintf(logPtr->writeStream, "%8x %8x %8x", id, time, pos);
    fseek(logPtr->writeStream, 0, 2);

    /*
     * Mark the file as "unmodified" at this point.
     */

    logPtr->flags |= CLEAN;
    logPtr->lastWritePos = pos;
    Undo_Mark(log);
}

/*
 *----------------------------------------------------------------------
 *
 * Undo_Last --
 *
 *	Undo the most recent set of changes associated with the given log.
 *	This is done by reading back through the log, undoing changes
 *	until a command marker (entered with Undo_Mark) is found.
 *
 * Results:
 *	Under normal circumstances, TCL_OK is returned.  If some
 *	problem kept the undo from being performed (e.g. nothing
 *	in the log or an error in reading back the undo record),
 *	then TCL_ERROR is returned and interp->result will point to an
 *	error message.
 *
 * Side effects:
 *	The file associated with log is modified.  The changes made
 *	while undo-ing are themselves logged in the file, so successive
 *	calls to Undo_Last will have the effect of toggling the file's
 *	state.
 *
 *----------------------------------------------------------------------
 */

int
Undo_Last(log, interp, mxwPtr)
    Undo_Log log;		/* Token for undo log. */
    Tcl_Interp *interp;		/* Interpreter to use for error reporting. */
    MxWindow *mxwPtr;		/* If non-NULL, gives window whose caret
				 * and view should be updated as part of
				 * the undo. */
{
    register Log *logPtr = (Log *) log;

    if (logPtr->flags & DISABLED) {
	interp->result = "Can't undo: undoing was disabled because of an error in reading or writing the undo log.";
	return TCL_ERROR;
    }
    return DoUndo(logPtr, logPtr->prevPos, &logPtr->undoMorePos, interp,
	    mxwPtr);
}

/*
 *----------------------------------------------------------------------
 *
 * Undo_More --
 *
 *	This procedure is similar to Undo_Last, except that instead
 *	of undo-ing the last command in the log, it undoes the one
 *	before the previous one undone for that log.  The result is
 *	that a call to Undo_Last followed by several calls to Undo_More
 *	will step back through the state of the file all the way to
 *	the file's original state.  If the file has been modified since
 *	the last undo operation, then Undo_More is equivalent to Undo_Last.
 *
 * Results:
 *	Under normal circumstances, TCL_OK is returned.  If some
 *	problem kept the undo from being performed (e.g. nothing
 *	in the log or an error in reading back the undo record),
 *	then TCL_ERROR is returned and interp->result points to an
 *	error message.
 *
 * Side effects:
 *	The file associated with log is modified.
 *
 *----------------------------------------------------------------------
 */

int
Undo_More(log, interp, mxwPtr)
    Undo_Log log;
    Tcl_Interp *interp;		/* Interpreter to use for error reporting. */
    MxWindow *mxwPtr;		/* If non-NULL, gives window whose caret
				 * and view should be updated as part of
				 * the undo. */
{
    register Log *logPtr = (Log *) log;

    if (logPtr->flags & DISABLED) {
	interp->result = "Can't undo: undoing was disabled because of an error in reading or writing the undo log.";
	return TCL_ERROR;
    }
    if (logPtr->undoMorePos == -2) {
	return DoUndo(logPtr, logPtr->prevPos, &logPtr->undoMorePos, interp,
		mxwPtr);
    } else {
	return DoUndo(logPtr, logPtr->undoMorePos, &logPtr->undoMorePos,
		interp,	mxwPtr);
    }
}

/*
 *----------------------------------------------------------------------
 *
 * Undo_Recover --
 *
 *	This procedure is called after there has been a crash during
 *	an edit session.  It reads a log file and reconstructs the
 *	state of the file being edited, as it was at the time of
 *	the crash (or shortly before).
 *
 * Results:
 *	If all went well, TCL_OK is returned.  If any sort of error
 *	occurred in opening or reading the log file, then TCL_ERROR
 *	is returned and interp->result will point to a string that describes
 *	the problem.
 *
 * Side effects:
 *	File is modified.
 *
 *----------------------------------------------------------------------
 */

int
Undo_Recover(file, logFile, interp)
    Mx_File file;		/* File whose contents are to be recovered. */
    char *logFile;		/* Name of log file to use for recovery. */
    Tcl_Interp *interp;		/* Interpreter to use for error recovery. */
{
    register FILE *stream;
    Mx_Position first;
    char bytes[MAX_BYTES+1];
    int result;
    int type, length, lineNum;

    stream = fopen(logFile, "r");
    if (stream == NULL ) {
	sprintf(interp->result,
		"Couldn't open recovery log file \"%.50s\": %.100s.",
	        logFile, strerror(errno));
	return TCL_ERROR;
    }

    first.lineIndex = -1;
    for (lineNum = 1; ; lineNum++) {
	type = getc(stream);
	if (type == EOF) {
	    break;
	}

	if (type == 'i') {
	    
	    /*
	     * Insert record:  re-insert the bytes.  Watch out for EOF
	     * conditions: the last record may not be complete.
	     */
	    
	    result = fscanf(stream, " %*d %d %d%*c", &first.lineIndex,
		    &first.charIndex);
	    if (ferror(stream) != 0) {
		ioError:
		sprintf(interp->result,
			"I/O error in reading log file \"%.50s\": %.100s.",
			logFile, strerror(ferror(stream)));
		fclose(stream);
		return TCL_ERROR;
	    }
	    if (feof(stream)) {
		break;
	    }
	    if (result != 2) {
		fmtError:
		sprintf(interp->result, "%s \"%.50s\" around line %d.",
			"Format error in log file", logFile,
			lineNum);
		fclose(stream);
		return TCL_ERROR;
	    }
	    length = BytesIn(stream, bytes);
	    Mx_ReplaceBytes(file, first, first, bytes);
	} else if (type == 'd') {

	    /*
	     * Delete record: re-delete the bytes.
	     */
	    
	    result = fscanf(stream, " %*d %d %d%*c", &first.lineIndex,
		    &first.charIndex);
	    if (ferror(stream) != 0) {
		goto ioError;
	    }
	    if (feof(stream)) {
		break;
	    }
	    if (result != 2) {
		goto fmtError;
	    }
	    length = BytesIn(stream, bytes);
	    Mx_ReplaceBytes(file, first, Mx_Offset(file, first, length),
		    (char *) NULL);
	} else if (type == 'f') {
	    int newPosition;

	    /*
	     * File id record:  use it to skip over undo information
	     * that isn't relevant any more.
	     */

	     result = fscanf(stream, " %*d %*s %*x %*x %x",
		    &newPosition);
	    if (result != 1) {
		goto fmtError;
	    }
	    fseek(stream, newPosition, 0);
	} else {
	    while ((type != EOF) && (type != '\n')) {
		type = getc(stream);
	    }
	}
    }

    fclose(stream);
    return TCL_OK;
}

/*
 *----------------------------------------------------------------------
 *
 * UndoSpyProc --
 *
 *	This procedure is invoked by the Mx_File routines just after
 *	any bytes are inserted in a file and just before any bytes
 *	are deleted.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	Information about the changes is logged in the undo log.
 *
 *----------------------------------------------------------------------
 */

    /* ARGSUSED */
void
UndoSpyProc(logPtr, file, type, first, oldLast, newLast, string)
    register Log *logPtr;		/* Stores undo information. (passed
					 * to us as ClientData). */
    Mx_File file;			/* File that was modified. */
    int type;				/* Type of modification:
					 * MX_AFTER or MX_BEFORE.
					 */
    Mx_Position first;			/* Position in file of first character
    					 * (to be) deleted. */
    Mx_Position oldLast;		/* Position in file of character just
    					 * after last one deleted. */
    Mx_Position newLast;		/* Position in file of character
					 * just after last new one inserted.*/
    char *string;			/* For inserts, the information that
					 * was inserted. */
{

    if (logPtr->flags & DISABLED) {
	return;
    }

    /*
     * Place a marker in the log if one is requested.
     */

    if (logPtr->flags & MARK_NEEDED) {
	int newPrevPos;
	Mx_Position selLeft, selRight;
	char id;

	newPrevPos = ftell(logPtr->writeStream);
	if (MxGetSelRange(logPtr->fileInfoPtr->mxwPtr, &selLeft,
		&selRight) != TCL_OK) {
	    Tcl_Return(logPtr->fileInfoPtr->mxwPtr->interp,
		    (char *) NULL, TCL_STATIC);
	    selLeft.lineIndex = selRight.lineIndex = -1;
	    selLeft.charIndex = selRight.charIndex = 0;
	}
	if (logPtr->flags & CLEAN) {
	    id = 'M';
	} else {
	    id = 'm';
	}
	fprintf(logPtr->writeStream, "%c %d %d %d %d %d %d %d\n", id,
		logPtr->prevPos, logPtr->fileInfoPtr->caretFirst.lineIndex,
		logPtr->fileInfoPtr->caretFirst.charIndex,
		selLeft.lineIndex, selLeft.charIndex,
		selRight.lineIndex, selRight.charIndex);
	logPtr->prevPos = newPrevPos;
	logPtr->flags &= ~MARK_NEEDED;
    }
    logPtr->flags &= ~CLEAN;

    /*
     * If this change to the file isn't because of an undo operation,
     * then clear the "undo more" position so that the next Undo_More
     * call will go back to the end of the log.
     */
    
    if (!(logPtr->flags & UNDO_IN_PROGRESS)) {
	logPtr->undoMorePos = -2;
    }

    /*
     * For very long inserts or deletes, break them up into multiple
     * shorter operations, each with at most MAX_BYTES of data.  Group
     * consecutive inserts together if they involve consecutive positions
     * in the file (this is likely when the user is just typing characters
     * into the file).
     */

    if ((type == MX_AFTER) && (string != NULL)) {
	int count;

	/*
	 * See if the next inserts follow the previous ones.  If not,
	 * flush the previous stuff.
	 */
	
	if (!MX_POS_EQUAL(logPtr->insertNext, first)) {
	    FlushInserts(logPtr);
	}

	while (*string != 0) {
	    
	    /*
	     * Compute how many bytes we can handle in this group,
	     * then output them.
	     */
	    
	    count = strlen(string);
	    if (count > (MAX_BYTES - logPtr->insertCount)) {
		count = MAX_BYTES - logPtr->insertCount;
	    }
	    if (logPtr->insertCount == 0) {
		int newPrevPos;
		newPrevPos = ftell(logPtr->writeStream);
		fprintf(logPtr->writeStream, "i %d %d %d ",
			logPtr->prevPos, first.lineIndex, first.charIndex);
		logPtr->prevPos = newPrevPos;
	    }
	    BytesOut(logPtr->writeStream, string, count);
	    string += count;
	    logPtr->insertCount += count;
	    first = Mx_Offset(logPtr->file, first, count);

	    if (logPtr->insertCount >= MAX_BYTES) {
		FlushInserts(logPtr);
	    }
	}
	logPtr->insertNext = first;
    } else if ((type == MX_BEFORE)
	    && (!MX_POS_EQUAL(first, oldLast))) {
	int count;		/* Number of bytes in current log record. */
	Mx_Position next;

	FlushInserts(logPtr);

	/*
	 * Process the deleted information one line at a time, but
	 * try to group several deleted lines together in a single
	 * log record.
	 */

	count = 0;
	for (next = first; MX_POS_LESS(next, oldLast); ) {
	    char *line;
	    int lineLength, bytesThisLine;
	    
	    /*
	     * Compute how many bytes must be output from this line.
	     */

	    line = Mx_GetLine(logPtr->file, next.lineIndex, &lineLength);
	    if (oldLast.lineIndex == next.lineIndex) {
		bytesThisLine = oldLast.charIndex - next.charIndex;
	    } else {
		bytesThisLine = lineLength - next.charIndex;
	    }
	    if (bytesThisLine == 0) {
		break;
	    }
	    if (bytesThisLine > (MAX_BYTES - count)) {
		bytesThisLine = MAX_BYTES - count;
	    }

	    /*
	     * Output the record header if we're starting a new record.
	     * Note:  when it takes several records to record a delete,
	     * the starting position in each record is the same.  To
	     * see this, consider the effect of re-processing the
	     * records in order:  each deletes enough material to bring
	     * the start of the next record back to the start of the
	     * previous.
	     */

	    if (count == 0) {
		int newPrevPos;
		newPrevPos = ftell(logPtr->writeStream);
		fprintf(logPtr->writeStream, "d %d %d %d ",
			logPtr->prevPos, first.lineIndex, first.charIndex);
		logPtr->prevPos = newPrevPos;
	    }

	    /*
	     * Output the bytes, and update pointers.
	     */

	    BytesOut(logPtr->writeStream, &line[next.charIndex], bytesThisLine);
	    count += bytesThisLine;
	    next = Mx_Offset(logPtr->file, next, bytesThisLine);
	    if (count >= MAX_BYTES) {
		putc('\n', logPtr->writeStream);
		count = 0;
	    }
	}
	if (count != 0) {
	    putc('\n', logPtr->writeStream);
	}
    }

    CheckError(logPtr);
}

/*
 *----------------------------------------------------------------------
 *
 * BytesOut --
 *
 *	Convert a collection of bytes to printable form, and output
 *	them.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	Information gets output on stream.  Each of the numChars
 *	bytes in info gets output in order, as is, unless the byte
 *	is '\' or not a printable ASCII character.  In this case it
 *	is output as "\ooo" where ooo is the octal representation
 *	of the character.
 *
 *----------------------------------------------------------------------
 */

void
BytesOut(stream, info, numChars)
    register FILE *stream;		/* Where to output. */
    register char *info;		/* Information to output. */
    int numChars;			/* How many chars to output. */
{
    register char c;

    for ( ; numChars > 0; numChars--, info++) {
	c = *info;
	if ((c >= 040) && (c < 0177) && (c != '\\')) {
	    putc(c, stream);
	} else {
	    putc('\\', stream);
	    putc('0' + ((c >> 6) & 03), stream);
	    putc('0' + ((c >> 3) & 07), stream);
	    putc('0' + (c & 07), stream);
	}
    }
}

/*
 *----------------------------------------------------------------------
 *
 * BytesIn --
 *
 *	Read bytes from stream and convert back to internal form.
 *
 * Results:
 *	The return value is the number of non-null bytes placed
 *	at info.
 *
 * Side effects:
 *	Bytes are read from stream until a newline is encountered.
 *	The bytes are converted back to internal form, in reverse
 *	fashion to what happened in BytesOut, and stored at *info,
 *	null-terminated.  At most MAX_BYTES non-null bytes will
 *	be placed at *info.
 *
 *----------------------------------------------------------------------
 */

int
BytesIn(stream, info)
    register FILE *stream;		/* Where to read bytes. */
    char *info;				/* Where to store converted infor. */
{
    register int c;
    register char *p;
    register int count;

    p = info;
    for (count = 0; count < MAX_BYTES; count++) {
	c = getc(stream);
	if ((c == EOF) || (c == '\n')) {
	    break;
	}
	if (c == '\\') {
	    c = (getc(stream) - '0') << 6;
	    c += (getc(stream) - '0') << 3;
	    c += (getc(stream) - '0');
	}
	*p = c;
	p++;
    }
    *p = 0;
    return count;
}

/*
 *----------------------------------------------------------------------
 *
 * DoUndo --
 *
 *	This procedure does all the actual work of undo-ing.  Given
 *	a pointer to the first byte of a record in the undo log, this
 *	procedure works backward through the log until an
 *	end-of-command marker is found.
 *
 * Results:
 *	Under normal circumstances, TCL_OK is returned.  If there was
 *	an error in reading back the log, then TCL_ERROR is returned,
 *	interp->result points to an error message, and undo-ing is disabled
 *	for the file.  If there was no error, then the value pointed
 *	to by prevPtr is filled in with the index in the log file of
 *	the command that just preceded the one that terminated this
 *	undo operation (e.g. the place to start processing again if
 *	you want to undo more).
 *
 * Side effects:
 *	The log's file gets modified to reflect the undos.
 *
 *----------------------------------------------------------------------
 */

int
DoUndo(logPtr, pos, prevPtr, interp, mxwPtr)
    register Log *logPtr;	/* Log that governs undo. */
    int pos;			/* Position in log file of latest
				 * record to be undone. */
    int *prevPtr;		/* Where to store position of preceding
				 * undo record. */
    Tcl_Interp *interp;		/* Interpreter to use for reporting errors. */
    MxWindow *mxwPtr;		/* Window;  if non-NULL, the selection,
				 * caret, and view of this window are
				 * adjusted during undo-ing. */
{
    logPtr->flags |= UNDO_IN_PROGRESS;
    for ( ; pos >= 0; pos = *prevPtr) {
	Mx_Position first;
	char bytes[MAX_BYTES+1];
	char type;
	int length, result;

	FlushInserts(logPtr);
	fflush(logPtr->writeStream);
	fseek(logPtr->readStream, pos, 0);
	result = fscanf(logPtr->readStream, "%c %d", &type, prevPtr);
	if (result != 2) {
	    error:
	    if (ferror(logPtr->readStream) != 0) {
		sprintf(interp->result,
			"I/O error while reading log file \"%.50s\": %.100s.  %s",
			logPtr->logName,
			strerror(ferror(logPtr->readStream)),
			"Undoing is now disabled.");
	    } else {
		sprintf(interp->result,
			"Format error in log file \"%s\".  %s",
			logPtr->logName,
			"Undoing is now disabled.");
	    }
	    logPtr->flags |= DISABLED;
	    logPtr->flags &= ~UNDO_IN_PROGRESS;
	    return TCL_ERROR;
	}
	if ((type == 'm') || (type == 'M')) {
	    Mx_Position caret, selLeft, selRight;
	    result = fscanf(logPtr->readStream, " %d %d %d %d %d %d",
		    &caret.lineIndex, &caret.charIndex,
		    &selLeft.lineIndex, &selLeft.charIndex,
		    &selRight.lineIndex, &selRight.charIndex);
	    if ((ferror(logPtr->readStream) != 0)
		    || feof(logPtr->readStream) || (result != 6)) {
		goto error;
	    }
	    if (mxwPtr != NULL) {
		MxCaretSetPosition(mxwPtr, caret, 0);
		if (selLeft.lineIndex >= 0) {
		    MxSelectionSet(mxwPtr, selLeft, selRight);
		}
		MxGetInWindow(mxwPtr, caret, mxwPtr->heightLines/2, 0);
	    }

	    /*
	     * If the file has been restored to the same state as its disk
	     * copy, then mark the file as clean again.  Any state preceding
	     * the state when the file was last written to disk is considered
	     * to be dirty.
	     */

	    if ((type == 'M') && (pos >= logPtr->lastWritePos)) {
		Mx_MarkClean(logPtr->file);
		logPtr->flags |= CLEAN;
	    }
	    break;
	} else if (type == 'i') {

	    /*
	     * Insert record: must delete the bytes that were inserted.
	     * Be careful to reposition the log to its EOF before actually
	     * undo-ing, since stuff will get added to the log as part
	     * of the undo.
	     */
	    
	    result =  fscanf(logPtr->readStream, " %d %d%*c",
		    &first.lineIndex, &first.charIndex);
	    length = BytesIn(logPtr->readStream, bytes);
	    if ((ferror(logPtr->readStream) != 0)
		    || feof(logPtr->readStream) || (result != 2)) {
		goto error;
	    }
	    Mx_ReplaceBytes(logPtr->file, first,
		    Mx_Offset(logPtr->file, first, length),
		    (char *) NULL);
	} else if (type == 'd') {
	    
	    /*
	     * Delete record: must re-insert the bytes that were deleted.
	     */
	    
	    result = fscanf(logPtr->readStream, " %d %d%*c",
		    &first.lineIndex, &first.charIndex);
	    (void) BytesIn(logPtr->readStream, bytes);
	    if ((ferror(logPtr->readStream) != 0)
		    || feof(logPtr->readStream) || (result != 2)) {
		goto error;
	    }
	    Mx_ReplaceBytes(logPtr->file, first, first, bytes);
	} else if (type == 'f') {
	    /* Nothing to do here. */
	} else {
	    goto error;
	}
    }
    logPtr->flags &= ~UNDO_IN_PROGRESS;
    if (*prevPtr >= 0) {
	return TCL_OK;
    } else {
	interp->result = "there's nothing more to undo";
	return TCL_ERROR;
    }
}

/*
 *----------------------------------------------------------------------
 *
 * FlushInserts --
 *
 *	If there is an insert record in progress for logPtr, finish
 *	it off.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	The insert record is terminated.
 *
 *----------------------------------------------------------------------
 */

void
FlushInserts(logPtr)
    register Log *logPtr;		/* Log for which to flush inserts. */
{
    if (logPtr->insertCount != 0) {
	putc('\n', logPtr->writeStream);
	logPtr->insertCount = 0;
    }
}

/*
 *----------------------------------------------------------------------
 *
 * CheckError --
 *
 *	Check the undo log streams for any errors;  if any have occurred,
 *	notify with an error message and then disable the log.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	The log will get disabled if an error occurred.
 *
 *----------------------------------------------------------------------
 */

void
CheckError(logPtr)
    register Log *logPtr;		/* Undo log to check. */
{
    MxWindow *mxwPtr;
    char msg[200];

    if (ferror(logPtr->writeStream) != 0) {
	sprintf(msg, "%s \"%.50s\": %.100s.  %s.",
		"An error occurred while writing undo log",
		logPtr->logName, strerror(ferror(logPtr->writeStream)),
		"Undoing and crash recovery are now disabled");
    } else if (ferror(logPtr->readStream) != 0) {
	sprintf(msg, "%s \"%.50s\": %.100s.  %s.",
		"An error occurred while reading undo log",
		logPtr->logName, strerror(ferror(logPtr->writeStream)),
		"Undoing and crash recovery are now disabled");
    } else {
	return;
    }
    mxwPtr = logPtr->fileInfoPtr->mxwPtr;
    (void) Sx_Notify(mxwPtr->display, DefaultRootWindow(mxwPtr->display),
	    -1, -1, 0, msg, mxwPtr->fontPtr,1, "Continue",
	    (char *) NULL);
    logPtr->flags |= DISABLED;
}
