/* SimpleEntryGroup.java
 *
 * created: Wed Nov 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: /nfs/disk222/yeastpub/Repository/powmap/diana/SimpleEntryGroup.java,v 1.11 2000/07/24 13:33:26 kmr Exp $
 **/

package diana;

import diana.sequence.*;
import uk.ac.sanger.pathogens.embl.Range;
import uk.ac.sanger.pathogens.embl.StreamSequence;
import uk.ac.sanger.pathogens.embl.SimpleDocumentEntry;
import uk.ac.sanger.pathogens.ReadOnlyException;
import uk.ac.sanger.pathogens.OutOfRangeException;

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

/**
 *  This class implements a vector of Entry objects, with additional methods
 *  for querying and changing the feature tables of all the entries at
 *  once.  Objects of this class act a bit like single Entry objects.
 *
 *  @author Kim Rutherford
 *  @version $Id: SimpleEntryGroup.java,v 1.11 2000/07/24 13:33:26 kmr Exp $
 **/

public class SimpleEntryGroup extends EntryVector
    implements EntryGroup
{
  /**
   *  Create a new empty SimpleEntryGroup object.
   **/
  public SimpleEntryGroup (final Bases bases) {
    this.bases = bases;
  }

  /**
   *  Returns true if and only if there are any unsaved changes in any of the
   *  Entry objects in this EntryGroup.
   **/
  public boolean hasUnsavedChanges () {
    for (int entry_index = 0 ;
         entry_index < size () ;
         ++entry_index) {
      if (elementAt (entry_index).hasUnsavedChanges ()) {
        return true;
      }
    }

    return false;
  }

  /**
   *  Return the default Entry for this SimpleEntryGroup.  The "default" is the
   *  Entry where new features are created.
   **/
  public Entry getDefaultEntry () {
    return default_entry;
  }

  /**
   *  Set the default Entry.  The "default" is the Entry where new features
   *  are created.
   *  @param entry The new default entry.  If this Entry is not active this
   *    method will return immediately.
   **/
  public void setDefaultEntry (Entry entry) {
    if (entry != null && !isActive (entry)) {
      return;
    }

    if (default_entry == entry) {
      // do nothing
      return;
    } else {
      default_entry = entry;
    }

    // now inform the listeners that a change has occured
    final EntryGroupChangeEvent event =
      new EntryGroupChangeEvent (this,
                                 getDefaultEntry (),
                                 EntryGroupChangeEvent.NEW_DEFAULT_ENTRY);

    fireAction (entry_group_listener_list, event);
  }

  /**
   *  Return the index of a feature within this object.  This method treats
   *  all the features in all the active entries as if they were in one big
   *  array.  The first feature of the first entry will have index 1, the
   *  first from the second entry will have index 1 + (the number of features
   *  in the first entry), etc.
   *  @param feature The feature to find the index of.
   *  @return The index of the feature or -1 if the feature isn't in any of
   *    the entries.  The first index is 0 the last is the total number of
   *    features in all the entries of this object minus one.
   **/
  public int indexOf (Feature feature) {
    int feature_count_of_previous_entries = 0;

    for (int entry_index = 0 ;
         entry_index < active_entries.size () ;
         ++entry_index) {
      final Entry this_entry = active_entries.elementAt (entry_index);
      final int feature_index = this_entry.indexOf (feature);

      if (feature_index != -1) {
        return feature_index + feature_count_of_previous_entries;
      }

      feature_count_of_previous_entries += this_entry.getFeatureCount ();
    }

    return -1;
  }

  /**
   *  Return true if any of the active entries in the group contains the given
   *  feature.
   **/
  public boolean contains (Feature feature) {
    for (int i = 0 ; i < active_entries.size () ; ++i) {
      final Entry current_entry = active_entries.elementAt (i);

      if (current_entry.contains (feature)) {
        return true;
      }
    }

    return false;
  }

  /**
   *  Return true if the given Entry is active (visible).  The Feature objects
   *  in an Entry that is not active will be ignored by the methods that deal
   *  will features: featureAt (), indexOf (), contains (), features (), etc.
   **/
  public boolean isActive (Entry entry) {
    if (active_entries.contains (entry)) {
      return true;
    } else {
      return false;
    }
  }

  /**
   *  Set the "active" setting of the Entry at the given index.  If the index
   *  refers to the default entry and new_active is false, the default entry
   *  will be set to the active entry or null if there are no active entries.
   *  @param index The index of the Entry to change.
   *  @param active The new active setting.
   **/
  public void setIsActive (int index, boolean new_active) {
    final Entry entry = elementAt (index);

    if (new_active) {
      if (isActive (entry)) {
        // no change
        return;
      } else {
        // this is slow but it guarantees that the Entry references in the
        // active_entries vector are in the same order as in the
        // SimpleEntryGroup

        final EntryVector new_active_entries = new EntryVector ();

        for (int i = 0 ; i < size () ; ++i) {
          if (active_entries.contains (elementAt (i)) || index == i) {
            new_active_entries.add (elementAt (i));
          }
        }

        active_entries.removeAllElements ();

        for (int i = 0 ; i < new_active_entries.size () ; ++i) {
          active_entries.add (new_active_entries.elementAt (i));
        }

        if (active_entries.size () >= 1 && getDefaultEntry () == null) {
          // there was no default entry before calling addElement () so
          // make the first non-sequence entry the default entry
          if (active_entries.elementAt (0) == getSequenceEntry () &&
              active_entries.size () == 1) {
            // don't set the default entry to be the sequence entry unless
            // the user asks for it
            setDefaultEntry (null);
          } else {
            if (active_entries.size () == 1) {
              setDefaultEntry (active_entries.elementAt (0));
            } else {
              setDefaultEntry (active_entries.elementAt (1));
            }
          }
        }
      }
    } else {
      if (!isActive (entry)) {
        // no change
        return;
      } else {
        active_entries.removeElement (entry);

        if (entry == getDefaultEntry ()) {
          if (active_entries.size () > 0) {
            if (active_entries.elementAt (0) == getSequenceEntry ()) {
              // don't set the default entry to be the sequence entry unless
              // the user asks for it
              if (active_entries.size () > 1) {
                setDefaultEntry (active_entries.elementAt (1));
              } else {
                setDefaultEntry (null);
              }
            } else {
              setDefaultEntry (active_entries.elementAt (0));
            }
          } else {
            setDefaultEntry (null);
          }
        }
      }
    }

    // now inform the listeners that a change has occured
    final EntryGroupChangeEvent event;

    if (new_active) {
      // the state was inactive - now it is active
      event = new EntryGroupChangeEvent (this,
                                         entry,
                                         EntryGroupChangeEvent.ENTRY_ACTIVE);
    } else {
      // the state was active - now it is inactive
      event = new EntryGroupChangeEvent (this,
                                         entry,
                                         EntryGroupChangeEvent.ENTRY_INACTIVE);
    }

    fireAction (entry_group_listener_list, event);
  }

  /**
   *  Set the "active" setting of the given Entry.  The Entry is the default
   *  entry and new_active is false, the default entry will be set to the
   *  active entry or null if there are no active entries.
   *  @param entry The Entry to activate or deactivate.
   *  @param new_active The new active setting.
   **/
  public void setIsActive (final Entry entry, final boolean new_active) {
    setIsActive (indexOf (entry), new_active);
  }


  /**
   *  Return the Entry from this SimpleEntryGroup that contains the sequence
   *  to view or return null if none of the entries contains a sequence.
   **/
  public Entry getSequenceEntry () {
    if (size () == 0) {
      return null;
    } else {
      return elementAt (0);
    }
  }

  /**
   *  Returns the base length of the sequence of the first Entry in this group
   *  or 0 if this group is empty.
   **/
  public int getSequenceLength () {
    return getBases ().getLength ();
  }

  /**
   *  Returns the Bases object of the first Entry in this group or null if
   *  this group is empty.
   **/
  public Bases getBases () {
    return bases;
  }

  /**
   *  Reverse and complement the sequence and all features in every Entry in
   *  this SimpleEntryGroup.
   **/
  public void reverseComplement ()
      throws ReadOnlyException {
    for (int i = 0 ; i < size () ; ++i) {
      if (elementAt (i).isReadOnly ()) {
        throw new ReadOnlyException ();
      }
    }

    // reverse the sequence
    getBases ().reverseComplement ();
  }

  /**
   *  Return the Feature at the given index.  This method treats all the
   *  features in all the entries as if they were in one big array.  See
   *  the comment on indexOf ().
   *  @param index The index of the required Feature.
   *  @return The Feature at the given index.  The first index is 0 the last
   *    is the total number of features in all the entries of this object minus
   *    one.  If the index is out of range then null will be returned.
   **/
  public Feature featureAt (int index) {
    if (index < 0) {
      return null;
    }

    int feature_count_of_previous_entries = 0;

    for (int entry_index = 0 ;
         entry_index < active_entries.size () ;
         ++entry_index) {
      final Entry this_entry = active_entries.elementAt (entry_index);

      if (index < this_entry.getFeatureCount ()) {
        return this_entry.getFeature (index);
      }

      index -= this_entry.getFeatureCount ();
    }

    return null;
  }

  /**
   *  Return a vector containing the references of the Feature objects within
   *  the given range of indices.
   *  @param start_index The index of the first feature to return.
   *  @param end_index The index of the last feature to return.
   **/
  public FeatureVector getFeaturesInIndexRange (final int start_index,
                                                final int end_index) {
    final FeatureVector return_vector = new FeatureVector ();

    for (int i = start_index ; i <= end_index ; ++i) {
      return_vector.add (featureAt (i));
    }

    return return_vector;
  }

  /**
   *  Return a vector containing the references of the Feature objects within
   *  the given range for all the active entries in the SimpleEntryGroup.
   *  @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_vector = new FeatureVector ();

    for (int i = 0 ; i < size () ; ++i) {
      final Entry this_entry = elementAt (i);

      if (isActive (this_entry)) {
        final FeatureVector visible_entry_features =
          elementAt (i).getFeaturesInRange (range);

        for (int feature_index = 0 ;
             feature_index < visible_entry_features.size () ;
             ++feature_index) {
          final Feature this_feature =
            visible_entry_features.elementAt (feature_index);
          return_vector.add (this_feature);
        }
      }
    }

    return return_vector;
  }

  /**
   *  Return a vector containing the references of the Feature objects from
   *  all the active entries in the SimpleEntryGroup.
   *  @return The non-source key features in active entries of this
   *    SimpleEntryGroup.  The returned object is a copy - changes will not
   *    effect the SimpleEntryGroup object itself.
   **/
  public FeatureVector getAllFeatures () {
    final FeatureVector return_vector = new FeatureVector ();

    for (int i = 0 ; i < size () ; ++i) {
      final Entry this_entry = elementAt (i);

      if (isActive (this_entry)) {
        final FeatureVector entry_features = elementAt (i).getAllFeatures ();

        for (int feature_index = 0 ;
             feature_index < entry_features.size () ;
             ++feature_index) {
          final Feature this_feature =
            entry_features.elementAt (feature_index);
          return_vector.add (this_feature);
        }
      }
    }

    return return_vector;
  }

  /**
   *  Return a count of the number of Feature objects from all the active
   *  entries in the SimpleEntryGroup.
   *  @return A count of the non-source key features in active entries of this
   *    SimpleEntryGroup.
   **/
  public int getAllFeaturesCount () {
    int return_count = 0;

    for (int i = 0 ; i < size () ; ++i) {
      final Entry this_entry = elementAt (i);

      if (isActive (this_entry)) {
        return_count += this_entry.getFeatureCount ();
      }
    }

    return return_count;
  }

  /**
   *  Add an Entry to this object and then emit the appropriate EntryChange
   *  events.
   **/
  public void addElement (Entry entry) {
    super.addElement (entry);

    if (default_entry == null) {
      // set the default Entry to whichever Entry gets added first
      default_entry = entry;
    }

    active_entries.add (entry);

    // now inform the listeners that an addition has occured
    final EntryGroupChangeEvent event =
      new EntryGroupChangeEvent (this,
                                 entry,
                                 EntryGroupChangeEvent.ENTRY_ADDED);

    fireAction (entry_group_listener_list, event);

    // make the new entry the default entry if and only if there was no entry
    // previously or there was only one entry previously and it contained
    // sequence but no features
    if (size () == 1) {
      setDefaultEntry (entry);
    } else {
      if (size () == 2) {
        final Entry first_entry = elementAt (0);

        if (first_entry.getFeatureCount () == 0) {
          final Bases first_entry_bases = first_entry.getBases ();

          if (first_entry_bases != null &&
              first_entry_bases.getLength () > 0) {
            setDefaultEntry (entry);
          }
        }
      }
    }

    entry.addEntryChangeListener (this);
    entry.addFeatureChangeListener (this);
  }

  /**
   *  A convenience method that does the same as addElement (Entry).
   **/
  public void add (final Entry entry) {
    addElement (entry);
  }

  /**
   *  Remove an Entry from this object and then emit the appropriate
   *  EntryGroupChange events.  The first entry in the group can only be
   *  removed if it is the only Entry because the first Entry contains the
   *  sequence.
   *  @return true if the removal succeeded, false if it fails (which can if
   *    the given Entry isn't in this SimpleEntryGroup or if the user tries to
   *    remove the first Entry).
   **/
  public boolean removeElement (final Entry entry) {
    // this call will sort out the default entry
    setIsActive (indexOf (entry), false);

    entry.dispose ();

    final boolean remove_return = super.removeElement (entry);

    entry.removeEntryChangeListener (this);
    entry.removeFeatureChangeListener (this);

    active_entries.removeElement (entry);

    // now inform the listeners that a deletion has occured
    final EntryGroupChangeEvent event =
      new EntryGroupChangeEvent (this,
                                 entry,
                                 EntryGroupChangeEvent.ENTRY_DELETED);

    fireAction (entry_group_listener_list, event);

    return remove_return;
  }

  /**
   *  A convenience method that does the same as removeElement (Entry).
   **/
  public boolean remove (final Entry entry) {
    return removeElement (entry);
  }

  /**
   *  Create a new (blank) Feature in the default Entry of this
   *  SimpleEntryGroup.  See getDefaultEntry () and Entry.createFeature ().
   *  @return The new Feature.
   **/
  public Feature createFeature ()
      throws ReadOnlyException {
    final Feature new_feature = getDefaultEntry ().createFeature ();

    return new_feature;
  }

  /**
   *  Create a new (empty) Entry in this SimpleEntryGroup.  See
   *  Entry.newEntry ().
   *  @return The reference of the new Entry.
   **/
  public Entry createEntry () {
    final Entry new_entry = Entry.newEntry (getBases ());

    add (new_entry);

    return new_entry;
  }

  /**
   *  Create a new (empty) Entry in this SimpleEntryGroup.  See
   *  Entry.newEntry ().
   *  @param name The (file) name of the new Entry.
   *  @return The reference of the new Entry.
   **/
  public Entry createEntry (final String name) {
    final Entry new_entry = createEntry ();

    new_entry.setName (name);

    return new_entry;
  }

  /**
   *  Returns an enumeration of the Feature objects in this
   *  SimpleEntryGroup. 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
     *  SimpleEntryGroup object.  The SimpleEntryGroup object must not be
     *  changed while the enumeration is active.
     **/
    public FeatureEnumerator () {
      active_entries = getActiveEntries ();

      entry_index = 0;

      if (active_entries.size () > 0) {
        feature_enumerator =
          active_entries.elementAt (entry_index).features ();
      } else {
        feature_enumerator = null;
      }
    }

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

      if (feature_enumerator.hasMoreFeatures ()) {
        return true;
      }

      ++entry_index;

      if (entry_index == active_entries.size ()) {
        return false;
      } else {
        feature_enumerator =
          active_entries.elementAt (entry_index).features ();
        return hasMoreFeatures ();
      }
    }

    /**
     *  See the FeatureEnumeration interface for details.
     **/
    public Feature nextFeature ()
        throws NoSuchElementException {
      if (feature_enumerator == null) {
        throw new NoSuchElementException ();
      }

      return feature_enumerator.nextFeature ();
    }

    /**
     *  The EntryVector object that we are enumerating.  Set to null when there
     *  are no more Feature objects.
     **/
    private EntryVector active_entries;

    /**
     *  The index of the Entry that we will get the next Feature from.
     **/
    private int entry_index = -1;

    /**
     *  The Enumeration for the current entry
     **/
    private FeatureEnumeration feature_enumerator;
  }

  /**
   *  Implementation of the FeatureChangeListener interface.  We listen for
   *  changes in every feature of every entry in this group.
   **/
  public void featureChanged (FeatureChangeEvent event) {
    // pass the action straight through
    fireAction (feature_listener_list, event);
  }

  /**
   *  Implementation of the EntryChangeListener interface.  We listen for
   *  changes from every entry in this group and pass the events though to all
   *  the object listening for EntryChangeEvents for the event from this
   *  SimpleEntryGroup.
   **/
  public void entryChanged (EntryChangeEvent event) {
    // pass the action straight through
    fireAction (entry_listener_list, event);
  }

  /**
   *  Implementation of the SequenceChangeListener interface.  We listen in
   *  here so that we can pass the event on to all the Feature objects in all
   *  the Entry objects in this object.
   **/
  public void sequenceChanged (final SequenceChangeEvent event) {
//      for (int i = 0 ; i < size () ; ++i) {
//        final FeatureEnumeration feature_enumerator = elementAt (i).features ();

//        while (feature_enumerator.hasMoreFeatures ()) {
//          final Feature this_feature = feature_enumerator.nextFeature ();

//          this_feature.sequenceChanged (event);
//        }
//      }
  }

  /**
   *  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 listeners, ChangeEvent event) {
    final Vector targets;
    // copied from a book - synchronising the whole method might cause a
    // deadlock
    synchronized (this) {
      targets = (Vector) listeners.clone ();
    }

    for (int i = 0 ; i < targets.size () ; ++i) {
      ChangeListener target = (ChangeListener) targets.elementAt (i);

      if (event instanceof EntryGroupChangeEvent) {
        final EntryGroupChangeListener entry_group_change_listener =
          (EntryGroupChangeListener) target;
        final EntryGroupChangeEvent group_change_event =
          (EntryGroupChangeEvent) event;
        entry_group_change_listener.entryGroupChanged (group_change_event);
      } else {
        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);
        }

      }
    }
  }

  /**
   *  Adds the specified event listener to receive entry group change events
   *  from this object.
   *  @param l the event change listener.
   **/
  public void addEntryGroupChangeListener (EntryGroupChangeListener l) {
    entry_group_listener_list.addElement (l);
  }

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

  /**
   *  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);
  }

  /**
   *  Return the reference of an EntryVector containing the active entries of
   *  this EntryGroup.
   **/
  public EntryVector getActiveEntries () {
    return (EntryVector) active_entries.clone ();
  }

  /**
   *  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 EntryGroup which has been translated into the new
   *    coordinate system.
   **/
  public EntryGroup truncate (final Range constraint) {
    final Bases new_bases = getBases ().truncate (constraint);

    final EntryGroup new_entry_group = new SimpleEntryGroup (new_bases);

    for (int i = 0 ; i < size () ; ++i) {
      final Entry this_entry = elementAt (i);

      final Entry new_entry = this_entry.truncate (new_bases, constraint);

      new_entry_group.add (new_entry);
    }

    if (size () > 0) {
      final StreamSequence sequence =
        (StreamSequence) new_bases.getSequence ();
      final SimpleDocumentEntry document_entry =
        (SimpleDocumentEntry) new_entry_group.elementAt (0).getEMBLEntry ();
      document_entry.setSequence (sequence);
    }

    return new_entry_group;
  }

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

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

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

  /**
   *  A Vector containing those entries that are currently active (visible).
   **/
  final private EntryVector active_entries = new EntryVector ();

  /**
   *  The default Entry for this SimpleEntryGroup.  The "default" is the Entry
   *  where new features are created.
   **/
  private Entry default_entry = null;

  /**
   *  The Bases object that was passed to the constructor.
   **/
  /*final*/ private Bases bases;
}
