/* Entry.java
 *
 * created: Sun Oct 11 1998
 *
 * This file is part of Artemis
 *
 * Copyright(C) 1998,1999,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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/Entry.java,v 1.11 2008-06-20 10:00:25 tjc Exp $
 */

package uk.ac.sanger.artemis;

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

import uk.ac.sanger.artemis.util.*;
import uk.ac.sanger.artemis.io.DatabaseDocumentEntry;
import uk.ac.sanger.artemis.io.EmblStreamFeature;
import uk.ac.sanger.artemis.io.DocumentEntry;
import uk.ac.sanger.artemis.io.EmblDocumentEntry;
import uk.ac.sanger.artemis.io.GFFDocumentEntry;
import uk.ac.sanger.artemis.io.GFFStreamFeature;
import uk.ac.sanger.artemis.io.IndexedGFFDocumentEntry;
import uk.ac.sanger.artemis.io.PartialSequence;
import uk.ac.sanger.artemis.io.Range;
import uk.ac.sanger.artemis.io.RangeVector;
import uk.ac.sanger.artemis.io.Key;
import uk.ac.sanger.artemis.io.Location;
import uk.ac.sanger.artemis.io.LocationParseException;
import uk.ac.sanger.artemis.io.QualifierVector;
import uk.ac.sanger.artemis.io.DocumentEntryFactory;
import uk.ac.sanger.artemis.io.EntryInformation;
import uk.ac.sanger.artemis.io.EntryInformationException;

import java.util.NoSuchElementException;
import java.util.Vector;
import java.io.IOException;
import java.io.File;


/**
 *  This class is a wrapper for the io.Entry class which contains the state
 *  and handles the events needed for editing one embl entry in Diana.  The
 *  state includes an EMBL.Entry object and a list of EntryChange listeners
 *  and FeatureChange listeners.  Other objects can register as listeners for
 *  changes to the entry.  For that reason, all changes to the embl.Entry
 *  objects should go through this class. (see ChangeEvent for details of the
 *  possible events.)
 *
 *  @author Kim Rutherford
 *  @version $Id: Entry.java,v 1.11 2008-06-20 10:00:25 tjc Exp $
 **/

public class Entry implements FeatureChangeListener, Selectable 
{

  /**
   *  The embl.Entry object that was passed to the constructor
   **/
  private uk.ac.sanger.artemis.io.Entry embl_entry;

  /**
   *  A vector of those objects listening for entry change events.
   **/
  final private Vector<ChangeListener> entry_listener_list = new Vector<ChangeListener>();

  /**
   *  A vector of those objects listening for feature change events.
   **/
  final private Vector<ChangeListener> feature_listener_list = new Vector<ChangeListener>();

  /**
   *  This is the Bases reference that was passed to the constructor.
   **/
  /*final*/ private Bases bases;

  /**
   *  Create a new Entry object.
   *  @param bases The Bases object which contains the Strand objects that will
   *    be used by the features of this Entry.
   *  @param embl_entry a reference to an embl.Entry object containing the
   *    underlying data for new object
   *  @exception OutOfRangeException Thrown if one of the features in
   *    embl_entry is out of range of the Bases object.
   **/
  public Entry(final Bases bases,
               final uk.ac.sanger.artemis.io.Entry embl_entry)
      throws OutOfRangeException 
  {
    this.embl_entry = embl_entry;
    this.bases = bases;

    checkLocations();
    createDianaFeatures();
  }

