/* 
 * mxFile.c --
 *
 *	This file implements low-level operations to manipulate
 *	files for the Mx editor.
 *
 * Copyright (C) 1986 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/mxFile.c,v 1.11 89/03/17 17:10:59 ouster Exp $ SPRITE (Berkeley)";
#endif not lint

#include <errno.h>
#include <list.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <tcl.h>

#include "mx.h"

extern void panic();

/*
 * Exported standard position of zero:
 */

Mx_Position Mx_ZeroPosition = {0, 0};

/*
 * The data structure below stores information about a single
 * spy procedure, which is to be called at particular times when
 * a file is modified.
 */

typedef struct Spy {
    int type;			/* When to call proc:  an OR'ed combination of
				 * MX_BEFORE_INSERT, MX_AFTER_INSERT,
				 * MX_BEFORE_DELETE, and MX_AFTER_DELETE. */
    void (*proc)();		/* Procedure to call. */
    ClientData clientData;	/* Parameter to pass to proc. */
    struct File *filePtr;	/* File spy is associated with. */
    struct Spy *nextPtr;	/* Next spy in list for file, or NULL for
				 * end of list. */
} Spy;

/*
 * The data structures below define how a file is represented while
 * it is loaded in memory.  It is stored as a linked list of lines.
 * Each line is just a null-terminated character string whose last
 * non-NULL character is a newline.  In order to avoid lengthy
 * traversals of the line list, a hint pointer is kept to a line
 * that was manipulated recently.  This way, if the next manipulation
 * is on the same line (or a nearby one), the line can be found quickly.
 */

typedef struct Line {
    List_Links links;		/* Lines are doubly-linked into a list.  This
				 * must be the first element of the record. */
    int length;			/* Number of non-NULL bytes in line. */
    char bytes[4];		/* Although only 4 bytes are specified
				 * here, in practice the array will be large
				 * enough to accomodate the line.  This MUST
				 * be the last declaration in the structure. */
} Line;

#define lineSize(numChars) (sizeof(Line) - 4 + (numChars))

typedef struct File {
    int numLines;		/* Number of lines in the file.  Must always
				 * be at least one. */
    List_Links lineList;	/* Header for doubly-linked list of lines
				 * making up this file. */
    int curLineNumber;		/* Index of a recently-accessed line, or -1. */
    Line *curLinePtr;		/* Line corresponding to curLineNumber
				 * or NULL. */
    Spy *spyPtr;		/* First spy in list for this file, or NULL. */
    struct Floater *floaterPtr;	/* First floater in list of those for this
				 * file (NULL if none). */
    int flags;			/* Miscellaneous flags (see below). */
} File;

/*
 * Flag bits for File objects:
 *
 * FILE_MODIFIED:		File has been modified since the last time
 *				it was written out.
 */

#define FILE_MODIFIED 1

/*
 * The data structure below is for a "floater", which is a range of
 * bytes within a file whose endpoints automatically get adjusted as
 * the file is modified.
 */

typedef struct Floater {
    File *filePtr;		/* File to which floater belongs. */
    Mx_Position *firstPtr;	/* Position to be updated:  gives beginning
				 * of range. */
    Mx_Position *lastPtr;	/* Position to be updated:  gives end of
				 * of range. */
    struct Floater *nextPtr;	/* Next floater in linked list of all those
				 * for this file (NULL means end of list). */
} Floater;

/*
 * Forward references:
 */

extern void		FloaterUpdate();
extern Line *		GetLinePtr();

/*
 *----------------------------------------------------------------------
 *
 * Mx_FileLoad --
 *
 *	Read in a file (if name is non-NULL) and create a structure
 *	to keep track of the lines in that file.
 *
 * Results:
 *	The result is a token that may be used to manipulate the
 *	file in other procedure calls, such as Mx_FileWrite and
 *	Mx_InsertBytes.  If the file was already loaded then the
 *	token will refer to a new private file object whose edits
 *	will be independent of any previous Mx_Open.  If name wasn't
 *	NULL, but the file couldn't be read from disk, then NULL
 *	is returned (io_Status will pinpoint the problem).
 *
 * Side effects:
 *	Resources are allocated to store the file, and the file is
 *	read from disk.
 *
 *----------------------------------------------------------------------
 */

