/**
 *#########################################################################
 *
 * A component of the Greenstone Librarian Interface (GLI) application, 
 * part of the Greenstone digital library software suite from the New 
 * Zealand Digital Library Project at the University of Waikato, 
 * New Zealand.
 *
 * Author: John Thompson, Greenstone Project, New Zealand Digital Library, 
 *         University of Waikato
 *         http://www.nzdl.org
 *
 * Copyright (C) 2004 New Zealand Digital Library, University of Waikato
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *########################################################################
 */

package org.greenstone.gatherer.util;

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.text.*;
import org.greenstone.gatherer.DebugStream;
import org.greenstone.gatherer.gui.GLIButton;

/** A Document whose underlying data is stored in a RandomAccessFile, and whose Element implementations lack the memory hogging problems associated with anything that extends the AbstractDocument class. This Document, for reasons of time constraints and sanity, only provides an editting ability of appending new lines to the end of the current document, perfect for our logging needs, completely useless for text editing purposes. Furthermore, since the append actions tend to somewhat swamp the IO, I'll temporarily store strings in the structure model, and write them out using a separate thread. 
 * @author John Thompson, Greenstone Project, New Zealand Digital Library, University of Waikato
 * @version 2.41 final
 */
public class AppendLineOnlyFileDocument
    implements Document {
    /** A default string to append at the start of each log file. Of special note is the beginning '.' character that will, upon completion of the import/build process, be replaced with a letter indicating the final status of this build attempt. */
    static final private String GLI_HEADER_STR = ".";
    /** The root element of the document model. */
    private AppendLineOnlyFileDocumentElement root_element;
    /** The current owner of this document, useful for callbacks when certain tasks are complete. */
    private AppendLineOnlyFileDocumentOwner owner;
    /** Is this document a build log, and hence has some extra functionality, of just a straight log. */
    private boolean build_log = false;
    /** A list of listeners attached to this document. */
    private EventListenerList listeners_list;
    /** This properties hash map allows you to store any metadata about this document for later review. */
    private HashMap properties;
    /** Indicates the current length of the underlying file (which may not equal the current number of characters in the document). */
    private long length;
    /** The random access file which will back this document model. */
    private RandomAccessFile file;
    /** The filename given to the underlying file. */
    private String filename;
    /** An independant thread responsible for writing the 'in memory' contents of the document model to the random access file as appropriate. This is done so IO lag does not effect the gui as much as it used to. */
    private WriterThread writer;

    
    public AppendLineOnlyFileDocument(String filename) {
	this(filename, true);
    }

    /** The constructor is responsible for creating several necessary data structures as well as opening the random access file. When it creates the new file, as it is wont to do, it will immediately add the special header line defined above.
     * @param filename the absolute path name of the file as a String
     * @see org.greenstone.gatherer.util.AppendLineOnlyFileDocument.AppendLineOnlyFileDocumentElement
     * @see org.greenstone.gatherer.util.AppendLineOnlyFileDocument.WriterThread
     * @see org.greenstone.gatherer.util.StaticStrings#NEW_LINE_CHAR
     */
    public AppendLineOnlyFileDocument(String filename, boolean build_log) {
	DebugStream.println("Creating log: " + filename);
	// Initialization 
	this.build_log = build_log;
	this.filename = filename;
	this.listeners_list = new EventListenerList();
	this.properties = new HashMap();
	this.writer = new WriterThread();
	writer.start();
	// Open underlying file
	try {
	    file = new RandomAccessFile(filename, "rw");
	    // Create the root element.
	    length = file.length();
	    root_element = new AppendLineOnlyFileDocumentElement();
	    // Now quickly read through the underlying file, building an Element for each line.
	    long start_offset = 0L;
	    file.seek(start_offset);
	    int character = -1;
	    while((character = file.read()) != -1) {
		if(character == StaticStrings.NEW_LINE_CHAR) {
		    long end_offset = file.getFilePointer();
		    Element child_element = new AppendLineOnlyFileDocumentElement(start_offset, end_offset);
		    root_element.add(child_element);
		    child_element = null;
		    start_offset = end_offset;
		}
	    }
	    // If there we no lines found, and we are a build log, then append the file header.
	    if(build_log && root_element.getElementCount() == 0) {
		appendLine(GLI_HEADER_STR);
	    }
	}
	catch (Exception error) {
	    DebugStream.printStackTrace(error);
	}
    }

    /** Adds a document listener for notification of any changes. 
     * @param listener the new DocumentListener to keep track of
     */
    public void addDocumentListener(DocumentListener listener) {
	///ystem.err.println("addDocumentListener(" + listener + ")");
	listeners_list.add(DocumentListener.class, listener);
    }

    /** Append some content after the document. 
     * @param str the String to add as a new line in the document
     * @see org.greenstone.gatherer.util.AppendLineOnlyFileDocument.AppendLineOnlyFileDocumentElement
     * @see org.greenstone.gatherer.util.AppendLineOnlyFileDocument.AppendLineOnlyFileDocumentEvent
     * @see org.greenstone.gatherer.util.AppendLineOnlyFileDocument.WriterThread#queue
     */
    public void appendLine(String str) {
	// Ensure the string ends in a newline
	try {
	    if (!str.endsWith("\n")) {
		str = str + "\n";
	    }
	    int str_length = str.getBytes("UTF-8").length;
	    long start_offset = length;
	    long end_offset = start_offset + (long) str_length;
	    length = length + str_length;
	    // Create a new element to represent this line
	    AppendLineOnlyFileDocumentElement new_line_element = new AppendLineOnlyFileDocumentElement(start_offset, end_offset, str);
	    root_element.add(new_line_element);
	    // Queue the content to be written.
	    writer.queue(new_line_element);
	    // Now fire an event so everyone knows the content has changed.
	    DocumentEvent event = new AppendLineOnlyFileDocumentEvent(new_line_element, (int)start_offset, str_length, DocumentEvent.EventType.INSERT);
	    Object[] listeners = listeners_list.getListenerList();
	    for (int i = listeners.length - 2; i >= 0; i = i - 2) {
		if (listeners[i] == DocumentListener.class) {
		    ((DocumentListener)listeners[i+1]).insertUpdate(event);
		}
	    }
	    listeners = null;
	    event = null;
	    new_line_element = null;
	}
	catch(Exception error) {
	    DebugStream.printStackTrace(error);
	}
    }

    /** Returns a position that will track change as the document is altered. 
     * @param offs the position offset as an int
     * @return the Position to be monitored
     *  @see org.greenstone.gatherer.util.AppendLineOnlyFileDocument.AppendLineOnlyFileDocumentPosition
     */
    public Position createPosition(int offs)  {
	return new AppendLineOnlyFileDocumentPosition(offs);
    }

	/** ensure the current build_log file has finished transferring from memory to disk, and that the random access
	* file is properly closed when a collection is closed.  Else we can't delete the just-closed collection since the 
	* build_log file resource is still kept open. */
	public void close() {
		if(writer == null) { // closed already
			return;
		}
		// get rid of the writer and then close the file
		setExit();
		writer = null;
		try {	
			file.close();
		} catch(Exception exception) {
			DebugStream.println("Exception in AppendLineOnlyFileDocument.close() - Unable to close internal file");
			DebugStream.printStackTrace(exception);
		}
	}
	
    /** The destructor is implemented to ensure the current log file has finished transferring from memory to disk, and that the random access file is properly closed, before GLI exits.
     * @see org.greenstone.gatherer.util.AppendLineOnlyFileDocument.WriterThread#finish
     */
    public void destroy() {
	try {
		if(writer != null) {
			writer.finish();
			writer = null;
			file.close();
		}
	}
	catch(Exception exception) {
	    DebugStream.println("Exception in AppendLineOnlyFileDocument.destroy() - unexpected");
	    DebugStream.printStackTrace(exception);
	}
    }

    /** Gets the default root element for the document model. 
     * @return the root Element of this document
     */
    public Element getDefaultRootElement() {
	return root_element;
    }

    /** Returns the length of the data. 
     * @return the length as an int (not a long)
     */
    public int getLength() {
	return (int) length;
    }

    /** A version of get length which returns the offset to the start of the last line in the document.
     * @return the offset length as an int
     * @see org.greenstone.gatherer.util.AppendLineOnlyFileDocument.AppendLineOnlyFileDocumentElement
     */
    public int getLengthToNearestLine() {
	AppendLineOnlyFileDocumentElement last_element = (AppendLineOnlyFileDocumentElement)root_element.getElement(root_element.getElementCount() - 1);
	if(last_element != null ) {
	    return last_element.getStartOffset();
	}
	else {
	    return (int) length; // The best we can do.
	}
    }
	
    /** Retrieve a property that has previously been set for this document.
     * @param key the key String under which the vaue is stored
     * @return the value which is also a String, or null if no such value
     */
    public Object getProperty(Object key) {
	return properties.get(key);
    }

    /** Determine if the writer thread is still running, ie a log file which is currently open for writing
     * @return true if the writer is still active, false otherwise
     * @see org.greenstone.gatherer.util.AppendLineOnlyFileDocument.WriterThread#isStillWriting
     */
    public boolean isStillWriting() {
	return writer.isStillWriting();
    }

    /** Gets a sequence of text from the document. 
     * @param offset the starting offset for the fragment of text required, as an int
     * @param l the length of the text, also as an int
     * @see org.greenstone.gatherer.util.AppendLineOnlyFileDocument.AppendLineOnlyFileDocumentElement
     * @see org.greenstone.gatherer.util.StaticStrings#EMPTY_STR
     */
    public String getText(int offset, int l) 
	throws BadLocationException {

	if (l == 0) {
	    return "";
	}

	try {
	    return read((long) offset, l);
	}
	catch (Exception ex) {
	}

	return "";

//  	// System.err.println("AppendLineOnlyFileDocument::getText() request...");
//  	String request = "getText(" + offset + ", " + l + ")";
//  	///ystem.err.println(request);
//  	String text = null;
//  	if(text == null || text.length() < l) {
//  	    try {
//  		int file_length = (int) file.length();
//  		///ystem.err.println("file_length = " + file_length + ", length = " + length);
//  		if(l == 0) {
//  		    text = StaticStrings.EMPTY_STR;
//  		}
//  		else if(0 <= offset && offset < length && (offset + l) <= length) {
//  		    if(offset < file_length) {
//  			text = read((long)offset, l);
//  			if(text.length() != l) {
//  			    ///ystem.err.println("Asked for " + l + " characters of text. But only read " + text.length());
//  			    throw new BadLocationException("AppendLineOnlyDocument.getText(" + offset + ", " + l + ")", offset); 
//  			}
//  		    }
//  		    else {
//  			int index = root_element.getElementIndex(offset);
//  			if(index < root_element.getElementCount()) {
//  			    AppendLineOnlyFileDocumentElement element = (AppendLineOnlyFileDocumentElement) root_element.getElement(index);
//  			    text = element.getContent();
//  			}
//  			else {
//  			    ///ystem.err.println("Index is " + index + " but there are only " + root_element.getElementCount() + " children.");
//  			    throw new BadLocationException("AppendLineOnlyDocument.getText(" + offset + ", " + l + ")", offset); 
//  			}
//  		    }
		    
//  		}
//  	    }
//  	    catch (IOException error) {
//  		DebugStream.printStackTrace(error);
//  	    }
//  	    if(text == null) {
//  		///ystem.err.println("Text is null.");
//  		throw new BadLocationException("AppendLineOnlyDocument.getText(" + offset + ", " + l + ")", offset);
//  	    }
//  	}
//  	request = null;
//  	return text;
    }

    /** Fetches the text contained within the given portion of the document. 
     * @param offset the starting offset of the desired text fragment, as an int
     * @param length the length of the text also as an int
     * @param txt the Segment into which the text fragment should be placed
     */
    public void getText(int offset, int length, Segment txt) 
	throws javax.swing.text.BadLocationException  {
	String str = getText(offset, length);
	txt.array = str.toCharArray();
	txt.count = str.length();
	txt.offset = 0;
	str = null;
    }
	
    /** Set a property for this document.
     * @param key the key to store the value under, as a String
     * @param value the property value itself also as a String
     */
    public void putProperty(Object key, Object value) {
	properties.put(key, value);
    }

    /** Determine if this document is ready by testing if it has an open descriptor to the random access file.
     * @return true if the file exists thus the document is ready, false otherwise
     */
    public boolean ready() {
	return (file != null);
    }
	
    /** Removes a document listener.
     * @param listener the Document we wish to remove
     */
    public void removeDocumentListener(DocumentListener listener) {
	listeners_list.remove(DocumentListener.class, listener);
    }

    /** Whenever the user requests a change of the loaded log, this method is called to ensure the previous log has been completely written to file. Note that this only garuentees that text fragments currently queued for writing will be correctly written - while the behaviour is undefined for text fragments added after this call (some may write depending on several race conditions).
     * @see org.greenstone.gatherer.util.AppendLineOnlyFileDocument.WriterThread#exit
     * @see org.greenstone.gatherer.util.AppendLineOnlyFileDocument.WriterThread#finish
     */
    public void setExit() {
	writer.exit();
	writer.finish();
    }

    /** Establish the current owner of this document.
     * @param owner the current AppendLineOnlyFileDocumentOwner
     */
    public void setOwner(AppendLineOnlyFileDocumentOwner owner) {
	this.owner = owner;
    }

    /** To record the final state of the logging process we reserve a single character at the start of the file, and then replace it when the build process is complete.
     * @param character the final status char to replace the X at the start of the log
     */
    public synchronized void setSpecialCharacter(char character) {
	if(build_log) {
	    try {
		file.seek(0L);
		file.write((int)character);
	    }
	    catch (Exception error) {
		DebugStream.printStackTrace(error);
	    }
	}
    }

    /** Request a text representation of this document which currently returns the filename.
     * @return the text as a String
     */
    public String toString() {
	return filename;
    }

    /** This method reads a fragment of text from the underlying random access file. It is synchronized so that the file isn't being written to at the same time.
     * @param start_offset the offset within the file to set the read pointer at, as a long
     * @param l the number of characters to read as an int
     */
    private synchronized String read(long start_offset, int l) 
	throws IOException {
	//print("read(" + start_offset + ", " + l + ")... ");
	byte[] buffer = new byte[l];
	file.seek(start_offset);
	int result = file.read(buffer, 0, l);
	return new String(buffer, "UTF-8");
	//print("read() complete");
    }

    /** This methods writes a fragment of text to the underlying random access file. It is synchronized so that the file isn't being read from at the same time.
     * @param start_offset the offset within the file to set the write pointer at, as a long
     * @param end_offset the final offset of the new text fragment within the file, as a long. This is used to extend the file to be the correct length
     * @param str the String to be written
     * @param l the number of characters from str to be written, as an int
     */
    private synchronized void write(long start_offset, long end_offset, String str, int l) 
	throws IOException {	
	//print("write(" + start_offset + ", " + end_offset + ", " + str + ", " + l + ")");
	file.setLength(end_offset);
	file.seek(start_offset);
	file.write(str.getBytes("UTF-8"), 0, l);
	//print("write() complete");
    }

    /** This class provides the building-block for the document model. Each element is a line in the document, or has no content but contains several child elements. Due to the basic nature of what we are modelling only the root node has children. */
    private class AppendLineOnlyFileDocumentElement
	extends ArrayList
	implements Element {
	/** Our parent Element. */
	private Element parent;
	/** The offset to the end of our fragment(s) of text, in respect to the entire document. */
	private long end;
	/** The offset to the start of our fragment(s) of text, in respect to the entire document. */
	private long start;
	/** If we haven't been written to file yet, this contains the text fragment itself. */
	private String content;

	/** Construct a new root element, which can have no content, but calculates its start and end from its children. */
	public AppendLineOnlyFileDocumentElement() {
	    super();
	    this.end = 0L;
	    this.parent = null;
	    this.start = 0L;
	}

	/** Construct a new element, whose content is found in bytes start to end - 1 within the random access file backing this document.
	 * @param start the starting offset as a long
	 * @param end the ending offset as a long
	 */
	public AppendLineOnlyFileDocumentElement(long start, long end) {
	    super();
	    this.end = end;
	    this.parent = null;
	    this.start = start;
	}

	/** Construct a new element, whose content is provided, but should at some later time be written to bytes start to end - 1 in the random access file backing this document.
	 * @param start the starting offset as a long
	 * @param end the ending offset as a long
	 * @param content the text fragment String which we represent in the model
	 */
	public AppendLineOnlyFileDocumentElement(long start, long end, String content) {
	    super();
	    this.content = content;
	    this.end = end;
	    this.parent = null;
	    this.start = start;
	}

	/** Add the given node as one of our children.
	 * @param child the new child Element
	 */
	public void add(Element child) {
	    super.add(child);
	    ((AppendLineOnlyFileDocumentElement)child).setParent(this);
	}

	/** Having written the content to file this method removes it from the element. */ 
	public void clearContent() {
	    content = null;
	}

	/** This document does not allow content markup. 
	 * @return always returns null
	 */
	public AttributeSet getAttributes() {
	    return null;
	}

	/** Retrieve the current content of this element, which may be the text fragment this element represents.
	 * @return either the text fragment as a String, or null if the fragment has already been written to disk
	 */
	public String getContent() {
	    return content;
	}

	/** Fetches the document associated with this element. 
	 * @return the AppendLineOnlyDocument containing this element
	 * @see org.greenstone.gatherer.util.AppendLineOnlyFileDocument
	 */
	public Document getDocument() {
	    return AppendLineOnlyFileDocument.this;
	}

	/** Fetches the child element at the given index. 
	 * @param index the int index of the element to retrieve
	 * @return the requested Element
	 */
	public Element getElement(int index) {
	    Element element;
	    if(0 <= index && index < size()) {
		element = (Element) get(index);
	    }
	    else {
		throw new IndexOutOfBoundsException("AppendLineOnlyDocument.AppendLineOnlyFileDocumentElement.getElement(" + index + ")");
	    }
	    return element;
	}
          
	/** Gets the number of child elements contained by this element. 
	 * @return an int
	 */
	public int getElementCount() {
	    return size();
	}

	/** Gets the child element index closest to the given offset.
	 * @param offset the offset within the text of the document, which due to the way the model is built must fll within the bounds of one of our child nodes
	 * @return the closest index as an int
	 */
	public int getElementIndex(int offset) {
	    int index = -1;
	    if(parent != null) {
		index = -1;
	    }
	    else if(offset < 0) {
		index = 0;
	    }
	    else if(offset >= length) {
		index = size() - 1;
	    }
	    else {
		int size = size();
		for(int i = 0; index == -1 && i < size; i++) {
		    Element child = (Element) get(i);
		    if(child.getStartOffset() <= offset && offset < child.getEndOffset()) {
			index = i;
		    }
		    child = null;
		}
	    }
	    return index;
	}
          
	/** Fetches the offset from the beginning of the document that this element ends at. 
	 * @return the offset as an int
	 */
	public int getEndOffset() {
	    if(parent != null) {
		return (int) end;
	    }
	    // Return the Documents length.
	    else {
		return (int) length;
	    }
	}
          
	/** This method retrieves the name of the element, however names are not important in this document so the name is always an empty string.
	 * @return an empty String
	 * @see org.greenstone.gatherer.util.StaticStrings#EMPTY_STR
	 */
	public String getName() {
	    return StaticStrings.EMPTY_STR;
	}
          
	/** Fetches the parent element. 
	 * @return the parent Element
	 */
	public Element getParentElement() {
	    return parent;
	}
          
	/** Fetches the offset from the beginning of the document that this element begins at. 
	 * @return the offset as an int
	 */
	public int getStartOffset() {
	    if(parent != null) {
		return (int) start;
	    }
	    else {
		return 0;
	    }
	}
          
	/** Since this is a very simple model, only the root node can have children. All the children are leaves. 
	 * @return true if this is the root node, false otherwise
	 */
	public boolean isLeaf() {
	    return (parent != null);
	} 

	/** Establish the parent of this element
	 * @param parent the parent Element
	 */
	public void setParent(Element parent) {
	    this.parent = parent;
	}
    }
    // AppendLineOnlyFileDocumentElement

    /** This event is created to encapsulate the details of some change to the document. */
    private class AppendLineOnlyFileDocumentEvent
	implements DocumentEvent {
	/** The type of change event. */
	private DocumentEvent.EventType type;
	/** The element this change occured to, or in. */
	private AppendLineOnlyFileDocumentElement element;
	/** Another encapsulating class which contains further detail about the change itself. */
	private AppendLineOnlyFileDocumentElementChange element_change;
	/** The length of the text fragment affected by the change. */
	private int len;
	/** The offset within the document of the start of the change. */
	private int offset;
	
	/** Construct a new AppendLineOnlyFileDocumentEvent given the pertinant details.
	 * @param element the AppendLineOnlyFileDocumentElement affected by this change
	 * @param offset the offset within the document of the start of the change
	 * @param len the length of the text fragment affected by the change
	 * @param type the type of change event
	 */
	public AppendLineOnlyFileDocumentEvent(AppendLineOnlyFileDocumentElement element, int offset, int len, DocumentEvent.EventType type) {
	    this.element = element;
	    this.element_change = null;
	    this.len = len;
	    this.offset = offset;
	    this.type = type;
	}

	/** Retrieve the docment which generated this event.
	 * @return the Document
	 * @see org.greenstone.gatherer.util.AppendLineOnlyFileDocument
	 */
	public Document getDocument() {
	    return AppendLineOnlyFileDocument.this;
	}

	/** Retrieve the length of the change.
	 * @return the length as an int
	 */
	public int getLength() {
	    return len;
	}

	/** Retrieve the start offset of the change.
	 * @return the offset as an int
	 */
	public int getOffset() {
	    return offset;
	}

	/** Retrieve the type of change.
	 * @return the type as a DocumentEvent.EventType
	 */
	public DocumentEvent.EventType getType() {
	    return type;
	}

	/** Retrieve the element change object which further describes the changes. Of course given that we only allow the appending of new lines there really is only one type of event.
	 * @param elem implementation side-effect
	 * @return a DocumentEvent.ElementChange
	 * @see org.greenstone.gatherer.util.AppendLineOnlyFileDocument.AppendLineOnlyFileDocumentEvent.AppendLineOnlyFileDocumentElementChange
	 */
	public DocumentEvent.ElementChange getChange(Element elem) {
	    if(element_change == null) {
		element_change = new AppendLineOnlyFileDocumentElementChange();
	    }
	    return element_change;
	}

	/** A pretty unexciting implementation of the document element change class. This usually describes the myriad of possible change events that can occur on a document. However we only allow line appends so there is really only one type of change. */
	private class AppendLineOnlyFileDocumentElementChange
	    implements DocumentEvent.ElementChange {
	    /** This array always contains the one Element that was appended. */ 
	    private Element[] children_added;
	    /** This Element array is always empty. */
	    private Element[] children_removed;
	    /** The index of the affected element within its parents children. */
	    private int index;
	    /** Construct a new AppendLineOnlyFileDocumentElementChange with the default 'append line' details. */
	    public AppendLineOnlyFileDocumentElementChange() {
		children_added = new Element[1];
		children_added[0] = element;
		children_removed = new Element[0];
		index = root_element.indexOf(element);
	    }

	    /** Gets the child element that was appended to the parent element. 
	     * @return an Element[] containing the added element
	     */
	    public Element[] getChildrenAdded() {
		return children_added;
	    }

	    /** This model does not allow elements to be removed.
	     * @return an Element[] containing nothing
	     */
	    public Element[] getChildrenRemoved() {
		return children_removed;
	    }
          
	    /** Returns the root element, as our document structure is only two layers deep.
	     * @return the root Element
	     */
	    public Element getElement() {
		return root_element;
	    }
          
	    /** Fetches the index within the element represented. 
	     * @return an int specifying the index of change within the root element
	     */
	    public int getIndex() {
		return index;
	    }
	}
	// AppendLineOnlyFileDocumentElementChange
    }
    // AppendLineOnlyFileDocumentEvent
    
    /** This class denoted a position within our document by an offset. Quite possibly the last interesting class I've ever had to deal with, but you have to implement it so here goes... */
    private class AppendLineOnlyFileDocumentPosition
	implements Position {
	/** The offset within our document. */
	private int offset;
	/** Construct a new position given an offset.
	 * @param offset the offset as an int
	 */
	public AppendLineOnlyFileDocumentPosition(int offset) {
	    this.offset = offset;
	}
	/** Retrieve the offset of this position.
	 * @return the offset as an int
	 */
	public int getOffset() {
	    return offset;
	}
    }
    // AppendLineOnlyFileDocumentPosition
    
    /** This thread is responsible to writing the in memory text fragment contents of the document elements out to the random access file. The writing proceedure is implemented this way so that the gui doesn't take the performance hit of the IO. */
    private class WriterThread
	extends Thread {
	/** When true was are being told to assume the contents of the queue are now static, and once completed the queue will be empty (and not expecting any new jobs). Used to finish the queue before the GLI exits. */
	private boolean empty_queue;
	/** Setting this flag to true tells the writer to stop regardless of whether there are items left on the queue. */
	private boolean exit;
	/** When the writer thread is busy writing content to the file this flag is set to true. */
	private boolean running;
	/** The fragment queue as a Vector (for thread reasons). */
	private Vector queue;
	/** Construct a new writer thread with an initially empty queue. */
	public WriterThread() {
	    super("WriterThread");
	    empty_queue = false;
	    exit = false;
	    queue = new Vector();
	}

	/** This method is called to ask the writer thread to die after it finishes any current write proceceedure. */
	public synchronized void exit() {
	    //print("WriterThread.exit() start");
	    exit = true;
	    notify();
	    //print("WriterThread.exit() complete");
	}

	/** When this returns there are no jobs waiting in the queue. */
	public void finish() {
	    if(!queue.isEmpty() && running) {
		///ystem.err.println("Somethings gone wrong, as there are still " + queue.size() + " items to be written out and yet the thread seems to be waiting.");
		empty_queue = true;
		run();
	    }
	}

	/** Determine if the writer is currently running.
	 * @return true if it is, false if it has been exited or if it was never running in the first place (an old log)
	 */
	public boolean isStillWriting() {
	    return running;
	}

	/** The run method of this thread checks if there are any document elements queued to be written to file, and then writes them as necessary.
	 * @see org.greenstone.gatherer.util.AppendLineOnlyFileDocument
	 * @see org.greenstone.gatherer.util.AppendLineOnlyFileDocument.AppendLineOnlyFileDocumentElement
	 */
	public void run() {
	    exit = false;
	    running = true;
	    while(!exit) {
		if(!queue.isEmpty()) { 
		    AppendLineOnlyFileDocumentElement element = (AppendLineOnlyFileDocumentElement) queue.remove(0);
		    // Write the content to file.
		    String content = element.getContent();
		    if(content != null) {
			try {
			    write(element.getStartOffset(), element.getEndOffset(), content, content.getBytes("UTF-8").length);
			}
			catch(Exception error) {
			    DebugStream.printStackTrace(error);
			}
			element.clearContent();
		    }
		}
		else if(empty_queue) {
		    exit = true;
		}
		else {
		    synchronized(this) {
			try {
			    print("WriterThread.wait() start");
			    wait();
			}
			catch(Exception exception) {
			}
			print("WriterThread.wait() complete");
		   }
		}
	    }
	    print("WriterThread completely finished. Exiting.");
	    running = false;
	    if(owner != null) {
		owner.remove(AppendLineOnlyFileDocument.this);
	    }
	}

	/** Enqueue a document element to have its text content written to file.
	 * @param element the Element needing its content written out
	 * @see org.greenstone.gatherer.util.AppendLineOnlyFileDocument.AppendLineOnlyFileDocumentElement#getContent
	 */
	public synchronized void queue(Element element) {
	    print("WriterThread.queue(): " + ((AppendLineOnlyFileDocumentElement)element).getContent());
	    queue.add(element);
	    notify();
	}
    }
    // WriterThread

    // ***** METHODS WE ARE NOW IGNORING BECAUSE WE ARE VIRTUALLY READ-ONLY *****
    
    /** Not implemented. Add a new undo listener to this document.
     * @param listener UndoableEditListener
     */
    public void addUndoableEditListener(UndoableEditListener listener) {}
    	
    /** Not implemented. Get the last position in the document.
     * @return null
     */
    public Position getEndPosition() { return null; }

    /** Not implemented. Gets all root elements defined.
     * @return null
     */
    public Element[] getRootElements() { return null; }
	
    /** Not implemented. Retrieve the first position in the document.
     * @return null
     */
    public Position getStartPosition() { return null; }
    
    /** Not implemented. Insert a text fragment into our document.
     * @param offset int
     * @param str String
     * @param a AttributeSet
     */
    public void insertString(int offset, String str, AttributeSet a) {}

    /** Not implemented. Removes some content from the document. 
     * @param offs int
     * @param len int
     */
    public void remove(int offs, int len) {}

    /** Not implemented. Removes an undo listener. 
     * @param listener UndoableEditListener
     */
    public void removeUndoableEditListener(UndoableEditListener listener) {}
    
    /** Not implemented. Renders a runnable apparently - whatever that means. 
     * @param r Runnable
     */
    public void render(Runnable r) {}

    // DEBUG ONLY

    /** Print a message to the GLI debug file stream.
     * @param message the message to be written as a String
     */
    static synchronized public void print(String message) {
	if (message.endsWith("\n")) {
	    DebugStream.print(message); 
	}
	else {
	    DebugStream.println(message); 
	}
    }

    /** Create a test document with the ability to add new lines of text as necessary.
     * @param args the initial starting arguments as a String array
     * @see org.greenstone.gatherer.gui.GLIButton
     * @see org.greenstone.gatherer.util.AppendLineOnlyFileDocument
     * @see org.greenstone.gatherer.util.AppendLineOnlyFileDocument.ReadButtonListener
     */
    static public void main(String[] args) {
	JFrame frame = new JFrame("AppendLineOnlyFileDocument Test");
	frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	frame.setSize(640,480);
	JPanel content = (JPanel) frame.getContentPane();
	
	//PlainDocument document = new PlainDocument();
	//document.setAsynchronousLoadPriority(-1);
	final AppendLineOnlyFileDocument document = new AppendLineOnlyFileDocument("temp.txt");

	final JTextArea text_area = new JTextArea(document);

	JButton read_button = new GLIButton("Read Huge File");
	read_button.addActionListener(new ReadButtonListener(document));
	content.setLayout(new BorderLayout());
	content.add(new JScrollPane(text_area), BorderLayout.CENTER);
	content.add(read_button, BorderLayout.SOUTH);

	frame.setVisible(true);
    }

    /** Listens for actions on the read button, and if detected creates a new ReadTask to test the document. 
     * @author John Thompson, Greenstone Project, New Zealand Digital Library, University of Waikato
     * @version 2.41 final
     */
    static private class ReadButtonListener
	implements ActionListener {
	/** The AppendLineOnlyFileDocument object we are testing. */
	private AppendLineOnlyFileDocument document;

	/** All the constructor does is take a copy of the document to test.
	 * @param document the Document
	 */
	public ReadButtonListener(AppendLineOnlyFileDocument document) {
	    this.document = document;
	}

	/** When the button is clicked this method is called to create the new task and run it.
	 * @param event an ActionEvent containing further information about the button click
	 * @see org.greenstone.gatherer.util.AppendLineOnlyFileDocument.ReadTask
	 */
	public void actionPerformed(ActionEvent event) {
	    Thread task = new ReadTask(document);
	    task.start();
	}
    }

    /** This threaded task opens a large document, aptly named 'big.txt', and then bombards the document object we are testing with lines from the file. This file should be several megs (such as Alice or TREC) to fully test functionality, thread conditions etc. 
     * @author John Thompson, Greenstone Project, New Zealand Digital Library, University of Waikato
     * @version 2.41 final
     */ 
    static private class ReadTask
	extends Thread {
	/** The AppendLineOnlyFileDocument object we are testing. */
	private AppendLineOnlyFileDocument document;

	/** Construct a new task which will perform testing on the given document.
	 * @param document the AppendLineOnlyFileDocument to append lines to
	 */
	public ReadTask(AppendLineOnlyFileDocument document) {
	    super("LoadHugeFileThread");
	    this.document = document;
	}

	/** The runable part of this class opens the file, then reads it in a line at a time, appending these lines to the ever growing document. */
	public void run() {
	    // Load the specified document
	    try {
		BufferedReader in = new BufferedReader(new FileReader(new File("big.txt")));
		String line;
		
		while ((line = in.readLine()) != null) {
		    document.appendLine(line);
		    try {
			// Wait a random ammount of time.
			synchronized(this) {
			    //print("LoadHugeFileThread.wait() start");
			    wait(100);
			    //print("LoadHugeFileThread.wait() complete");
			}
		    }
		    catch(Exception error) {
			error.printStackTrace();
		    }
		}
		in.close();
	    }
	    catch (Exception error) {
		error.printStackTrace();
	    }
	}
    }
    // ReadTask
}