  /**
   *  Create a new Entry object from a uk.ac.sanger.artemis.io.Entry
   *  object.  A new Bases object will be created for the new Entry.
   *  @param embl_entry a reference to an embl.Entry object containing the
   *    underlying data for new object
   *  @exception OutOfRangeException Thrown if one of the features in
   *    embl_entry is out of range of the Bases object.
   *  @exception NoSequenceException Thrown if embl_entry contains no sequence.
   **/
  public Entry(final uk.ac.sanger.artemis.io.Entry embl_entry)
      throws OutOfRangeException, NoSequenceException 
  {
    this.embl_entry = embl_entry;

    if(embl_entry.getSequence() == null ||
       embl_entry.getSequence().length() == 0) 
      throw new NoSequenceException();
    else 
      this.bases = new Bases(embl_entry.getSequence());

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

  /**
   *  Save the changes to this Entry back to where the Entry came from.  If
   *  this object is read only a ReadOnlyException will be thrown.
   *  @param destination_type Should be one of EMBL_FORMAT, GENBANK_FORMAT or
   *    ANY_FORMAT.  If ANY_FORMAT then the Entry will be saved in the
   *    same format it was created, otherwise it will be saved in the given
   *    format.
   *  @exception EntryInformationException Thrown if the destination type
   *    cannot contain the Key, Qualifier or Key/Qualifier combinations of one
   *    of the features in this Entry.
   *  @exception IOException Thrown if an IO error occurs while saving.  One
   *    possibility is ReadOnlyException.
   **/
  public void save(final int destination_type)
      throws IOException, EntryInformationException 
  {
    if(destination_type == DocumentEntryFactory.ANY_FORMAT) 
      getEMBLEntry().save();
    else 
    {
      if(getEMBLEntry() instanceof DocumentEntry) 
        getEMBLEntryAsNewType(getEntryInformation(),
                              destination_type, false).save();
      else 
        throw new ReadOnlyException("operation cannot be " +
                                    "applied to this entry");
    }
  }

  /**
   *  Save the changes to this Entry back to where the Entry came from(like
   *  save()) but without any uk.ac.sanger.artemis extensions.  If this object is read only
   *  or doesn't support this a ReadOnlyException will be thrown.
   *  @param destination_type Should be one of EMBL_FORMAT, GENBANK_FORMAT or
   *    ANY_FORMAT.  If ANY_FORMAT then the Entry will be saved in the
   *    same format it was created, otherwise it will be saved in the given
   *    format.
   *  @exception EntryInformationException Thrown if the destination type
   *    cannot contain the Key, Qualifier or Key/Qualifier combinations of one
   *    of the features in this Entry.
   **/
  public void saveStandardOnly(final int destination_type)
      throws IOException,EntryInformationException 
  {
    if(destination_type == DocumentEntryFactory.ANY_FORMAT) 
      getEMBLEntry().save();
    else 
    {
      if(getEMBLEntry() instanceof DocumentEntry) 
      {
        final EntryInformation entry_information =
          Options.getDBEntryInformation();

        getEMBLEntryAsNewType(entry_information,
                               destination_type, false).save();
      } 
      else 
        throw new ReadOnlyException("operation cannot be " +
                                    "applied to this entry");
    }
  }

  /**
   *  Save the changes to this Entry to the file.  If this object is read only
   *  a ReadOnlyException will be thrown.  This method only works when the
   *  underlying embl.Entry object is a DocumentEntry.
   *  @param destination_type Should be one of EMBL_FORMAT, GENBANK_FORMAT,
   *    GFF_FORMAT or ANY_FORMAT.  If ANY_FORMAT then the Entry will
   *    be saved in the same format it was created, otherwise it will be saved
   *    in the given format.
   *  @param force If true then invalid qualifiers and any features with
   *    invalid keys will be quietly thrown away when saving.  "Invalid" means
   *    that the key/qualifier is not allowed to occur in the destination type
   *   (probably determined by the default EntryInformation object of the
   *    destination type).  If false an EntryInformationException will be
   *    thrown for invalid keys or qualifiers.
   *  @exception EntryInformationException Thrown if force is false and if the
   *    destination type cannot contain the Key, Qualifier or Key/Qualifier
   *    combination of one of the features in this Entry.
   **/
  public void save(final File file, final int destination_type,
                   final boolean force)
      throws IOException, EntryInformationException 
  {
    final EntryInformation artemis_entry_information;
    
    if((getEMBLEntry() instanceof DatabaseDocumentEntry ||
        getEMBLEntry() instanceof GFFDocumentEntry || 
        getEMBLEntry() instanceof IndexedGFFDocumentEntry) &&
       destination_type == DocumentEntryFactory.EMBL_FORMAT)
      artemis_entry_information = Options.getArtemisEntryInformation();
    else
      artemis_entry_information = getEntryInformation();
    
    save(file, destination_type, force, artemis_entry_information);
  }
  
  
  /**
   *  Save the changes to this Entry to the file.  If this object is read only
   *  a ReadOnlyException will be thrown.  This method only works when the
   *  underlying embl.Entry object is a DocumentEntry.
   *  @param destination_type Should be one of EMBL_FORMAT, GENBANK_FORMAT,
   *    GFF_FORMAT or ANY_FORMAT.  If ANY_FORMAT then the Entry will
   *    be saved in the same format it was created, otherwise it will be saved
   *    in the given format.
   *  @param force If true then invalid qualifiers and any features with
   *    invalid keys will be quietly thrown away when saving.  "Invalid" means
   *    that the key/qualifier is not allowed to occur in the destination type
   *   (probably determined by the default EntryInformation object of the
   *    destination type).  If false an EntryInformationException will be
   *    thrown for invalid keys or qualifiers.
   *  @exception EntryInformationException Thrown if force is false and if the
   *    destination type cannot contain the Key, Qualifier or Key/Qualifier
   *    combination of one of the features in this Entry.
   **/
  public void save(final File file, final int destination_type,
                   final boolean force, final EntryInformation artemis_entry_information)
      throws IOException, EntryInformationException 
  {
    final FileDocument file_document = new FileDocument(file);
    final DocumentEntry document_entry =
      (DocumentEntry)getEMBLEntryAsNewType(artemis_entry_information,
                                           destination_type, force);
    document_entry.save(file_document);
  }

  /**
   *  Save the changes to this Entry to the file(like save(Document)) but
   *  without any uk.ac.sanger.artemis extensions.  If this object is read only a
   *  ReadOnlyException will be thrown.  This method only works when the
   *  underlying embl.Entry object is a DocumentEntry.
   *  @param destination_type Should be one of EMBL_FORMAT, GENBANK_FORMAT or
   *    ANY_FORMAT.  If ANY_FORMAT then the Entry will be saved in the
   *    same format it was created, otherwise it will be saved in the given
   *    format.
   *  @param force If true then invalid qualifiers and any features with
   *    invalid keys will be quietly thrown away when saving.  "Invalid" means
   *    that the key/qualifier is not allowed to occur in the destination type
   *   (probably determined by the default EntryInformation object of the
   *    destination type).  If false an EntryInformationException will be
   *    thrown for invalid keys or qualifiers.
   *  @exception EntryInformationException Thrown if force is false and if the
   *    destination type cannot contain the Key, Qualifier or Key/Qualifier
   *    combination of one of the features in this Entry.
   **/
  public void saveStandardOnly(final File file,
                               final int destination_type,
                               final boolean force)
      throws IOException, EntryInformationException 
  {
    final FileDocument file_document = new FileDocument(file);

    final DocumentEntry document_entry =
     (DocumentEntry) getEMBLEntryAsNewType(Options.getDBEntryInformation(),
                                           destination_type, force);
    document_entry.save(file_document);
  }

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

  /**
   *  Create a new Entry in the given EntryGroup.
   *  @return A reference to the new Entry.
   **/
  public static Entry newEntry(final Bases bases) 
  {
    final uk.ac.sanger.artemis.io.Entry entry =
      new EmblDocumentEntry(Options.getArtemisEntryInformation());

    try 
    {
      return new Entry(bases, entry);
    } 
    catch(OutOfRangeException e) 
    {
      // an empty Entry cannot have any out of range features
      throw new Error("internal error - unexpected exception: " + e);
    }
  }

  /**
   *  Return the name of this Entry or null if it has no name.
   **/
  public String getName() 
  {
    return getEMBLEntry().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) 
  {
    final String orig_entry_name = getName();

    if(getEMBLEntry().setName(name)) 
    {
      if(orig_entry_name == null || ! name.equals(orig_entry_name)) 
      {
        // now inform the listeners that the name has changed
        final EntryChangeEvent event =
          new EntryChangeEvent(name, this, EntryChangeEvent.NAME_CHANGED);

        fireAction(entry_listener_list, event);
      }
      return true;
    } 
    else 
      return false;
  }

  /**
   *  Return the text of the EMBL header of this Entry or null if there is no
   *  header.
   **/
  public String getHeaderText() 
  {
    return getEMBLEntry().getHeaderText();
  }

  /**
   *  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.
   *  @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 
  {
    if(getEMBLEntry().setHeaderText(new_header)) 
    {
      final EntryChangeEvent event =
        new EntryChangeEvent(new_header,
                              this,
                              EntryChangeEvent.HEADER_CHANGED);

      fireAction(entry_listener_list, event);

      return true;
    }
    else 
      return false;
  }

  /**
   *  Return the path of the directory that this entry is in.
   **/
  public Document getRootDocument() 
  {
    if(getEMBLEntry() instanceof DocumentEntry &&  // XXX FIXME:
       ((DocumentEntry) getEMBLEntry()).getDocument() != null) 
    {
      return((DocumentEntry)getEMBLEntry()).getDocument().getParent();
    } 
    else 
      return null;
  }

  /**
   *  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 non-source key 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)
      throws OutOfRangeException 
  {
    final FeatureVector return_features = new FeatureVector();
    final uk.ac.sanger.artemis.io.FeatureVector embl_features =
      getEMBLEntry().getFeaturesInRange(range);

//    System.err.println("starting getFeaturesInRange()");
    Feature diana_feature;
    for(int i = 0 ; i < embl_features.size() ; ++i)
    {
      diana_feature = getFeatureOf(embl_features.featureAt(i));

//       System.err.println("getFeaturesInRange() - diana_feature:" +
//                           diana_feature + " " +
//                           embl_features.elementAt(i));

      return_features.add(diana_feature);
    }

//    System.err.println("ending getFeaturesInRange()"); 

    return return_features;
  }

  /**
   *  Return a vector containing the references of the Feature objects in this
   *  Entry.
   *  @return The non-source key features of this Entry.  The
   *    returned object is a copy - changes will not effect the Entry
   *    object itself.
   **/
  public FeatureVector getAllFeatures() 
  {
    final FeatureVector return_features = new FeatureVector();
    final uk.ac.sanger.artemis.io.FeatureVector embl_features =
      getEMBLEntry().getAllFeatures();

    Feature diana_feature;
    for(int i = 0; i < embl_features.size(); ++i) 
    {
      diana_feature = getFeatureOf(embl_features.featureAt(i));
      return_features.add(diana_feature);
    }

    return return_features;
  }
  
  public void remove(final Feature feature)
      throws ReadOnlyException 
  {
    remove(feature, false);
  }

  /**
   *  Delete a Feature from this object and the underlying embl.Entry object.
   *  @param feature The Feature to delete.
   **/
  public void remove(final Feature feature,
                     final boolean duplicate)
      throws ReadOnlyException 
  {
    synchronized(getBases()) 
    {
      // get the embl.Feature object out of the uk.ac.sanger.artemis.Feature object then
      // remove it
      if(!getEMBLEntry().remove(feature.getEmblFeature())) 
        throw new Error("internal error - remove failed");

      // now inform the listeners that a deletion has occured
      final EntryChangeEvent event =
        new EntryChangeEvent(this, feature, duplicate, EntryChangeEvent.FEATURE_DELETED);

      fireAction(entry_listener_list, event);
      feature.setEntry(null);
    }
  }

  /**
   *  Remove all the features in this Entry.
   **/
  public void removeAllFeatures()
      throws ReadOnlyException 
  {
    // remove the first feature
    while(getFeatureCount() > 0) 
      remove(getFeature(0));
  }

  /**
   *  Add a Feature to the Entry.  The new feature will be inserted in order
   *  in the feature vector.  The features in the vector are ordered by the
   *  first base of each feature.
   *  @param new_feature The new feature to add.  It should not be a member
   *    of the Entry object.
   *  @param force If true then invalid qualifiers will be quietly thrown away
   *    and a feature with invalid keys will not be added.  "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).
   *    If false an EntryInformationException will be thrown for invalid keys
   *    or qualifiers.
   *  @exception EntryInformationException Thrown force is false if this Entry
   *    cannot contain the Key, Qualifier or Key/Qualifier combination of the
   *    given Feature
   **/
  public void add(final Feature new_feature,
                  final boolean force)
        throws EntryInformationException, OutOfRangeException,
        ReadOnlyException 
  {
    add(new_feature, false, force);
  }
  
  /**
   *  Add a Feature to the Entry.  The new feature will be inserted in order
   *  in the feature vector.  The features in the vector are ordered by the
   *  first base of each feature.
   *  @param new_feature The new feature to add.  It should not be a member
   *    of the Entry object.
   *  @param force If true then invalid qualifiers will be quietly thrown away
   *    and a feature with invalid keys will not be added.  "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).
   *    If false an EntryInformationException will be thrown for invalid keys
   *    or qualifiers.
   *  @exception EntryInformationException Thrown force is false if this Entry
   *    cannot contain the Key, Qualifier or Key/Qualifier combination of the
   *    given Feature
   **/
  public void add(final Feature new_feature, final boolean duplicate,
                  final boolean force)
      throws EntryInformationException, OutOfRangeException,
             ReadOnlyException 
  {
    try
    {
      if(new_feature.getEntry() != null)
        throw new Error("internal error - Feature has a parent");

      new_feature.setEntry(this);

      final uk.ac.sanger.artemis.io.Feature old_embl_reference =
        new_feature.getEmblFeature();

      // this call will add the new_feature in the correct place in the
      // embl.Feature vector of the FeatureTable object
      final uk.ac.sanger.artemis.io.Feature new_embl_reference;

      if(force) 
      {
        new_embl_reference =
          getEMBLEntry().forcedAdd(new_feature.getEmblFeature());
      } 
      else 
      {
        new_embl_reference =
          getEMBLEntry().add(new_feature.getEmblFeature());
      }

      if(new_embl_reference != old_embl_reference) 
      {
        new_feature.getEmblFeature().setUserData(null);
        new_feature.setEmblFeature(new_embl_reference);
        new_embl_reference.setUserData(new_feature);
      }

      // now inform the listeners that a addition has occured
      final EntryChangeEvent event =
        new EntryChangeEvent(this, new_feature, duplicate,
                             EntryChangeEvent.FEATURE_ADDED);

      fireAction(entry_listener_list, event);

    } 
    catch(EntryInformationException e) 
    {
      new_feature.setEntry(null);
      throw e;
    }
  }

  /**
   *  Forget all the features in this entry and inform all the EntryChange
   *  listeners that all the features have gone away.  The garbage collector
   *  will really dispose of the features later.
   **/
  public void dispose() 
  {
    final FeatureEnumeration feature_enum = features();

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

      // now inform the listeners that a deletion has occured
      final EntryChangeEvent event =
        new EntryChangeEvent(this, current_feature,
                              EntryChangeEvent.FEATURE_DELETED);

      fireAction(entry_listener_list, event);

      current_feature.setEntry(null);
    }
  }