Mx_File
Mx_FileLoad(name)
    char *name;			/* Name of the file.  If NULL, a new
				 * file is created with no name. */
{
    register FILE *stream;
    register File *filePtr;
    Line *linePtr;
    register char *p;
    int count, c;

    /*
     * A buffer (pointed to by line) is used to hold the line as it
     * is being read from the file.  A small static buffer is big
     * enough for most purposes, but if it runs out of space then
     * larger and larger buffers get allocated dynamically, so that
     * any size of input line can be handled.
     */

#define STATICLINELENGTH 200
    char staticLine[STATICLINELENGTH];
    char *line = staticLine;
    int curLineLength = STATICLINELENGTH-2;
				/* How many characters may be stored at
				 * line, not including the newline and null. */

    if (name != NULL) {
	stream = fopen(name, "r");
	if (stream == NULL) {
	    return (Mx_File) NULL;
	}
    }

    filePtr = (File *) malloc(sizeof(File));
    filePtr->numLines = 0;
    List_Init(&filePtr->lineList);
    filePtr->curLineNumber = -1;
    filePtr->curLinePtr = NULL;
    filePtr->spyPtr = NULL;
    filePtr->floaterPtr = NULL;
    filePtr->flags = 0;

    /*
     * Read in the lines of the file, one at a time.  Ignore NULL
     * characters:  they will just screw up everything inside the
     * editor.
     */

    if (name != NULL) {
	while (1) {
	    for (count = 0, p = line; ; count++, p++) {
		/*
		 * Grow the line buffer automatically to accomodate lines
		 * of any length.
		 */
		if (count == curLineLength) {
		    int newLength;
		    char *newLine;

		    newLength = curLineLength*2;
		    newLine = (char *) malloc((unsigned) (newLength + 2));
		    bcopy(line, newLine, curLineLength);
		    curLineLength = newLength;
		    p += (newLine - line);
		    if (line != staticLine) {
			free((char *) line);
		    }
		    line = newLine;
		}
		do {
		    c = getc(stream);
		} while (c == 0);
		if ((c == EOF) || (c == '\n')) {
		    break;
		}
		*p = c;
	    }
	    if ((c == EOF) && (count == 0)) {
		break;
	    }
	    *p = '\n';
	    p++;
	    *p = 0;
	    linePtr = (Line *) malloc((unsigned) (lineSize(count+2)));
	    List_InitElement(&linePtr->links);
	    linePtr->length = count+1;
	    (void) strcpy(linePtr->bytes, line);
	    List_Insert(&linePtr->links, LIST_ATREAR(&filePtr->lineList));
	    filePtr->numLines++;
	}
	fclose(stream);
	if (line != staticLine) {
	    free((char *) line);
	}
	if (ferror(stream)) {
	    Mx_FileClose((Mx_File) filePtr);
	    return NULL;
	}
    }

    /*
     * If there are no lines in the file, create a single empty line.
     */

    if (filePtr->numLines == 0) {
	linePtr = (Line *) malloc((unsigned) lineSize(2));
	List_InitElement(&linePtr->links);
	linePtr->length = 1;
	(void) strcpy(linePtr->bytes, "\n");
	List_Insert(&linePtr->links, LIST_ATREAR(&filePtr->lineList));
	filePtr->numLines++;
    }
    
    return (Mx_File) filePtr;
}

/*
 *----------------------------------------------------------------------
 *
 * Mx_FileClose --
 *
 *	Close out a file.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	The given file is closed, which means that all the resources
 *	associated with it in main memory are released.  The caller
 *	should not use "file" anymore.  Nothing happens on disk: the
 *	file is not deleted, nor is it overwritten with what's in
 *	memory.  If Mx_FileWrite hasn't been called since the last
 *	edits were made to the file, then the edits will all be lost.
 *	All spies and floaters for the file are deleted.
 *
 *----------------------------------------------------------------------
 */

void
Mx_FileClose(fileToken)
    Mx_File fileToken;		/* Token for the file (as returned by
				 * Mx_Open. */
{
    register File *filePtr = (File *) fileToken;
    Address delete;

    while (!List_IsEmpty(&filePtr->lineList)) {
	delete = (Address) List_First(&filePtr->lineList);
	List_Remove(List_First(&filePtr->lineList));
	free((char *) delete);
    }
    while (filePtr->spyPtr != NULL) {
	delete = (Address) filePtr->spyPtr;
	filePtr->spyPtr = filePtr->spyPtr->nextPtr;
	free((char *) delete);
    }
    while (filePtr->floaterPtr != NULL) {
	delete = (Address) filePtr->floaterPtr;
	filePtr->floaterPtr = filePtr->floaterPtr->nextPtr;
	free((char *) delete);
    }
    free((char *) filePtr);
}

