/* SimpleDocumentEntry.java
 *
 * created: Tue Feb 15 2000
 *
 * This file is part of Artemis
 *
 * Copyright(C) 2000  Genome Research Limited
 *
 * 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., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 * $Header: /cvsroot/pathsoft/artemis/uk/ac/sanger/artemis/io/SimpleDocumentEntry.java,v 1.1 2004/06/09 09:50:28 tjc Exp $
 */

package uk.ac.sanger.artemis.io;

import uk.ac.sanger.artemis.util.*;

import java.io.*;
import java.util.Date;
import java.util.Vector;

/**
 *  This class contains the methods common to all DocumentEntry objects.
 *
 *  @author Kim Rutherford <kmr@sanger.ac.uk>
 *  @version $Id: SimpleDocumentEntry.java,v 1.1 2004/06/09 09:50:28 tjc Exp $
 **/

abstract public class SimpleDocumentEntry
                       implements DocumentEntry 
{

  /**
   *  A vector of ReadOnlyEmblStreamFeature objects - one for each fasta
   *  record in the DocumentEntry.  These won't be written when the
   *  DocumentEntry is written.
   **/
  private FeatureVector fake_fasta_features = new FeatureVector();
 
  /** EntryInformation object that was passed to constructor */
  final private EntryInformation entry_information;
                                                                                                     
  /** collection to send ReadEvents to */
  private Vector listeners = new Vector();

  /** 
   *  The Document object that was passed to the constructor.  This should be
   *  the document that this SimpleDocumentEntry was read from.
   **/
  private Document document = null;
                                                                                                               
  /**
   *  This contains all the lines(stored as LineGroup objects) from the entry
   *  stream that was passed to the constructor.
   **/
  protected LineGroupVector line_groups = new LineGroupVector();
                                                                                                               
  /**
   *  The DocumentEntryAutosaveThread that is started when the first call is
   *  made to setDirtyFlag().
   **/
  private Thread autosave_thread = null;
                                                                                                               
  /**
   *  The Date when this Entry last changed or null if this Entry
   *  hasn't changed since the last save.  Set to null by save().
   **/
  private java.util.Date last_change_time = null;
                                                                                                               
  /**
   *  Set to true in the constructor while features are added.  setDirtyFlag()
   *  will do nothing while this is true.
   **/
  private boolean in_constructor = false;

  /**
   *  Create a new SimpleDocumentEntry from the given Document.
   *  @param entry_information The EntryInformation object of the new Entry.
   *  @param document This is the file that we will read from.  This is also
   *    used for saving the entry back to the file it came from and to give
   *    the new object a name.
   *  @exception IOException thrown if there is a problem reading the entry -
   *    most likely ReadFormatException.
   *  @exception EntryInformationException Thrown if force is false and if this
   *    Entry cannot contain the Key, Qualifier or Key/Qualifier combination of
   *    one of the features in the given Entry.
   **/
  public SimpleDocumentEntry(final EntryInformation entry_information,
                              final Document document,
                              final ReadListener read_listener)
      throws IOException, EntryInformationException 
  {
    this.document = document;
    this.entry_information = new SimpleEntryInformation(entry_information);
    this.in_constructor    = true;  // flag used by setDirtyFlag()

    addReadListener(read_listener);

//tjc  final Reader in_file = getDocument().getReader();

    final LinePushBackReader pushback_reader =
              getDocument().getLinePushBackReader();

    LineGroup new_line_group;

    while((new_line_group =
            LineGroup.readNextLineGroup(pushback_reader)) != null) 
    {
      if(new_line_group instanceof SimpleDocumentFeature) 
      {
        final SimpleDocumentFeature new_feature =
                   (SimpleDocumentFeature)new_line_group;

        // try several times because adding the Feature may cause more than
        // one exception
        int i;
        final int MAX_LOOP = 9999;
        EntryInformationException saved_error = null;

        for(i = 0; i<MAX_LOOP; ++i) 
        {
          try 
          {
            addInternal(new_feature, true);
            break;
          } 
          catch(EntryInformationException e) 
          {
            getEntryInformation().fixException(e);
            saved_error = e;
          }
        }

        if(i == MAX_LOOP) 
          throw new Error("internal error - too many exceptions: " +
                           saved_error.getMessage());
      }
      else 
        addLineGroup(new_line_group);
    }

    // we added some features above hence:
    last_change_time = null;

    final Sequence sequence = getSequence();

    if(sequence != null && sequence instanceof FastaStreamSequence) 
    {
      // add a feature for each FASTA record if there are more 
      // than one record in the FASTA sequence
      final FastaStreamSequence fasta_sequence =
                                (FastaStreamSequence)sequence;

      final String[] header_strings = fasta_sequence.getFastaHeaderStrings();

      if(header_strings.length > 1) 
      {
        final int[] header_positions =
                      fasta_sequence.getFastaHeaderPositions();

        final FeatureTable feature_table = getFeatureTable();

        for(int i = 0 ; i < header_strings.length ; ++i) 
        {
          try
          {
            final Range new_range;

            if(i == header_strings.length - 1) 
            {
              if(header_positions[i] == fasta_sequence.length()) 
                throw new ReadFormatException("empty FASTA record: >" +
                                               header_strings[i]);

              new_range = new Range(header_positions[i] + 1,
                                     fasta_sequence.length());
            }  
            else
            {
              if(header_positions[i] == header_positions[i+1]) 
                throw new ReadFormatException("empty FASTA record: >" +
                                               header_strings[i]);

              new_range = new Range(header_positions[i] + 1,
                                     header_positions[i+1]);
            }

            final QualifierVector qualifiers = new QualifierVector();

            qualifiers.setQualifier(new Qualifier("note",
                                                    header_strings[i]));
            qualifiers.setQualifier(new Qualifier("label",
                                                    header_strings[i]));
            if(i % 2 == 0) 
              qualifiers.setQualifier(new Qualifier("colour", "10"));
            else 
              qualifiers.setQualifier(new Qualifier("colour", "11"));

            final ReadOnlyEmblStreamFeature new_feature =
              new ReadOnlyEmblStreamFeature(new Key("fasta_record"),
                                             new Location(new_range),
                                             qualifiers);

            fake_fasta_features.add(new_feature);
          }
          catch(InvalidRelationException e) 
          {
            throw new Error("internal error - unexpected exception: " + e);
          }
          catch(OutOfRangeException e)
          {
            throw new Error("internal error - unexpected exception: " + e);
          }
        }

        addFakeFeatures();
      }
    }

    this.in_constructor = false;
  }

  /**
   *  Create a new SimpleDocumentEntry with no Document associated with it.
   *  @param entry_information The EntryInformation object of the new Entry.
   **/
  public SimpleDocumentEntry(final EntryInformation entry_information)
  {
    this.entry_information = new SimpleEntryInformation(entry_information);
  }

  /**
   *  Create a new SimpleDocumentEntry that will be a copy of the given Entry
   *  and has no Document associated with it.  The new SimpleDocumentEntry
   *  cannot be saved to a file with save() unless save(Document) has been
   *  called first.  Some qualifier and location information will be lost if
   *  the argument Entry is a different type to this class.
   *  @param entry_information The EntryInformation object of the Entry that
   *    will contain this Feature.
   *  @param force If true then invalid qualifiers and any features with
   *    invalid keys in the new Entry will be quietly thrown away.  "Invalid"
   *    means that the key/qualifier is not allowed to occur in an Entry of
   *    this type(probably determined by the EntryInformation object of this
   *    Entry).  If false an EntryInformationException will be thrown for
   *    invalid keys or qualifiers.
   **/
  public SimpleDocumentEntry(final EntryInformation entry_information,
                              final Entry new_entry, final boolean force)
      throws EntryInformationException
  {
    this.entry_information = new SimpleEntryInformation(entry_information);

    if(new_entry.getClass().equals(this.getClass())) 
    {
      try
      {
        setHeaderText(new_entry.getHeaderText());
      } 
      catch(IOException e) 
      {
        System.err.println(e);
        // if it doesn't work just ignore it
      }
    }

    final FeatureEnumeration feature_enum = new_entry.features();

    while(feature_enum.hasMoreFeatures()) 
    {
      final Feature new_feature = feature_enum.nextFeature();

      try
      {
        if(force) 
          forcedAdd(makeNativeFeature(new_feature, true));
        else 
          add(makeNativeFeature(new_feature, true));
      } 
      catch(ReadOnlyException e) 
      {
        throw new Error("internal error - unexpected exception: " + e);
      }
    }

    final Sequence new_sequence = new_entry.getSequence();

    if(new_sequence != null) 
      setSequence(makeNativeSequence(new_sequence));
  }

  /**
   *  Return the text of the header of this Entry or null if there is no
   *  header.
   **/
  public String getHeaderText() 
  {
    final StringBuffer buffer = new StringBuffer();

    for(int i = 0 ; i < line_groups.size() ; ++i) 
    {
      final LineGroup current_line_group = line_groups.elementAt(i);

      if(!(current_line_group instanceof FeatureTable) &&
         !(current_line_group instanceof Sequence)) 
        buffer.append(current_line_group.toString());
    }

    if(buffer.length() > 0) 
      return buffer.toString();
    else 
      return null;
  }

  /**
   *  Set the header of this Entry to be the given text.
   *  @return true if and only if the header was successfully set.  Not all
   *    Entry objects can change their header, so it is up to the calling
   *    function to check the return value.  If null the current header will be
   *    removed.
   *  @exception IOException thrown if there is a problem reading the header
   *    from the String - most likely ReadFormatException.
   **/
  public boolean setHeaderText(final String new_header)
      throws IOException 
  {

    final LineGroupVector new_line_groups = new LineGroupVector();

    if(new_header != null) 
    {
      final Reader reader = new StringReader(new_header);

      final LinePushBackReader pushback_reader =
                          new LinePushBackReader(reader);

      LineGroup new_line_group;

      try 
      {
        while((new_line_group =
                LineGroup.readNextLineGroup(pushback_reader)) != null)
        {
          if(new_line_group instanceof MiscLineGroup) 
            new_line_groups.addElement(new_line_group);
          else
            throw new ReadFormatException("the header must contain only " +
                                           "header lines");
        }
      } 
      catch(InvalidRelationException e) 
      {
        throw new ReadFormatException("the header must contain only " +
                                       "header lines");
      }
    }

    // now remove all the EmblMisc and GenbankMisc LineGroup objects from
    // this Entry
    for(int i = line_groups.size() - 1 ; i >= 0  ; --i) 
    {
      final LineGroup current_line_group = line_groups.elementAt(i);

      if(current_line_group instanceof MiscLineGroup) 
        line_groups.removeElementAt(i);
    }

    if(new_header != null) 
    {
      // then add the new LineGroup objects
      for(int i = 0 ; i < new_line_groups.size() ; ++i) 
        line_groups.insertElementAt(new_line_groups.elementAt(i), i);
    }

    setDirtyFlag();

    return true;
  }

  /**
   *  Write this Entry to the given stream.
   *  @param writer The stream to write to.
   *  @exception IOException thrown if there is a problem writing the entry.
   **/
  public void writeToStream(final Writer writer)
      throws IOException 
  {
    removeFakeFeatures();

    try
    {
      for(int i = 0 ; i < line_groups.size() ; ++i) 
      {
        final LineGroup current_line_group = line_groups.elementAt(i);
        current_line_group.writeToStream(writer);
      }

      if(line_groups.size() == 1) 
      {
        // don't write out the "//" end of entry marker if we have only one
        // LineGroup - this makes life easier for external programs that need
        // to read the feature table or sequence
        return;
      }
      else
      {
        if(line_groups.size() == 2) 
        {
          final LineGroup second_line_group = line_groups.elementAt(1);

          // don't write out the "//" end of entry marker if this is raw or
          // FASTA sequence
          if(second_line_group instanceof RawStreamSequence ||
              second_line_group instanceof FastaStreamSequence) 
            return;
        }
      }

      if(this instanceof PublicDBDocumentEntry) 
        LineGroup.writeEndOfEMBLEntry(writer);
    } 
    finally
    {
      addFakeFeatures();
    }
  }

  /**
   *  Return a count of the number of Feature objects in this Entry.
   **/
  public int getFeatureCount() 
  {
    final FeatureTable feature_table = findFeatureTable();

    if(feature_table == null) 
      return 0;
    else 
      return feature_table.getFeatureCount();
  }

  /**
   *  The method is identical to add() except that it doesn't check the
   *  read_only flag before adding and it doesn't throw ReadOnlyExceptions.
   *  This is used by the constructor to avoid exceptions.
   *
   *  Add the given Feature to this Entry.  If the Feature is already in an
   *  Entry then Entry.remove() should be called on that Entry before calling
   *  Entry.add().  An Error will be thrown otherwise.
   *  @param throw_entry_info_exceptions if true throw
   *    EntryInformationExceptions, otherwise just send a ReadEvent.
   *  @exception EntryInformationException Thrown if this Entry
   *    cannot contain the Key, Qualifier or Key/Qualifier combination of the
   *    given Feature or if a required qualifier is missing.
   *  @return A reference that was passed to add(), if that Feature can be
   *    stored directly in this Entry, otherwise returns a reference to a new
   *    Feature, that is a copy of the argument.  The argument reference
   *    should not be used after the call to add(), unless the return
   *    reference happens to be the same as the argument.
   **/
  private Feature addInternal(final Feature feature,
                               final boolean throw_entry_info_exceptions)
      throws EntryInformationException 
  {
    if(feature.getEntry() != null) 
      throw new Error("internal error - a feature must have one owner");

    final EntryInformation entry_information = getEntryInformation();

    if(!entry_information.isValidKey(feature.getKey()))  
    {
      final String message = feature.getKey() + " is not a valid key";

      fireEvent(new ReadEvent(this, message));

      throw new InvalidKeyException(feature.getKey() + " is not a valid " +
                                     "key for this entry", feature.getKey());
    }

    final QualifierVector new_qualifiers = feature.getQualifiers();

    final Key new_key = feature.getKey();

    // check the qualifiers
    for(int i = 0 ; i < new_qualifiers.size() ; ++i) 
    {
      final Qualifier this_qualifier = new_qualifiers.elementAt(i);
      final String this_qualifier_name = this_qualifier.getName();

      if(!entry_information.isValidQualifier(new_key, this_qualifier_name)) 
      {
        final String message = new_key + " can't have " + this_qualifier_name 
                                       + " as a qualifier";

        fireEvent(new ReadEvent(this, message));

        if(throw_entry_info_exceptions) 
          throw new InvalidRelationException(message, new_key,
                                             this_qualifier);
      }
    }

    final SimpleDocumentFeature native_feature =
      makeNativeFeature(feature, false);

    final FeatureTable feature_table = getFeatureTable();
    feature_table.add(native_feature);

    try 
    {
      native_feature.setDocumentEntry(this);
    }
    catch(ReadOnlyException e) 
    {
      // makeNativeFeature() should never return a read only feature
      throw new Error("internal error - unexpected exception: " + e);
    }

    setDirtyFlag();

    return native_feature;
  }

  /**
   *  Add the given Feature to this Entry.  If the Feature is already in an
   *  Entry then Entry.remove() should be called on that Entry before calling
   *  Entry.add().  An Error will be thrown otherwise.
   *  @exception ReadOnlyException If this entry is read only.
   *  @exception EntryInformationException Thrown if this Entry
   *    cannot contain the Key, Qualifier or Key/Qualifier combination of the
   *    given Feature or if a required qualifier is missing.
   *  @return A reference that was passed to add(), if that Feature can be
   *    stored directly in this Entry, otherwise returns a reference to a new
   *    Feature, that is a copy of the argument.  The argument reference
   *    should not be used after the call to add(), unless the return
   *    reference happens to be the same as the argument.
   **/
  public Feature add(final Feature feature)
      throws EntryInformationException, ReadOnlyException 
  {
    if(isReadOnly()) 
      throw new ReadOnlyException();

    return addInternal(feature, true);
  }

  /**
   *  Add the given Feature to this Entry.  If the Feature is already in an
   *  Entry then Entry.remove() should be called on that Entry before calling
   *  Entry.forcedAdd()(An Error will be thrown otherwise).  Invalid
   *  qualifiers will be quietly thrown away.  Features with invalid keys will
   *  not be added(and null will be returned).  "Invalid" means that the
   *  key/qualifier is non allowed to occur in an Entry of this type(probably
   *  determined by the EntryInformation object of this Entry).  Any
   *  qualifiers that are required for this Entry will be quietly added(with
   *  a zero-length string as the value).
   *  @exception ReadOnlyException If this entry is read only.
   *  @return A reference that was passed to add(), if that Feature can be
   *    stored directly in this Entry, otherwise returns a reference to a new
   *    Feature, that is a copy of the argument.  The argument reference
   *    should not be used after the call to add(), unless the return
   *    reference happens to be the same as the argument.  Returns null if and
   *    only if the new Feature has a key that is invalid for this Entry.
   **/
  public Feature forcedAdd(final Feature feature)
      throws ReadOnlyException 
  {
    if(isReadOnly()) 
      throw new ReadOnlyException();

    if(feature.getEntry() != null) 
      throw new Error("internal error - a feature must have one owner");

    final EntryInformation entry_information = getEntryInformation();

    if(!entry_information.isValidKey(feature.getKey())) 
      return null;

    final QualifierVector feature_qualifiers = feature.getQualifiers();
    final QualifierVector fixed_qualifiers = new QualifierVector();
    final Key new_key = feature.getKey();

    // set to true if there is an invalid qualifier
    boolean qualifiers_fixed = false;

    // check the qualifiers
    for(int i = 0 ; i < feature_qualifiers.size() ; ++i)
    {
      final Qualifier this_qualifier = feature_qualifiers.elementAt(i);

      final String this_qualifier_name = this_qualifier.getName();

      if(entry_information.isValidQualifier(new_key, this_qualifier_name)) 
        fixed_qualifiers.setQualifier(this_qualifier);
      else 
        qualifiers_fixed = true;
    }

    final SimpleDocumentFeature native_feature =
      makeNativeFeature(feature, false);

    if(qualifiers_fixed) 
    {
      try
      {
        native_feature.setQualifiers(fixed_qualifiers);
      }
      catch(EntryInformationException e) 
      {
        throw new Error("internal error - unexpected exception: " + e);
      }
    } 
    else
    {
      // otherwise use the feature as is
    }

    final FeatureTable feature_table = getFeatureTable();
    feature_table.add(native_feature);
    native_feature.setDocumentEntry(this);
    setDirtyFlag();

    return native_feature;
  }

  /**
   *  The method is identical to remove() except that it doesn't check the
   *  read_only flag before removing and it doesn't throw ReadOnlyExceptions.
   *
   *  Remove the given Feature from this Entry.
   *  @return true if and only if the Feature was in this Entry.
   **/
  protected boolean removeInternal(Feature feature) 
  {
    final FeatureTable feature_table = findFeatureTable();

    if(feature_table == null) 
      return false;
    else 
    {
      final SimpleDocumentFeature feature_from_table =
       (SimpleDocumentFeature) feature_table.remove(feature);

      if(feature_from_table == null) 
        return false;
      else  
      {
        try 
        {
          feature_from_table.setDocumentEntry(null);
        } 
        catch(ReadOnlyException e) 
        {
          throw new Error("internal error - unexpected exception: " + e);
        }

        // get rid of the feature table
        if(feature_table.getFeatureCount() == 0) 
          removeLineGroup(feature_table);

        setDirtyFlag();

        return true;
      }
    }
  }

  /**
   *  Remove the given Feature from this Entry.
   *  @return true if and only if the Feature was in this Entry.
   **/
  public boolean remove(Feature feature)
      throws ReadOnlyException 
  {
    if(isReadOnly() || feature.isReadOnly()) 
      throw new ReadOnlyException();

    return removeInternal(feature);
  }

  /**
   *  Return the ith Feature from this Entry.  This Features are returned in a
   *  consistent order, sorted by the first base of each Feature.
   **/
  public Feature getFeatureAtIndex(int i) 
  {
    final FeatureTable feature_table = findFeatureTable();

    if(feature_table == null) 
      return null;
    else 
      return feature_table.getFeatureAtIndex(i);
  }

  /**
   *  Return the index of the given Feature.  This does the reverse of
   *  getFeatureAtIndex().
   **/
  public int indexOf(final Feature feature) 
  {
    final FeatureTable feature_table = findFeatureTable();

    if(feature_table == null) 
      return -1;
    else 
      return feature_table.indexOf(feature);
  }

  /**
   *  Returns true if and only if this Entry contains the given feature.
   **/
  public boolean contains(final Feature feature) 
  {
    final FeatureTable feature_table = findFeatureTable();

    if(feature_table == null) 
      return false;
    else
      return feature_table.contains(feature);
  }

  /**
   *  Create a new Feature object of an appropriate type in this Entry.
   *  @param key The new feature key
   *  @param location The Location object for the new feature
   *  @param qualifiers The qualifiers for the new feature(can be null if
   *    there are no qualifiers).
   **/
  public Feature createFeature(final Key key,
                                final Location location,
                                final QualifierVector qualifiers)
      throws EntryInformationException, ReadOnlyException,
             OutOfRangeException 
  {
    if(isReadOnly())
      throw new ReadOnlyException();

    final Feature new_feature;

    if(this instanceof EmblDocumentEntry) 
      new_feature = new EmblStreamFeature(key, location, qualifiers);
    else
      new_feature = new GenbankStreamFeature(key, location, qualifiers);

    add(new_feature);
    setDirtyFlag();

    return new_feature;
  }

  /**
   *  Return a vector containing the references of the Feature objects within
   *  the given range.
   *  @param range Return features that overlap this range - ie the start of
   *    the feature is less than or equal to the end of the range and the end
   *    of the feature is greater than or equal to the start of the range.
   *  @return The features of this feature table the are within
   *    the given range.  The returned object is a copy - changes will not
   *    effect the FeatureTable object itself.
   **/
  public FeatureVector getFeaturesInRange(Range range) 
  {
    final FeatureTable feature_table = findFeatureTable();

    if(feature_table == null) 
      return new FeatureVector();
    else 
      return feature_table.getFeaturesInRange(range);
  }

  /**
   *  Return a vector containing the references of all the Feature objects in
   *  this Entry.
   *  @return The features of this Entry.  The returned object
   *    is a copy - changes will not effect the Entry object itself.
   **/
  public FeatureVector getAllFeatures() 
  {
    final FeatureTable feature_table = findFeatureTable();

    if(feature_table == null) 
      return new FeatureVector();
    else 
      return feature_table.getAllFeatures();
  }

  /**
   *  Returns an enumeration of the Feature objects in this
   *  SimpleDocumentEntry.  The returned Enumeration object will generate
   *  all features in this object in turn. The first item generated is the
   *  item at index 0, then the item at index 1, and so on.
   **/
  public FeatureEnumeration features() 
  {
    final FeatureTable feature_table = findFeatureTable();

    if(feature_table == null)
    {
      return new FeatureEnumeration() {
        public boolean hasMoreFeatures() {
          return false;
        }

        public Feature nextFeature() {
          return null;
        }
      };
    }
    else 
      return feature_table.features();
  }

  /**
   *  Return the Sequence object from this entry or null if it does not
   *  contain one.
   *  @return a Sequence object for this Entry.  the returned object is
   *    not a copy - changes to it will change the Entry object itself
   **/
  public Sequence getSequence() 
  {
    for(int i = 0 ; i < line_groups.size() ; ++i) 
    {
      final LineGroup current_line_group = line_groups.elementAt(i);

      if(current_line_group instanceof Sequence)
        return(Sequence) current_line_group;
    }

    return null;
  }

  /**
   *  Add a new LineGroup object to this Entry.
   *  @param new_line_group A new LineGroup to add.
   **/
  private void addLineGroup(final LineGroup new_line_group) 
  {
    if(new_line_group instanceof FeatureHeader) 
    {
      // insert FH lines before the Sequence and Features(if any)
      for(int i = 0 ; i < line_groups.size() ; ++i) 
      {
        final LineGroup this_line_group = line_groups.elementAt(i);

        if(this_line_group instanceof Feature ||
            this_line_group instanceof FeatureTable ||
            this_line_group instanceof Sequence) 
        {
          line_groups.insertElementAt(new_line_group, i);
          return;
        }
      }
    } 
    else
    {
      if(new_line_group instanceof Feature)
      {
        // Features before the Sequence and FeatureTable(if any)
        for(int i = 0 ; i < line_groups.size() ; ++i) 
        {
          final LineGroup this_line_group = line_groups.elementAt(i);

          if(this_line_group instanceof FeatureTable ||
              this_line_group instanceof Sequence) 
          {
            line_groups.insertElementAt(new_line_group, i);
            return;
          }
        }
      } 
      else
      {
        if(!(new_line_group instanceof Sequence) &&
            !(new_line_group instanceof FeatureTable)) 
        {
          // insert before features and sequence
          for(int i = 0 ; i < line_groups.size() ; ++i) 
          {
            final LineGroup this_line_group = line_groups.elementAt(i);

            if(this_line_group instanceof Feature ||
                this_line_group instanceof FeatureTable ||
                this_line_group instanceof FeatureHeader ||
                this_line_group instanceof Sequence)
            {
              line_groups.insertElementAt(new_line_group, i);
              return;
            }
          }
        }
        else
        {
          // fall throw
        }
      }
    }

    // otherwise add at end
    line_groups.addElement(new_line_group);
  }

  /**
   *  Return the name of this Entry or null if it has no name.
   **/
  public String getName() 
  {
    if(getDocument() == null || getDocument().getName() == null) 
      return null;
    else 
      return getDocument().getName();
  }

  /**
   *  Set the name of this Entry - if possible(the return value will let the
   *  caller know).
   *  @return true if and only if the name was successfully set.  Not all
   *    Entry objects can change there name, so it is up to the calling
   *    function to check the return value.
   **/
  public boolean setName(final String name) 
  {
    if(name == null || name.length() == 0) 
      return false;

    setDocument(new FileDocument(new File(name)));

    return true;
  }

  /**
   *  Return the EntryInformation object for this Entry.
   **/
  public EntryInformation getEntryInformation() 
  {
    return entry_information;
  }

  /**
   *  Create and return a new FeatureTable().  The new FeatureTable will be
   *  added to line_groups in the appropriate place.
   **/
  private FeatureTable createFeatureTable() 
  {
    final FeatureTable new_feature_table;

    new_feature_table = new StreamFeatureTable();

    // put feature table just before SEQUENCE LineGroup
    if(line_groups.size() > 0 &&           
        line_groups.lastElement() instanceof Sequence) 
      line_groups.insertElementAt(new_feature_table,
                                   line_groups.size() - 1);
    else  // no SEQUENCE so add the feature table at the end
      line_groups.insertElementAt(new_feature_table, line_groups.size());

    return new_feature_table;
  }

  /**
   *  Return the FeatureTable of this DocumentEntry or null if there isn't one
   *  yet.
   **/
  private FeatureTable findFeatureTable() 
  {
    for(int i = 0 ; i < line_groups.size() ; ++i) 
    {
      final LineGroup current_line_group = line_groups.elementAt(i);

      if(current_line_group instanceof FeatureTable) 
        return(FeatureTable) current_line_group;
    }

    return null;
  }


  /**
   *  Return the FeatureTable object from this entry(which will be created if
   *  this object does not already contain one).  This method has package
   *  scope because other package should add, remove and query Features in the
   *  FeatureTable indirectly(through the Entry class).
   *  @return a FeatureTable object for this Entry.  the returned object is
   *    not a copy - changes to it will change the Entry object itself
   **/
  FeatureTable getFeatureTable() 
  {
    final FeatureTable found_feature_table = findFeatureTable();

    if(found_feature_table == null) 
      return createFeatureTable();
    else
      return found_feature_table;
  }

  /**
   *  Remove the given LineGroup from this SimpleDocumentEntry.
   **/
  private void removeLineGroup(final LineGroup line_group) 
  {
    for(int i = 0 ; i < line_groups.size() ; ++i) 
    {
      final LineGroup current_line_group = line_groups.elementAt(i);

      if(current_line_group == line_group) 
      {
        line_groups.removeElementAt(i);
        setDirtyFlag();
        return;
      }
    }
  }

  /**
   *  Return the File reference that was passed to the constructor or null if
   *  none was passed.
   **/
  public Document getDocument() 
  {
    return document;
  }


  /**
   *  Set the document to use when saving this DocumentEntry.
   **/
  public void setDocument(final Document document) 
  {
    this.document = document;
  }


  /**
   *  Write this Entry to the Document it came from.  This method uses the
   *  current Document reference(as given by getDocument()) and will throw a
   *  NullPointerException if the is no current Document ie. if getDocument()
   *  returns null.  Use save(Document) to save the current Entry
   *  to a different Document.
   *  @exception IOException thrown if there is an IO problem saving the entry.
   **/
  public void save()
      throws IOException
  {
    save(getDocument());
  }


  /**
   *  Write this Entry to the given Document.
   *  @param document This is the file that we will write to.
   *  @exception IOException thrown if there is an IO problem saving the entry.
   **/
  public void save(final Document document)
      throws IOException 
  {
    final Writer out_file = document.getWriter();
    writeToStream(out_file);
    out_file.close();

    last_change_time = null;
  }


  /**
   *  Returns true if and only if this entry is read only.
   **/
  public boolean isReadOnly() 
  {
    return false;
  }


  /**
   *  Returns true if and only if there have been some changes to this Entry
   *  since the last save.
   **/
  public boolean hasUnsavedChanges() 
  {
    return last_change_time != null;
  }


  /**
   *  Set last_change_time so that hasUnsavedChanges() will return true.
   *  last_change_time can be read with getLastChangeTime().
   **/
  public void setDirtyFlag() 
  {
    if(in_constructor) 
    {
      // features are being added, but the entry hasn't really changed
    }
    else
    {
//  XXX FIXME - disabled for now because the code is suspect

//       if(autosave_thread == null && getName() != null) {
//         // this is the first change so start autosaving
//         autosave_thread = new DocumentEntryAutosaveThread(this);

//         autosave_thread.start();
//       }


      final java.util.Calendar calendar = java.util.Calendar.getInstance();

      last_change_time = calendar.getTime();
    }
  }


  /**
   *  Return the Date when this Entry last changed.
   **/
  public Date getLastChangeTime()
  {
    return last_change_time;
  }


  /**
   *  Add the given Sequence object to this Entry.
   **/
  public void setSequence(final StreamSequence sequence) 
  {
    final Sequence old_sequence = getSequence();

    if(old_sequence != null) 
      removeLineGroup((StreamSequence) old_sequence);

    addLineGroup(sequence);
  }


  /**
   *  If the given feature can be added directly to this Entry, then return
   *  it, otherwise create and return a new feature of the appropriate type
   * (eg. return an EmblStreamFeature if this Entry is a EmblDocumentEntry).
   *  The feature should not be in an Entry when this method is called.
   *  @param copy if true then always make a new copy of the Feature.
   **/
  protected abstract SimpleDocumentFeature
    makeNativeFeature(final Feature feature,
                       final boolean copy);

  /**
   *  If the given Sequence can be added directly to this Entry, then return a
   *  copy of it, otherwise create and return a new feature of the appropriate
   *  type for this Entry.
   **/
  protected abstract StreamSequence
    makeNativeSequence(final Sequence sequence);

  /**
   *  Add the elements of fake_fasta_features to the feature table.
   **/
  private void addFakeFeatures() 
  {
    final FeatureTable feature_table = getFeatureTable();

    for(int i = 0 ; i < fake_fasta_features.size() ; ++i) 
      feature_table.add(fake_fasta_features.elementAt(i));
  }

  /**
   *  Remove the elements of fake_fasta_features from the feature table.
   **/
  private void removeFakeFeatures() 
  {
    final FeatureTable feature_table = getFeatureTable();

    for(int i = 0 ; i < fake_fasta_features.size() ; ++i) 
      feature_table.remove(fake_fasta_features.elementAt(i));
  }

  /**
   *  Add an object that will receive ReadEvents.
   **/
  private void addReadListener(final ReadListener listener) 
  {
    listeners.addElement(listener);
  }

  /**
   *  Send the given event to all the listeners.
   **/
  private void fireEvent(final ReadEvent event)
  {
    for(int i = 0 ; i < listeners.size() ; ++i) 
     ((ReadListener)listeners.elementAt(i)).notify(event);
  }

}