  /**
   *  Returns the Feature at the given index in the vector of Features.
   **/
  public Feature getFeature(int i) 
  {
    return getFeatureOf(getEMBLEntry().getFeatureAtIndex(i));
  }

  /**
   *  Create a new Feature in this Entry with no qualifiers, a default key and
   *  a location of "1..max_base"
   **/
  public Feature createFeature()
      throws ReadOnlyException 
  {
    final Key new_key;
    final Location new_location;
    final QualifierVector new_qualifiers = new QualifierVector();

    try 
    {
      new_key = getEntryInformation().getDefaultKey();
      new_location = new Location("1" + ".." + getBases().getLength());

      return createFeature(new_key, new_location, new_qualifiers);
    } 
    catch(LocationParseException e) 
    {
      throw new Error("internal error - unexpected exception: " + e);
    } 
    catch(EntryInformationException e) 
    {
      throw new Error("internal error - unexpected exception: " + e);
    } 
    catch(OutOfRangeException e) 
    {
      throw new Error("internal error - unexpected exception: " + e);
    }
  }

  /**
   *  Create and return a new Feature in this Entry with the given key,
   *  location and qualifiers.  This method creates a new embl.Feature object
   *  and then wraps it in a uk.ac.sanger.artemis.Feature object and 
   *  then inserts it in thisEntry object.
   *  @param new_key The Key to use for the new Feature.
   *  @param new_location The Location to use for the new Feature.
   *  @param new_qualifiers The a vector containing the Qualifier objects to
   *    use for the new Feature.
   *  @exception EntryInformationException Thrown force is false if this Entry
   *    cannot contain a feature with the given Key, Qualifier or
   *    Key/Qualifier combination.
   **/
  public Feature createFeature(Key new_key,
                                Location new_location,
                                QualifierVector new_qualifiers)
      throws EntryInformationException, ReadOnlyException,
      OutOfRangeException
  {
    final uk.ac.sanger.artemis.io.Feature new_embl_feature =
      getEMBLEntry().createFeature(new_key, new_location, new_qualifiers);

    final Feature new_feature = getFeatureOf(new_embl_feature);

    // now inform the listeners that a addition has occured
    final EntryChangeEvent event =
      new EntryChangeEvent(this, new_feature, EntryChangeEvent.FEATURE_ADDED);

    fireAction(entry_listener_list, event);
    return new_feature;
  }