/*
 *----------------------------------------------------------------------
 *
 * Mx_FileWrite --
 *
 *	Updates the disk copy of a file with the information stored
 *	in the main-memory database.
 *
 * Results:
 *	Returns TCL_OK if the write was successful.  If an error
 *	occurred, then TCL_ERROR is returned and interp->result will
 *	point to a string describing what went wrong.
 *
 * Side effects:
 *	The disk file is overwritten.  This is the ONLY file-related
 *	procedure that actually modifies the disk.
 *
 *----------------------------------------------------------------------
 */

int
Mx_FileWrite(fileToken, fileName, interp)
    Mx_File fileToken;		/* Token for file to be written. */
    char *fileName;		/* Where to write the file. */
    Tcl_Interp *interp;		/* Place to store error message. */
{
    FILE *stream;
    File *filePtr = (File *) fileToken;
    register Line *linePtr;

    stream = fopen(fileName, "w");
    if (stream == NULL) {
	writeError:
	sprintf(interp->result, "couldn't write file \"%.50s\": %.80s",
		fileName, strerror(errno));
	return TCL_ERROR;
    }
    LIST_FORALL(&filePtr->lineList, (List_Links *) linePtr) {
	fputs(linePtr->bytes, stream);
	if (ferror(stream)) {
	    goto writeError;
	}
    }
    fflush(stream);
    fsync(fileno(stream));
    fclose(stream);
    if (ferror(stream)) {
	goto writeError;
    }
    filePtr->flags &= ~FILE_MODIFIED;
    return TCL_OK;
}

/*
 *----------------------------------------------------------------------
 *
 * Mx_GetLine --
 *
 *	Return the information in one line of a file.
 *
 * Results:
 *	The result is a pointer to a string containing the given line
 *	of the given file, or NULL if the given line does not exist.
 *	This string should never be modified in any way by the caller.
 *	If lengthPtr is non-NULL, the word it points to is filled in
 *	with the number of non-NULL characters in the line.  The count
 *	includes the newline character at the end of the line.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

char *
Mx_GetLine(fileToken, line, lengthPtr)
    Mx_File fileToken;		/* Token for the file. */
    int line;			/* Desired line number.  Must be >= 0. */
    int *lengthPtr;		/* Pointer to value to fill in with length
				 * of line, or NULL. */
{
    Line *linePtr;

    linePtr = GetLinePtr((File *) fileToken, line);
    if (linePtr == NULL) {
	return NULL;
    } else {
	if (lengthPtr != NULL) {
	    *lengthPtr = linePtr->length;
	}
	return linePtr->bytes;
    }
}

/*
 *----------------------------------------------------------------------
 *
 * InsertBytes --
 *
 *	Add a range of bytes to a file.
 *
 * Results:
 *	The return value is the position of the byte just after the
 *	last new one added.
 *
 * Side effects:
 *	The file is modified to insert all the characters in the
 *	given string just before the character at the given position
 *	in the file.
 *
 *----------------------------------------------------------------------
 */