  /**
   *  Create and return a new Feature in this Entry with the given key,
   *  location and no qualifiers.  This method creates a new embl.Feature
   *  object and then wraps it in a uk.ac.sanger.artemis.Feature object and then inserts it
   *  in this Entry object.
   *  @param new_key The Key to use for the new Feature.
   *  @param new_location The Location to use for the new Feature.
   **/
  public Feature createFeature(Key new_key,
                                Location new_location)
      throws EntryInformationException, ReadOnlyException,
      OutOfRangeException 
  {
    final QualifierVector new_qualifiers = new QualifierVector();
    return createFeature(new_key, new_location, new_qualifiers);
  }

  /**
   *  Return true if this Entry contains the given Feature.
   **/
  public boolean contains(Feature feature) 
  {
    return getEMBLEntry().contains(feature.getEmblFeature());
  }

  /**
   *  Return the index of the given Feature in the feature table of this Entry
   *  or -1 if the feature isn't in this Entry.
   **/
  public int indexOf(Feature feature) 
  {
    return getEMBLEntry().indexOf(feature.getEmblFeature());
  }

  /**
   *  Return the index of a feature in the embl feature table of this entry.
   *  The indices start at zero.
   *  @param feature The Feature to lookup.
   *  @return The index of the feature or -1 if the feature isn't in this
   *    Entry.
   **/
  public int getIndexOfFeature(Feature feature) 
  {
    return getEMBLEntry().indexOf(feature.getEmblFeature());
  }

  /**
   *  Returns an enumeration of the Feature objects in this Entry. The
   *  returned FeatureEnumeration 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() 
  {
    return new FeatureEnumerator();
  }

  /**
   *  An Enumeration of Feature objects.
   **/
  public class FeatureEnumerator implements FeatureEnumeration 
  {
    /**
     *  Create a new FeatureEnumeration that will enumerate the enclosing
     *  Entry object.  The Entry object must not be changed while the
     *  enumeration is active.
     **/
    public FeatureEnumerator()
    {
      feature_enumerator = getEMBLEntry().features();
    }

    /**
     *  See the FeatureEnumeration interface for details.
     **/
    public boolean hasMoreFeatures() 
    {
      return feature_enumerator.hasMoreFeatures();
    }

    /**
     *  See the FeatureEnumeration interface for details.
     **/
    public Feature nextFeature()
        throws NoSuchElementException 
    {

      final uk.ac.sanger.artemis.io.Feature this_feature =
        feature_enumerator.nextFeature();

      return getFeatureOf(this_feature);
    }

    /**
     *  The Enumeration for the current entry
     **/
    private uk.ac.sanger.artemis.io.FeatureEnumeration feature_enumerator;
  }

  /**
   *  Returns the number of features in this entry.
   **/
  public int getFeatureCount() 
  {
    return getEMBLEntry().getFeatureCount();
  }