Mx_Position
InsertBytes(filePtr, position, string)
    register File *filePtr;	/* Token for file to be modified. */
    Mx_Position position;	/* Position of character before which string
				 * is to be inserted.  This character must
				 * exist in the file. */
    char *string;		/* New characters to be added.  May contain
				 * newlines.  Terminating NULL character is
				 * not inserted. */
{
    Line *linePtr, *newPtr;
    register char *p, *eol;
    Mx_Position last;		/* Final position of last inserted byte. */

    /*
     * Find the line in which the insertion will occur, and move the
     * file's hint to a safe place where it won't be invalidated by
     * the insertion.
     */

    linePtr = GetLinePtr(filePtr, position.lineIndex);
    if (position.lineIndex > 0) {
	filePtr->curLineNumber = position.lineIndex - 1;
	filePtr->curLinePtr = (Line *) List_Prev(&linePtr->links);
    } else {
	filePtr->curLineNumber = -1;
	filePtr->curLinePtr = NULL;
    }

    /*
     * Since the new bytes could contain newlines, chop the insertions
     * up into multiple insertions, each of which handles the stuff on
     * one line.
     */

    p = string;
    last = position;
    for (p = string; ; p = eol+1) {
	int prefixLength, suffixLength, newChunkLength;
	
	/*
	 * Find one line's worth of bytes.
	 */
	
	for (eol = p; *eol != '\n'; eol++) {
	    if (*eol == 0) {
		break;
	    }
	}
	newChunkLength = eol - p;

	/* 
	 * Make a new line consisting of (at most) a prefix from the
	 * old line, new material from string, and a suffix from the
	 * old line.
	 */
	
	if (p == string) {
	    prefixLength = position.charIndex;
	} else {
	    prefixLength = 0;
	}
	if (*eol == 0) {
	    suffixLength = linePtr->length - position.charIndex;
	} else {
	    suffixLength = 0;
	    newChunkLength += 1;	/* Include the newline. */
	    last.lineIndex += 1;
	}
	newPtr = (Line *) malloc((unsigned) (lineSize(prefixLength
		+ newChunkLength + suffixLength + 1)));
	List_InitElement(&newPtr->links);
	newPtr->length = prefixLength + newChunkLength + suffixLength;
	if (prefixLength != 0) {
	    (void) strncpy(newPtr->bytes, linePtr->bytes, prefixLength);
	}
	if (newChunkLength != 0) {
	    (void) strncpy(&newPtr->bytes[prefixLength], p, newChunkLength);
	}
	if (suffixLength != 0) {
	    (void) strncpy(&newPtr->bytes[prefixLength+newChunkLength],
		    &linePtr->bytes[position.charIndex], suffixLength);
	}
	newPtr->bytes[prefixLength + newChunkLength + suffixLength] = 0;
	List_Insert(&newPtr->links, LIST_BEFORE(linePtr));
	filePtr->numLines++;
	if (*eol == 0) {
	    last.charIndex = prefixLength + newChunkLength;
	    break;
	}
    }

    /*
     * Delete the line into which the insertion was made:  it's
     * superseded by the lines just inserted.
     */
    
    List_Remove(&linePtr->links);
    free((char *) linePtr);
    filePtr->numLines--;
    filePtr->flags |= FILE_MODIFIED;
    return last;
}

/*
 *----------------------------------------------------------------------
 *
 * DeleteBytes --
 *
 *	Delete a range of bytes from a file.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	The range of bytes given by first and last is removed
 *	from file.
 *
 *----------------------------------------------------------------------
 */

void
DeleteBytes(filePtr, first, last)
    register File *filePtr;	/* File from which to delete bytes. */
    Mx_Position first;		/* Location of the first byte to be deleted. */
    Mx_Position last;		/* Location of the byte just after the last
				 * one to be deleted.  Must be > first, but
				 * no greater than the end of the file. */
{
    register Line *firstPtr, *lastPtr;
    Line *newPtr;
    int numLines, i, length1, length2;

    numLines = last.lineIndex + 1 - first.lineIndex;
    firstPtr = GetLinePtr(filePtr, first.lineIndex);

    /*
     * Move the hint out of the way so it can't get invalidated
     * by the deletion.
     */

    if (first.lineIndex > 0) {
	filePtr->curLineNumber = first.lineIndex - 1;
	filePtr->curLinePtr = (Line *) List_Prev(&firstPtr->links);
    } else {
	filePtr->curLineNumber = -1;
	filePtr->curLinePtr = NULL;
    }

    /*
     * Delete all the lines in the range but the first and last.
     */
    
    for (i = numLines-2; i > 0; i--) {
	lastPtr = (Line *) List_Next(&firstPtr->links);
	List_Remove(&lastPtr->links);
	free((char *) lastPtr);
	filePtr->numLines--;
    }

    /*
     * Make a new line that combines the remains of the first line and the
     * last.
     */
    
    if (numLines == 1) {
	lastPtr = firstPtr;
    } else {
	lastPtr = (Line *) List_Next(&firstPtr->links);
    }
    length1 = first.charIndex;
    length2 = lastPtr->length - last.charIndex;
    newPtr = (Line *) malloc((unsigned) lineSize(length1+length2+1));
    List_InitElement(&newPtr->links);
    newPtr->length = length1+length2;
    if (length1 > 0) {
	(void) strncpy(newPtr->bytes, firstPtr->bytes, length1);
    }
    (void) strncpy(&newPtr->bytes[length1],
	    &lastPtr->bytes[lastPtr->length-length2], length2+1);
    List_Insert(&newPtr->links, LIST_BEFORE(&firstPtr->links));
    filePtr->numLines++;

    /*
     * Delete the old first and last lines.
     */

    List_Remove(&firstPtr->links);
    free((char *) firstPtr);
    filePtr->numLines--;
    if (lastPtr != firstPtr) {
	List_Remove(&lastPtr->links);
	free((char *) lastPtr);
	filePtr->numLines--;
    }
    filePtr->flags |= FILE_MODIFIED;
}

/*
 *----------------------------------------------------------------------
 *
 * Mx_ReplaceBytes --
 *
 *	This is the only exported procedure that modifies the contents of
 *	a file.  It replaces one string of bytes with another.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	The file is modified.  All the bytes are deleted from first
 *	to the byte just before last.  If first and last are identical
 *	then nothing is deleted.  After the deletion, string is
 *	inserted in the file just before first, unless string is NULL.
 *	Spies are invoked and floaters are updated.
 *
 *----------------------------------------------------------------------
 */

void
Mx_ReplaceBytes(fileToken, first, last, string)
    Mx_File fileToken;		/* Token for file to be modified. */
    Mx_Position first;		/* Position of first character to be deleted.
    				 * Also position before which string is to
				 * be inserted.  This character must exist
				 * in the file. */
    Mx_Position last;		/* Position of character just after last
    				 * character to be deleted.  Must be >=
				 * first, but no greater than the end of
				 * the file.  If same as first, nothing is
				 * deleted. */
    char *string;		/* New characters to be added.  May contain
				 * newlines.  Terminating NULL character is
				 * not inserted.   If string is NULL then
				 * nothing is inserted. */
{
    register File *filePtr = (File *) fileToken;
    register Spy *spyPtr;
    Mx_Position newLast;

    if (((string == NULL) || (*string == 0)) && MX_POS_EQUAL(first, last)) {
	return;
    }

    /*
     * Make sure that the range is valid.  If not, panic.
     */
    
    if ((first.lineIndex >= filePtr->numLines)
	    || (first.lineIndex < 0)
	    || (last.lineIndex >= filePtr->numLines)
	    || (MX_POS_LESS(last, first))) {
	panic("Mx_ReplaceBytes:  bad range.");
    }
    
    /*
     * Invoke all of the spies that want to know before the modification
     * occurs.
     */

    for (spyPtr = filePtr->spyPtr; spyPtr != NULL; spyPtr = spyPtr->nextPtr) {
	if (spyPtr->type & MX_BEFORE) {
	    (*spyPtr->proc)(spyPtr->clientData, filePtr, MX_BEFORE,
	    	    first, last, last, string);
	}
    }

    /*
     * Delete the old bytes, then insert the new ones.
     */

    if (!MX_POS_EQUAL(first, last)) {
	DeleteBytes(filePtr, first, last);
    }
    newLast = first;
    if (string != NULL) {
	newLast = InsertBytes(filePtr, first, string);
    }
    
    /*
     * Fix up the floaters, then invoke all of the spies that want
     * to know after material is inserted.
     */

    FloaterUpdate(filePtr, first, last, newLast);
    for (spyPtr = filePtr->spyPtr; spyPtr != NULL; spyPtr = spyPtr->nextPtr) {
	if (spyPtr->type & MX_AFTER) {
	    (*spyPtr->proc)(spyPtr->clientData, filePtr, MX_AFTER,
	    	    first, last, newLast, string);
	}
    }
}

/*
 *----------------------------------------------------------------------
 *
 * Mx_SpyCreate --
 *
 *	Arrange for a procedure to be invoked whenever a particular
 *	file is modified.  Proc should have the following format:
 *
 *	void
 *	proc(clientData, file, type, first, oldLast, newLast, string)
 *	    ClientData clientData;
 *	    Mx_File file;
 *	    int type;
 *	    Mx_Position first;
 *	    Mx_Position oldLast;
 *	    Mx_Position newLast;
 *	    char *string;
 *	{
 *	}
 *
 *	In the call to proc, file is a token for the file on which
 *	a replace operation was done (or is about to be done).  ClientData
 *	is a copy of the argument provided when the spy was created.
 *	Type describes the type of callback:  MX_BEFORE means a range
 *	of bytes is about to be replaced with the contents of string,
 *	and MX_AFTER means the replacement has already occurred.  First
 *	and oldLast indicate the range of bytes that is about to be
 *	replaced (or was replaced), just as they are passed to
 *	Mx_ReplaceBytes:  first is the location of the first byte (to be)
 *	replaced, and oldLast is the location of the byte just after the
 *	last byte (to be) replaced.  If they are the same, then no bytes
 *	were (will be) replaced.  First is also the location before which
 *	the new bytes are inserted.  NewLast gives the location of the
 *	byte just after the last new one inserted (it is valid only if
 *	type is MX_AFTER).  String gives the new bytes, just as they
 *	were passed to Mx_ReplaceBytes.  It may be NULL, if no new bytes
 *	were to be inserted.  In this case, newLast will be the same as
 *	oldLast.
 *
 *	Floaters are updated after MX_BEFORE procedures are invoked, but
 *	before MX_AFTER procedures are invoked.
 *
 * Results:
 *	The return value is a token for the spy, which can be used
 *	to delete the spy by calling Mx_SpyDelete.
 *
 * Side effects:
 *	Information about the spy is remembered for later.
 *
 * Warning:
 *	A spy procedure (and its descendants) must never call SpyDelete
 *	on itself.  If a spy is deleted while the spy is active, a core
 *	dump is likely.
 *
 *----------------------------------------------------------------------
 */