  /**
   *  Return the Bases object that was passed to the constructor.
   **/
  public Bases getBases() 
  {
    return bases;
  }

  /**
   *  This method translates the start and end of every each Range in every
   *  Location into another coordinate system.  The Ranges will be truncated
   *  if necessary.
   *  @param constraint This contains the start and end base of the new
   *    coordinate system.  The position given by constraint.getStart() will
   *    be at postion/base 1 in the new coordinate system.
   *  @return a copy of the Entry which has been translated into the new
   *    coordinate system.
   **/
  public Entry truncate(final Bases new_bases, final Range constraint) 
  {
    final uk.ac.sanger.artemis.io.Entry new_embl_entry;

    if(getEMBLEntry() instanceof GFFDocumentEntry || 
       getEMBLEntry() instanceof IndexedGFFDocumentEntry)
      new_embl_entry = new GFFDocumentEntry(getEntryInformation());
    else
      new_embl_entry = new EmblDocumentEntry(getEntryInformation());

    final Entry new_entry;

    try 
    {
      new_entry = new Entry(new_bases, new_embl_entry);
    } 
    catch(OutOfRangeException e) 
    {
      throw new Error("internal error - unexpected exception: " + e);
    }
    
    if(getEMBLEntry() instanceof IndexedGFFDocumentEntry)
    {
      ((IndexedGFFDocumentEntry)getEMBLEntry()).truncate(constraint, new_entry);
      return new_entry;
    }

    final FeatureEnumeration feature_enum = features();

    while(feature_enum.hasMoreFeatures()) 
    {
      final Feature current_feature = feature_enum.nextFeature();
      final Location current_feature_location = current_feature.getLocation();

      final Location new_location =
        current_feature_location.truncate(constraint);
      
      if(new_location != null) 
      {
        final Key current_key = current_feature.getKey();
        final QualifierVector current_qualifiers =
          current_feature.getQualifiers();
        try 
        {
          final uk.ac.sanger.artemis.io.Feature new_feature; 
          if(current_feature.getEmblFeature() instanceof GFFStreamFeature)
            new_feature = new GFFStreamFeature(current_key, new_location,
                                                    current_qualifiers);
          else
            new_feature = new EmblStreamFeature(current_key, new_location,
                                                    current_qualifiers);

          new_entry.add(new Feature(new_feature), true);
        }
        catch(EntryInformationException e) 
        {
          throw new Error("internal error - unexpected exception: " + e);
        } 
        catch(OutOfRangeException e) 
        {
          throw new Error("internal error - unexpected exception: " + e);
        } 
        catch(ReadOnlyException e) 
        {
          throw new Error("internal error - unexpected exception: " + e);
        }
      }
    }

    return new_entry;
  }

  /**
   *  Return a Vector containing those features that don't have valid EMBL
   *  key.
   **/
  public FeatureVector checkForNonEMBLKeys() 
  {
    final FeatureVector non_embl_features = new FeatureVector();
    final FeatureEnumeration feature_enum = features();

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

      if(!current_feature.hasValidEMBLKey()) 
        non_embl_features.add(current_feature);
    }