Mx_Spy
Mx_SpyCreate(fileToken, when, proc, clientData)
    Mx_File fileToken;		/* Token for file to be monitored. */
    int when;			/* OR'ed combination of flags indicating
				 * when to call proc:  MX_BEFORE or
				 * MX_AFTER. */
    void (*proc)();		/* Procedure to call. */
    ClientData clientData;	/* Arbitrary argument to pass to proc. */
{
    register Spy *spyPtr, *prevPtr;
    File *filePtr = (File *) fileToken;

    spyPtr = (Spy *) malloc(sizeof(Spy));
    spyPtr->type = when;
    spyPtr->proc = proc;
    spyPtr->clientData = clientData;
    spyPtr->filePtr = filePtr;
    spyPtr->nextPtr = NULL;

    /*
     * Put the new spy at the end of the list, so that spies will be
     * invoked in the order they were created.
     */

    if (filePtr->spyPtr == NULL) {
	filePtr->spyPtr = spyPtr;
    } else {
	for (prevPtr = filePtr->spyPtr; prevPtr->nextPtr != NULL;
		prevPtr = prevPtr->nextPtr) {
	    /* Null loop body. */
	}
	prevPtr->nextPtr = spyPtr;
    }
    return (Mx_Spy) spyPtr;
}

/*
 *----------------------------------------------------------------------
 *
 * Mx_SpyDelete --
 *
 *	Nullify the effects of a previous call to Mx_SpyCreate, so
 *	that the spy procedure will no longer be called.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	Resources associated with the spy are freed, and the spy procedure
 *	will not be called anymore.  The caller should not use the spy
 *	token anymore.
 *
 *----------------------------------------------------------------------
 */

void
Mx_SpyDelete(spy)
    Mx_Spy spy;			/* Spy to be deleted. */
{
    register Spy *spyPtr = (Spy *) spy;
    register Spy *prevPtr;

    if (spyPtr->filePtr->spyPtr == spyPtr) {
	spyPtr->filePtr->spyPtr = spyPtr->nextPtr;
    } else {
	for (prevPtr = spyPtr->filePtr->spyPtr; prevPtr->nextPtr != spyPtr;
		prevPtr = prevPtr->nextPtr) {
	    /* Null loop body. */
	}
	prevPtr->nextPtr = spyPtr->nextPtr;
    }
    free((char *) spyPtr);
}

/*
 *----------------------------------------------------------------------
 *
 * Mx_EndOfFile --
 *
 *	This procedure returns the location of the last or next-to-last
 *	character in a file.
 *
 * Results:
 *	The return value is the position of the last character in the
 *	file, which is always a newline character.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

Mx_Position
Mx_EndOfFile(fileToken)
    Mx_File fileToken;		/* File whose size is wanted. */
{
    register File *filePtr = (File *) fileToken;
    Mx_Position position;

    position.lineIndex = filePtr->numLines - 1;
    position.charIndex = ((Line *) List_Last(&filePtr->lineList))->length - 1;
    return position;
}

/*
 *----------------------------------------------------------------------
 *
 * Mx_FloaterCreate --
 *
 *	Create a floater, which is a range that floats as the file is
 *	edited.
 *
 * Results:
 *	The return value is a token that may be used to refer to the
 *	floater in Mx_FloaterDelete.
 *
 * Side effects:
 *	From now on, the positions indicated by *firstPtr and *lastPtr
 *	will be automatically modified by this module so that they refer
 *	to the same characters in file, no matter how much information
 *	is inserted or deleted before or after the range.  If stuff is
 *	inserted in the range delimited by *firstPtr and *lastptr, then
 *	the range expands to include the new insertion as well as its
 *	old contents.  If stuff is deleted inside the range, then the
 *	range shrinks.  If all the characters in the range get deleted,
 *	*firstPtr will point to a location AFTER *lastPtr.
 *
 *----------------------------------------------------------------------
 */