    return non_embl_features;
  }

  /**
   *  Returns a vector containing those CDS features that have no /pseudo
   *  qualifier and do not have a valid start codon.  The returned features
   *  may be illegal in EMBL submissions.
   **/
  public FeatureVector checkFeatureStartCodons() 
  {
    // get all the CDS features that do not have a /pseudo or /pseudogene qualifier
    final FeaturePredicateConjunction predicate = new FeaturePredicateConjunction(
        new FeatureKeyQualifierPredicate(Key.CDS, "pseudo", false),
        new FeatureKeyQualifierPredicate(Key.CDS, "pseudogene", false),
        FeaturePredicateConjunction.AND);

    final FeatureVector non_embl_features = new FeatureVector();
    final FeatureEnumeration feature_enum = features();

    while(feature_enum.hasMoreFeatures()) 
    {
      final Feature current_feature = feature_enum.nextFeature();
      if(predicate.testPredicate(current_feature)) 
      {
        if(!current_feature.hasValidStartCodon()) 
          non_embl_features.add(current_feature);
      }
    }
    return non_embl_features;
  }

  /**
   *  Returns a vector containing those CDS features that have no /pseudo
   *  qualifier and do not have a valid stop codon.  The returned features may
   *  be illegal in EMBL submissions.
   **/
  public FeatureVector checkFeatureStopCodons() 
  {
    // get all the CDS features that do not have a /pseudo or /pseudogene qualifier
    final FeaturePredicateConjunction predicate = new FeaturePredicateConjunction(
        new FeatureKeyQualifierPredicate(Key.CDS, "pseudo", false),
        new FeatureKeyQualifierPredicate(Key.CDS, "pseudogene", false),
        FeaturePredicateConjunction.AND);

    final FeatureVector non_embl_features = new FeatureVector();

    final FeatureEnumeration feature_enum = features();

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

      if(predicate.testPredicate(current_feature)) 
      {
        if(!current_feature.hasValidStopCodon()) 
          non_embl_features.add(current_feature);
      }
    }

    return non_embl_features;
  }

  /**
   *  Returns a vector containing those features that have the same key and
   *  location as one or more other features.  These features are not allowed
   *  in EMBL submissions.
   **/
  public FeatureVector checkForEMBLDuplicates() 
  {
    final FeatureVector non_embl_features = new FeatureVector();
    final FeatureEnumeration feature_enum = features();
    Feature last_feature = null;

    while(feature_enum.hasMoreFeatures()) 
    {
      final Feature current_feature = feature_enum.nextFeature();
      final Key current_key = current_feature.getKey();
      final Location current_location = current_feature.getLocation();

      if(last_feature != null &&
          last_feature.getKey().equals(current_key) &&
          last_feature.getLocation().equals(current_location)) 
      {
        if(! non_embl_features.contains(last_feature)) 
          non_embl_features.add(last_feature);
        
        non_embl_features.add(current_feature);
        continue;
      }
      last_feature = current_feature;
    }

    return non_embl_features;
  }

  /**
   *  Returns a vector containing those features that have the same key and
   *  location as one or more other features.  These features are not allowed
   *  in EMBL submissions.
   **/
  public FeatureVector checkForOverlappingCDSs() 
  {
    final FeatureVector cds_features = new FeatureVector();
    final FeatureEnumeration feature_enum = features();

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

      if(current_feature.isCDS()) 
        cds_features.add(current_feature);
    }

    final FeatureVector return_features = new FeatureVector();

    for(int i = 0 ; i + 1 < cds_features.size() ; ++i) 
    {
      final Feature this_feature = cds_features.elementAt(i);
      final Feature next_feature = cds_features.elementAt(i + 1);

      final int this_feature_end = this_feature.getRawLastBase();
      final int next_feature_start = next_feature.getRawFirstBase();

      if(this_feature_end >= next_feature_start)
        return_features.add(this_feature);
    }

    return return_features;
  }

  /**
   *  Returns a vector containing those features that have a feature that is
   *  missing a required qualifier.  These features are not allowed in EMBL
   *  submissions.
   **/
  public FeatureVector checkForMissingQualifiers() 
  {
    final FeatureVector non_embl_features = new FeatureVector();

    final FeatureEnumeration feature_enum = features();

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

      if(!current_feature.hasRequiredQualifiers()) 
        non_embl_features.add(current_feature);
    }

    return non_embl_features;
  }

  /**
   *  Adds the specified event listener to receive entry change events from
   *  this object.
   *  @param l the event change listener.
   **/
  public void addEntryChangeListener(EntryChangeListener l) 
  {
    entry_listener_list.addElement(l);
  }

  /**
   *  Removes the specified event listener so that it no longer receives
   *  entry change events from this object.
   *  @param l the event change listener.
   **/
  public void removeEntryChangeListener(EntryChangeListener l) 
  {
    entry_listener_list.removeElement(l);
  }

  /**
   *  Adds the specified event listener to receive feature change events from
   *  this object.
   *  @param l the event change listener.
   **/
  public void addFeatureChangeListener(FeatureChangeListener l)
  {
    feature_listener_list.addElement(l);
  }

  /**
   *  Removes the specified event listener so that it no longer receives
   *  feature change events from this object.
   *  @param l the event change listener.
   **/
  public void removeFeatureChangeListener(FeatureChangeListener l) 
  {
    feature_listener_list.removeElement(l);
  }

  /**
   *  Implementation of the FeatureChangeListener interface.  We need to
   *  listen to feature change events from the Features in this object.
   *  @param event The change event.
   **/
  public void featureChanged(FeatureChangeEvent event) 
  {
    // pass the action straight through
    fireAction(feature_listener_list, event);
  }

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

  /**
   *  Send an event to those object listening for it.
   *  @param listeners A Vector of the objects that the event should be sent
   *    to.
   *  @param event The event to send
   **/
  private void fireAction(Vector<ChangeListener> listeners, ChangeEvent event) 
  {
    final Vector<ChangeListener> targets;
    // copied from a book - synchronising the whole method might cause a
    // deadlock
    synchronized(this) 
    {
      targets = (Vector) listeners.clone();
    }

    for(ChangeListener target: targets) 
    {
      if(event instanceof EntryChangeEvent) 
      {
        final EntryChangeListener entry_change_listener =
          (EntryChangeListener) target;
        entry_change_listener.entryChanged((EntryChangeEvent) event);
      } 
      else
      {
        final FeatureChangeListener feature_change_listener =
          (FeatureChangeListener) target;
        feature_change_listener.featureChanged((FeatureChangeEvent) event);
      }
    }
  }

  /**
   *  Return the uk.ac.sanger.artemis.Feature object of the given embl.Feature object.  This
   *  method will create an appropriate uk.ac.sanger.artemis.Feature if none exists.
   **/
  private Feature
    getFeatureOf(uk.ac.sanger.artemis.io.Feature embl_feature) 
  {
	if (embl_feature == null)
	{
	  return null;
	}
	
    final Feature test_feature =(Feature) embl_feature.getUserData();
 
    if(test_feature == null) 
    {
      final Feature new_feature = new Feature(embl_feature);
      new_feature.setEntry(this);
      // hack to create a FeatureSegment for each exon, which has to be done
      // aftre setting the Entry of the Feature
      new_feature.getSegments();
      return new_feature;
    } 
    else 
      return test_feature;
  }

  /**
   *  Check that all features in the embl.Entry object are in range for the
   *  Bases object that was passed to the constructor.
   **/
  private void checkLocations() throws OutOfRangeException
  {
    final uk.ac.sanger.artemis.io.FeatureEnumeration feature_enumerator =
      getEMBLEntry().features();

    while(feature_enumerator.hasMoreFeatures()) 
    {
      final uk.ac.sanger.artemis.io.Feature feature =
        feature_enumerator.nextFeature();

      final Location location = feature.getLocation();
      final RangeVector ranges = location.getRanges();

      int numRanges = ranges.size();
      for(int i = 0 ; i < numRanges ; ++i) 
      {
        if(((Range)ranges.elementAt(i)).getEnd() > getBases().getLength())
        {
          if(!(getBases().getSequence() instanceof PartialSequence))
            throw new OutOfRangeException(location.toString());
        }
      }
    }
  }

  /**
   *  Create a uk.ac.sanger.artemis.Feature object for each embl.Feature in embl_entry.
   **/
  private void createDianaFeatures() 
  {
    // enumerating the features will call getFeatureOf() for each
    // embl.Feature, which will in turn create a uk.ac.sanger.artemis.Feature

    final FeatureEnumeration feature_enumerator = features();
    while(feature_enumerator.hasMoreFeatures())  
      feature_enumerator.nextFeature();
  }

  /**
   *  Return the embl.Entry object that was passed to the constructor.
   **/
  public uk.ac.sanger.artemis.io.Entry getEMBLEntry() 
  {
    return embl_entry;
  }

  /**
   *  Return the embl.Entry object that was passed to the constructor.
   *  @param entry_information The EntryInformation object for the new Entry.
   *  @param new_type Should be one of EMBL_FORMAT, GENBANK_FORMAT or
   *    ANY_FORMAT.  If ANY_FORMAT then the Entry will be returned as is.
   *  @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.
   *  @exception EntryInformationException Thrown if an Entry using the given
   *    EntryInformation object cannot contain the Key, Qualifier or
   *    Key/Qualifier combination of one of the features in the Document.
   **/
  private uk.ac.sanger.artemis.io.Entry
    getEMBLEntryAsNewType(final EntryInformation entry_information,
                           final int new_type, final boolean force)
      throws EntryInformationException 
  {
    return DocumentEntryFactory.makeDocumentEntry(entry_information,
                                                  embl_entry, new_type,
                                                  force);
  }

}