Mx_Floater
Mx_FloaterCreate(file, firstPtr, lastPtr)
    Mx_File file;		/* File to which the positions refer. */
    Mx_Position *firstPtr;	/* Pointer to position giving first byte
				 * of floating range. */
    Mx_Position *lastPtr;	/* Pointer to position giving last byte of
				 * of floating range.  Must be >= *firstPtr. */
{
    File *filePtr = (File *) file;
    register Floater *floaterPtr;

    floaterPtr = (Floater *) malloc(sizeof(Floater));
    floaterPtr->filePtr = filePtr;
    floaterPtr->firstPtr = firstPtr;
    floaterPtr->lastPtr = lastPtr;
    floaterPtr->nextPtr = filePtr->floaterPtr;
    filePtr->floaterPtr = floaterPtr;
    return (Mx_Floater) floaterPtr;
}

/*
 *----------------------------------------------------------------------
 *
 * Mx_FloaterDelete --
 *
 *	Delete a floating range.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	From now on, the positions given when floater was created
 *	will no longer be updated as the file changes.  The caller
 *	should not use the floater token anymore.
 *
 *----------------------------------------------------------------------
 */

void
Mx_FloaterDelete(floater)
    Mx_Floater floater;		/* Token for floater to be deleted. */
{
    register Floater *floaterPtr = (Floater *) floater;
    register Floater *fl2Ptr;

    if (floaterPtr->filePtr->floaterPtr == floaterPtr) {
	floaterPtr->filePtr->floaterPtr = floaterPtr->nextPtr;
    } else {
	for (fl2Ptr = floaterPtr->filePtr->floaterPtr;
		fl2Ptr->nextPtr != floaterPtr; fl2Ptr = fl2Ptr->nextPtr) {
	    /* Empty loop body. */
	}
	fl2Ptr->nextPtr = floaterPtr->nextPtr;
    }
    free((char *) floaterPtr);
}

/*
 *----------------------------------------------------------------------
 *
 * Mx_Offset --
 *
 *	Compute a position in a file, based on an old position
 *	and an offset from that position.
 *
 * Results:
 *	The return value is the position of the character in "file"
 *	"offset" characters from "position".  Offset may be either 
 *	positive or negative.  If an offset of the given amount would
 *	run off the end or beginning of the file, then the position
 *	of the last or first character in the file is returned
 *	(respectively).  Offset may also be zero.  This provides a
 *	way of "rounding" a position to the closest actual position
 *	in the file (e.g. if position specifies a character location
 *	off the end of a line).
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

Mx_Position
Mx_Offset(file, position, offset)
    Mx_File file;		/* Token for file that position pertains to. */
    Mx_Position position;	/* Position of a character within file. */
    int offset;			/* Number of characters forward or backward
				 * that position should be offset. */
{
    register File *filePtr = (File *) file;
    register Line *linePtr;

    /*
     * Find the line containing the initial position, and round the
     * position off if necessary to keep it at a legal position in
     * the file.
     */

    linePtr = GetLinePtr(filePtr, position.lineIndex);
    if (linePtr == NULL) {
	if (position.lineIndex < 0) {
	    linePtr = (Line *) List_First(&filePtr->lineList);
	    position.lineIndex = 0;
	    position.charIndex = 0;
	} else {
	    linePtr = (Line *) List_Last(&filePtr->lineList);
	    position.lineIndex = filePtr->numLines - 1;
	    position.charIndex = linePtr->length - 1;
	}
    } else {
	if (position.charIndex >= linePtr->length) {
	    position.charIndex = linePtr->length - 1;
	}
    }

    /*
     * Offset the position, then move forward or backward lines
     * until we're at a valid position on a line.
     */
    
    position.charIndex += offset;
    while (position.charIndex < 0) {
	if (position.lineIndex == 0) {
	    position.charIndex = 0;
	    return position;
	}
	linePtr = (Line *) List_Prev(&linePtr->links);
	position.lineIndex --;
	position.charIndex += linePtr->length;
    }
    while (position.charIndex >= linePtr->length) {
	if ((position.lineIndex+1) == filePtr->numLines) {
	    position.charIndex = linePtr->length - 1;
	    return position;
	}
	position.lineIndex ++;
	position.charIndex -= linePtr->length;
	linePtr = (Line *) List_Next(&linePtr->links);
    }

    return position;
}

/*
 *----------------------------------------------------------------------
 *
 * Mx_FileModified --
 *
 *	Tell whether or not a file is "dirty".
 *
 * Results:
 *	1 is returned if the file has been modified since the last
 *	time it was written out or the last time Mx_MarkClean was
 *	called.  0 is returned if it hasn't
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

int
Mx_FileModified(file)
    Mx_File file;		/* Token for file. */
{
    if (((File *) file)->flags & FILE_MODIFIED) {
	return 1;
    } else {
	return 0;
    }
}

/*
 *----------------------------------------------------------------------
 *
 * Mx_MarkClean --
 *
 *	Clear a file's "dirty" bit.  No matter how the file has
 *	been modified in the past, after this call Mx_FileModified
 *	will return 0 until the next modification to the file.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	The file's modified bit is cleared.
 *
 *----------------------------------------------------------------------
 */

void
Mx_MarkClean(file)
    Mx_File file;		/* Token for file. */
{
    ((File *) file)->flags &= ~FILE_MODIFIED;
}

/*
 *----------------------------------------------------------------------
 *
 * GetLinePtr --
 *
 *	This procedure is used internally to locate a particular
 *	line of a file.
 *
 * Results:
 *	The result is a pointer to the line'th line of the given
 *	file, or NULL if that line doesn't exist.
 *
 * Side effects:
 *	The line hint for the file gets updated.
 *
 *----------------------------------------------------------------------
 */


Line *
GetLinePtr(filePtr, line)
    register File *filePtr;	/* File whose line is wanted. */
    int line;			/* Desired line number.  Must be >= 0. */
{
    register Line *linePtr;
    register int current;

    current = filePtr->curLineNumber;
    linePtr = filePtr->curLinePtr;
    if (linePtr == NULL) {
	if (List_IsEmpty(&filePtr->lineList)) {
	    return NULL;
	}
	current = 0;
	linePtr = (Line *) List_First(&filePtr->lineList);
    }
    while (1) {
	if (current == line) {
	    filePtr->curLineNumber = current;
	    filePtr->curLinePtr = linePtr;
	    return linePtr;
	}
	if (current < line) {
	    current++;
	    linePtr = (Line *) List_Next((List_Links *) linePtr);
	} else {
	    current--;
	    linePtr = (Line *) List_Prev((List_Links *) linePtr);
	}

	if (List_IsAtEnd(&filePtr->lineList, &linePtr->links)) {
	    return NULL;
	}
    }
}

/*
 *----------------------------------------------------------------------
 *
 * FloaterUpdate --
 *
 *	This procedure is invoked after each file modification to
 *	update the floaters for that file.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	All floaters for the file get updated to reflect the change.
 *
 *----------------------------------------------------------------------
 */

void
FloaterUpdate(filePtr, first, last, newLast)
    File *filePtr;		/* File that was modified. */
    Mx_Position first;		/* Location of first byte in file that
				 * was deleted. */
    Mx_Position last;		/* Location of byte just after last one
    				 * in file that was deleted. */
    Mx_Position newLast;	/* Location in file of byte just after
    				 * last one inserted at first. */
{
    register Floater *floaterPtr;
    register Mx_Position *posPtr;

    for (floaterPtr = filePtr->floaterPtr; floaterPtr != NULL;
	    floaterPtr = floaterPtr->nextPtr) {
	posPtr = floaterPtr-> firstPtr;
	if (MX_POS_LEQ(first, *posPtr)) {
	    if (MX_POS_LESS(*posPtr, last)) {
		*posPtr = newLast;
	    } else if (posPtr->lineIndex > last.lineIndex) {
		posPtr->lineIndex += newLast.lineIndex - last.lineIndex;
	    } else {
		posPtr->lineIndex = newLast.lineIndex;
		posPtr->charIndex += newLast.charIndex - last.charIndex;
	    }
	}
	posPtr = floaterPtr->lastPtr;
	if (MX_POS_LEQ(first, *posPtr)) {
	    if (MX_POS_LESS(*posPtr, last)) { 
		*posPtr = Mx_Offset((Mx_File) filePtr, first, -1);
	    } else if (posPtr->lineIndex > last.lineIndex) {
		posPtr->lineIndex += newLast.lineIndex - last.lineIndex;
	    } else {
		posPtr->lineIndex = newLast.lineIndex;
		posPtr->charIndex += newLast.charIndex - last.charIndex;
	    }
	}
    }
}
