/* Selection.java
 *
 * created: Fri Nov  6 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: /cvsroot/pathsoft/artemis/uk/ac/sanger/artemis/Selection.java,v 1.1 2004/06/09 09:45:04 tjc Exp $
 */

package uk.ac.sanger.artemis;

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

import uk.ac.sanger.artemis.util.OutOfRangeException;
import uk.ac.sanger.artemis.io.Range;
import uk.ac.sanger.artemis.io.RangeVector;

import java.io.*;
import java.util.Vector;
import java.awt.datatransfer.*;

/**
 *  Objects of this class hold the features/feature segments or entries that
 *  the user has selected.
 *
 *  @author Kim Rutherford
 *  @version $Id: Selection.java,v 1.1 2004/06/09 09:45:04 tjc Exp $
 *
 **/

public class Selection
    implements FeatureChangeListener, EntryChangeListener,
               Transferable, ClipboardOwner {
  /**
   *  Create a new Selection object.
   *  @param clipboard The system clipboard reference.  This may be null if
   *    this Selection should not set the system clipboard.
   **/
  public Selection (final Clipboard clipboard) {
    this.clipboard = clipboard;
  }

  private final DataFlavor flavors [] = {
    DataFlavor.stringFlavor,
    // deprecated at 1.3
    //   DataFlavor.plainTextFlavor
  };

  /**
   *  Returns the array of the data flavors that this object can provide.
   **/
  public synchronized DataFlavor[] getTransferDataFlavors () {
    return flavors;
  }

  /**
   *  Returns whether the requested flavor is supported by this object.
   *  @param flavor the requested flavor for the data
   **/
  public boolean isDataFlavorSupported (final DataFlavor flavor) {
    return
      flavor.equals (DataFlavor.stringFlavor)
// deprecated at 1.3
//      || flavor.equals (DataFlavor.plainTextFlavor)
      ;
  }

  /**
   *  If the data was requested in the "java.lang.String" flavor, return the
   *  String representing the selection, else throw an
   *  UnsupportedFlavorException.
   *  @param flavor the requested flavor for the data
   **/
  public synchronized Object getTransferData (final DataFlavor flavor)
      throws UnsupportedFlavorException, IOException {
    if (flavor.equals (DataFlavor.stringFlavor)) {
      return getSelectionText ();
// deprecated at 1.3
//    } else if (flavor.equals (DataFlavor.plainTextFlavor)) {
//      return new StringReader (getSelectionText ());
    } else {
      throw new UnsupportedFlavorException (flavor);
    }
  }

  /**
   *  Implementation of the ClipboardOwner interface.
   **/
  public void lostOwnership(final Clipboard clipboard,
                            final Transferable contents) {

  }

  /**
   *  Returns this Selection in a String.
   **/
  public String getSelectionText () {
    final FeatureVector selection_features = getAllFeatures ();

    final StringBuffer buffer = new StringBuffer ();

    final MarkerRange marker_range = getMarkerRange ();

    if (marker_range != null) {
      final String selection_bases = Strand.markerRangeBases (marker_range);

      buffer.append (selection_bases);
    }

    for (int i = 0 ; i < selection_features.size () && i < 50 ; ++i) {
      buffer.append (selection_features.elementAt (i).toString ());
    }

    return buffer.toString ();
  }

  /**
   *  Implementation of the FeatureChangeListener interface.  We need to
   *  listen to feature change events so a SelectionChangeEvent can be sent if
   *  a selected feature is changed.
   *  @param event The change event.
   **/
  public void featureChanged (FeatureChangeEvent event) {
    if (segments == null && features.contains (event.getFeature ()) ||
        getAllFeatures ().contains (event.getFeature ())) {
      if (event.getType () == FeatureChangeEvent.QUALIFIER_CHANGED ||
          event.getType () == FeatureChangeEvent.KEY_CHANGED) {
        // no need to reset the cache in this case
        final SelectionChangeEvent selection_change_event =
          new SelectionChangeEvent (this, SelectionChangeEvent.OBJECT_CHANGED);

        fireAction (selection_listener_list, selection_change_event);
      } else {
        changeSelection (SelectionChangeEvent.OBJECT_CHANGED);
      }
    }
  }

  /**
   *  Implementation of the EntryChangeListener interface.  We listen to
   *  EntryChange events so that we remove feature from the selection when
   *  they are removed from the Entry.
   **/
  public void entryChanged (final EntryChangeEvent event) {
    switch (event.getType ()) {
    case EntryChangeEvent.FEATURE_DELETED:
      
      features.remove (event.getFeature ());
      
      if (contains (event.getFeature ())) {
        // we have a segment of the feature in the Selection -
        // this will fire a SelectionChangeEvent
        removeSegmentsOf (event.getFeature ());
      } else {
        changeSelection (SelectionChangeEvent.SELECTION_CHANGED);
      }
      
      break;
    }
  }

  /**
   *  Adds the specified event listener to receive selection change events
   *  from this object.
   *  @param l the event change listener.
   **/
  public void addSelectionChangeListener (final SelectionChangeListener l) {
    selection_listener_list.addElement (l);
  }

  /**
   *  Removes the specified event listener so that it no longer receives
   *  selection change events from this object.
   *  @param l the event change listener.
   **/
  public void removeSelectionChangeListener (final SelectionChangeListener l) {
    selection_listener_list.removeElement (l);
  }

  /**
   *  Broadcast an event to notify SelectionChange event listeners that the
   *  selection has changed.
   *  @param type The type to use for the new event (see SelectionChangeEvent
   *    for details).
   **/
  private void changeSelection (final int type) {
    resetCache ();

    final SelectionChangeEvent event =
      new SelectionChangeEvent (this, type);

    fireAction (selection_listener_list, event);

    if (clipboard != null) {
      clipboard.setContents (this, this);
    }
  }

  /**
   *  Add a Feature to the selection if it isn't there already.
   **/
  public void add (final Feature feature) {
    if (addWithoutEvent (feature)) {
      changeSelection (SelectionChangeEvent.SELECTION_CHANGED);
    }
  }

  /**
   *  Add the given Feature objects to the selection, and then send an
   *  appropriate event to the SelectionChangeEvent listeners.
   **/
  public void add (final FeatureVector features) {
    for (int i = 0 ; i < features.size () ; ++i) {
      addWithoutEvent (features.elementAt (i));
    }
    changeSelection (SelectionChangeEvent.SELECTION_CHANGED);
  }

  /**
   *  Add a Feature to the selection if it isn't there already, but don't send
   *  an event to the SelectionChangeEvent listeners.
   *  @return true if and only if the segment with not already in the
   *    selection.
   **/
  private boolean addWithoutEvent (final Feature feature) {
    if (features.contains (feature)) {
      return false;
      // do nothing
    } else {
      features.add (feature);
      return true;
    }
  }

  /**
   *  Add a FeatureSegment to the selection if it isn't there already, and
   *  send an event to the SelectionChangeEvent listeners..
   **/
  public void add (final FeatureSegment segment) {
    if (addWithoutEvent (segment)) {
      changeSelection (SelectionChangeEvent.SELECTION_CHANGED);
    }
  }

  /**
   *  Add a FeatureSegment to the selection if it isn't there already, but
   *  don't send an event to the SelectionChangeEvent listeners.
   *  @return true if and only if the segment with not already in the
   *    selection.
   **/
  private boolean addWithoutEvent (final FeatureSegment segment) {
    if (segments.contains (segment)) {
      return false;
      // do nothing
    } else {
      segments.addElement (segment);
      return true;
    }
  }

  /**
   *  Remove all the objects from the selection, add the given Feature object
   *  to the selection, and then send an appropriate event to the
   *  SelectionChangeEvent listeners.
   **/
  public void set (final Feature feature) {
    clearWithoutEvent ();
    addWithoutEvent (feature);
    changeSelection (SelectionChangeEvent.SELECTION_CHANGED);
  }

  /**
   *  Remove all the objects from the selection, add the given FeatureSegment
   *  object to the selection, and then send an appropriate event to the
   *  SelectionChangeEvent listeners.
   **/
  public void set (final FeatureSegment feature_segment) {
    clearWithoutEvent ();
    addWithoutEvent (feature_segment);
    changeSelection (SelectionChangeEvent.SELECTION_CHANGED);
  }

  /**
   *  Remove all the objects from the selection, add the given Feature objects
   *  to the selection, and then send an appropriate event to the
   *  SelectionChangeEvent listeners.
   **/
  public void set (final FeatureVector features) {
    clearWithoutEvent ();
    for (int i = 0 ; i < features.size () ; ++i) {
      addWithoutEvent (features.elementAt (i));
    }
    changeSelection (SelectionChangeEvent.SELECTION_CHANGED);
  }

  /**
   *  Set the MarkerRange object that this Selection holds and remove all
   *  other objects.
   **/
  public void setMarkerRange (final MarkerRange marker_range) {
    if (this.marker_range != marker_range) {
      final MarkerRange old_marker_range = this.marker_range;
      clearWithoutEvent ();
      this.marker_range = marker_range;
      if (old_marker_range == null) {
        changeSelection (SelectionChangeEvent.SELECTION_CHANGED);
      } else {
        changeSelection (SelectionChangeEvent.OBJECT_CHANGED);
      }
    }
  }

  /**
   *  Return true if and only if there is nothing in the selection.
   **/
  public boolean isEmpty () {
    if (features.size () == 0 &&
        segments.size () == 0 &&
        marker_range == null) {
      return true;
    } else {
      return false;
    }
  }

  /**
   *  Remove all the objects from the selection, and send an event to the
   *  SelectionChangeEvent listeners.
   **/
  public void clear () {
    if (!isEmpty ()) {
      clearWithoutEvent ();
      changeSelection (SelectionChangeEvent.SELECTION_CHANGED);
    }
  }

  /**
   *  Remove all the objects from the selection, but don't send an event to
   *  the SelectionChangeEvent listeners.
   **/
  private void clearWithoutEvent () {
    features.removeAllElements ();
    segments.removeAllElements ();
    marker_range = null;
  }

  /**
   *  Remove a Feature from the selection.
   **/
  public void remove (final Feature feature) {
    if (features.remove (feature)) {
      changeSelection (SelectionChangeEvent.SELECTION_CHANGED);
    }
  }

  /**
   *  Remove the FeatureSegments of the given Feature from the selection.
   **/
  public void removeSegmentsOf (final Feature feature) {
    final FeatureSegmentVector segments = feature.getSegments ();

    for (int segment_index = 0 ;
         segment_index < segments.size () ;
         ++segment_index) {
      final FeatureSegment this_segment = segments.elementAt (segment_index);

      remove (this_segment);
    }
  }

  /**
   *  Remove a FeatureSegment from the selection.
   **/
  public void remove (final FeatureSegment segment) {
    if (segments.removeElement (segment)) {
      changeSelection (SelectionChangeEvent.SELECTION_CHANGED);
    }
  }

  /**
   *  Return true if this selection contains the given Feature.
   **/
  public boolean contains (final Feature feature) {
    if (getSelectedFeatures ().contains (feature)) {
      return true;
    }

    for (int i = 0 ; i < segments.size () ; ++i) {
      final FeatureSegment this_segment = segments.elementAt (i);
      final Feature this_feature = this_segment.getFeature ();

      if (feature == this_feature) {
        return true;
      }
    }

    return false;
  }

  /**
   *  Return true if this selection contains the given Feature.
   **/
  public boolean contains (final FeatureSegment feature_segment) {
    if (getAllSegments ().indexOf (feature_segment) == -1) {
      return false;
    } else {
      return true;
    }
  }

  /**
   *  Return a Range object that exactly covers the current selection.
   *  Returns null if and only if nothing is selected.
   **/
  public Range getSelectionRange () {
    final Marker lowest_base_marker = getLowestBaseOfSelection ();
    final Marker highest_base_marker = getHighestBaseOfSelection ();

    if (lowest_base_marker == null || highest_base_marker == null) {
      return null;
    } else {
      final Strand strand = lowest_base_marker.getStrand ();

      final int first_postion = lowest_base_marker.getRawPosition ();
      final int last_postion = highest_base_marker.getRawPosition ();

      try {
        return new Range (first_postion, last_postion);
      } catch (OutOfRangeException e) {
        throw new Error ("internal error - unexpected exception: " + e);
      }
    }
  }

  /**
   *  Return a Range object for each of the selected objects.
   **/
  public RangeVector getSelectionRanges () {
    final RangeVector return_ranges = new RangeVector ();

    if (getSelectedFeatures ().size () > 0 ||
        getSelectedSegments ().size () > 0) {
      final FeatureSegmentVector segments = getAllSegments ();

      for (int i = 0 ; i < segments.size () ; ++i) {
        final FeatureSegment this_segment = segments.elementAt (i);

        return_ranges.add (this_segment.getRawRange ());
      }
    } else {
      final MarkerRange marker_range = getMarkerRange ();

      if (marker_range != null) {
        return_ranges.add (marker_range.getRawRange ());
      }
    }

    return return_ranges;
  }

  /**
   *  Return a Marker for the base in this selection that is closest to the
   *  first base of the sequence, or null if nothing is selected.
   **/
  public Marker getLowestBaseOfSelection () {
    if (lowest_base_marker != null) {
      return lowest_base_marker;
    }

    if (getSelectedFeatures ().size () > 0 ||
        getSelectedSegments ().size () > 0) {
      final FeatureSegmentVector segments = getAllSegments ();

      int current_min = 99999999;
      Marker current_min_marker = null;

      for (int i = 0 ; i < segments.size () ; ++i) {
        final FeatureSegment this_segment = segments.elementAt (i);

        final Marker start_marker = this_segment.getStart ();
        if (start_marker.getRawPosition () < current_min) {
          current_min = start_marker.getRawPosition ();
          current_min_marker = start_marker;
        }

        final Marker end_marker = this_segment.getEnd ();
        if (end_marker.getRawPosition () < current_min) {
          current_min = end_marker.getRawPosition ();
          current_min_marker = end_marker;
        }
      }

      lowest_base_marker = current_min_marker;
    } else {
      final MarkerRange range = getMarkerRange ();

      if (range == null) {
        lowest_base_marker = null;
      } else {
        if (range.isForwardMarker ()) {
          lowest_base_marker = range.getStart ();
        } else {
          lowest_base_marker = range.getEnd ();
        }
      }
    }

    return lowest_base_marker;
  }

  /**
   *  Return a Marker for the base in this selection that is closest to the
   *  last base of the sequence, or null if nothing is selected.
   **/
  public Marker getHighestBaseOfSelection () {
    if (highest_base_marker != null) {
      return highest_base_marker;
    }

    if (getSelectedFeatures ().size () > 0 ||
        getSelectedSegments ().size () > 0) {
      final FeatureSegmentVector segments = getAllSegments ();

      int current_max = -1;
      Marker current_max_marker = null;

      for (int i = 0 ; i < segments.size () ; ++i) {
        final FeatureSegment this_segment = segments.elementAt (i);

        final Marker start_marker = this_segment.getStart ();
        if (start_marker.getRawPosition () > current_max) {
          current_max = start_marker.getRawPosition ();
          current_max_marker = start_marker;
        }

        final Marker end_marker = this_segment.getEnd ();
        if (end_marker.getRawPosition () > current_max) {
          current_max = end_marker.getRawPosition ();
          current_max_marker = end_marker;
        }
      }

      highest_base_marker = current_max_marker;
    } else {
      final MarkerRange range = getMarkerRange ();

      if (range == null) {
        highest_base_marker = null;
      } else {
        if (range.isForwardMarker ()) {
          highest_base_marker = range.getEnd ();
        } else {
          highest_base_marker = range.getStart ();
        }
      }
    }

    return highest_base_marker;
  }


  /**
   *  Return the first base of the MarkerRange (if it is set), or the first
   *  base of the selected features (if there are any selected features), or
   *  the first base of the selected FeatureSegment objects (if there are any
   *  selected FeatureSegment objects), otherwise null.  If the selection
   *  contains features or segments from both strands, then the result of
   *  calling this method is the same as calling getLowestBaseOfSelection ().
   **/
  public Marker getStartBaseOfSelection () {
    if (start_base_marker != null) {
      return start_base_marker;
    }

    if (getSelectedFeatures ().size () > 0 ||
        getSelectedSegments ().size () > 0) {
      final FeatureSegmentVector segments = getAllSegments ();

      boolean seen_forward_feature = false;
      boolean seen_reverse_feature = false;

      int current_min = 99999999;
      FeatureSegment current_min_segment = null;

      for (int i = 0 ; i < segments.size () ; ++i) {
        final FeatureSegment this_segment = segments.elementAt (i);

        if (this_segment.getFeature ().isForwardFeature ()) {
          seen_forward_feature = true;
        } else {
          seen_reverse_feature = true;
        }

        if (seen_forward_feature && seen_reverse_feature) {
          return getLowestBaseOfSelection ();
        }

        final Marker start_marker = this_segment.getStart ();

        if (current_min_segment == null ||
            start_marker.getPosition () < current_min) {
          current_min = start_marker.getPosition ();
          current_min_segment = this_segment;
        }
      }

      if (current_min_segment == null) {
        // there are no segments with real ranges
        start_base_marker = null;
      } else {
        start_base_marker = current_min_segment.getStart ();
      }
    } else {
      final MarkerRange range = getMarkerRange ();

      if (range == null) {
        start_base_marker = null;
      } else {
        start_base_marker = range.getStart ();
      }
    }

    return start_base_marker;
  }

  /**
   *  Return the last base of the MarkerRange (if it is set), or the last base
   *  of the selected features (if there are any selected features), or the
   *  last base of the selected FeatureSegment objects (if there are any
   *  selected FeatureSegment objects), otherwise null.  If the selection
   *  contains features or segments from both strands, then the result of
   *  calling this method is the same as calling getLowestBaseOfSelection ().
   **/
  public Marker getEndBaseOfSelection () {
    if (end_base_marker != null) {
      return end_base_marker;
    }

    if (getSelectedFeatures ().size () > 0 ||
        getSelectedSegments ().size () > 0) {
      final FeatureSegmentVector segments = getAllSegments ();

      boolean seen_forward_feature = false;
      boolean seen_reverse_feature = false;

      int current_max = -1;
      FeatureSegment current_max_segment = null;

      for (int i = 0 ; i < segments.size () ; ++i) {
        final FeatureSegment this_segment = segments.elementAt (i);

        if (this_segment.getFeature ().isForwardFeature ()) {
          seen_forward_feature = true;
        } else {
          seen_reverse_feature = true;
        }

        if (seen_forward_feature && seen_reverse_feature) {
          return getHighestBaseOfSelection ();
        }

        final Marker start_marker = this_segment.getStart ();

        if (current_max_segment == null ||
            start_marker.getPosition () > current_max) {
          current_max = start_marker.getPosition ();
          current_max_segment = this_segment;
        }
      }

      if (current_max_segment == null) {
        // there are no segments with real ranges
        end_base_marker = null;
      } else {
        end_base_marker = current_max_segment.getEnd ();
      }
    } else {
      final MarkerRange range = getMarkerRange ();

      if (range == null) {
        end_base_marker = null;
      } else {
        end_base_marker = range.getEnd ();
      }
    }

    return end_base_marker;
  }

  /**
   *  Return the bases of the selected features or the selected marker range.
   **/
  public String getSelectedBases () {
    if (getMarkerRange () == null) {
      final StringBuffer buffer = new StringBuffer ();

      final FeatureVector all_features = getAllFeatures ();

      for (int i = 0 ; i < all_features.size () ; ++i) {
        final Feature this_feature = all_features.elementAt (i);
        buffer.append (this_feature.getBases ());
      }

      return buffer.toString ();
    } else {
      return Strand.markerRangeBases (getMarkerRange ());
    }
  }

  /**
   *  Return the object that was set with the setMarkerRange (MarkerRange)
   *  method.
   **/
  public MarkerRange getMarkerRange () {
    return marker_range;
  }

  /**
   *  Return a vector of the Feature objects of this selection.
   **/
  public FeatureVector getSelectedFeatures () {
    return features;
  }

  /**
   *  Return a vector of the FeatureSegment objects of this selection.
   **/
  public FeatureSegmentVector getSelectedSegments () {
    return segments;
  }

  /**
   *  Return a vector of all the FeatureSegment objects of this selection.
   *  This will include the FeatureSegment objects that are owned by Feature
   *  objects in the selection.
   **/
  public FeatureSegmentVector getAllSegments () {
    final FeatureSegmentVector return_segments = new FeatureSegmentVector ();

    for (int i = 0 ; i < features.size () ; ++i) {
      final Feature selection_feature = features.elementAt (i);
      final FeatureSegmentVector segments = selection_feature.getSegments ();

      for (int segment_index = 0 ;
           segment_index < segments.size () ;
           ++segment_index) {
        final FeatureSegment this_segment = segments.elementAt (segment_index);

        if (!return_segments.contains (this_segment)) {
          return_segments.addElement (this_segment);
        }
      }
    }

    for (int i = 0 ; i < segments.size () ; ++i) {
      final FeatureSegment this_segment = segments.elementAt (i);

      if (!return_segments.contains (this_segment)) {
        return_segments.addElement (this_segment);
      }
    }

    return return_segments;
  }

  /**
   *  Return a vector of all the Feature objects of this selection.  This will
   *  include the Feature objects that own the FeatureSegment objects in the
   *  selection.
   **/
  public FeatureVector getAllFeatures () {
    if (all_features == null) {
      all_features = (FeatureVector) features.clone ();
      
      for (int i = 0 ; i < segments.size () ; ++i) {
        final FeatureSegment this_segment = segments.elementAt (i);
        final Feature this_feature = this_segment.getFeature ();
        
        if (!all_features.contains (this_feature)) {
          all_features.add (this_feature);
        }
      }
    }

    return all_features;
  }

  /**
   *  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 (final Vector listeners, final 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 SelectionChangeEvent) {
        final SelectionChangeListener selection_change_listener =
          (SelectionChangeListener) target;
        selection_change_listener.selectionChanged ((SelectionChangeEvent) event);
      } else {
        throw new Error ("Selection.fireAction () - unknown event");
      }
    }
  }

  /**
   *  Reset all cached values in the Selection.
   **/
  private void resetCache () {
    lowest_base_marker = null;
    highest_base_marker = null;
    start_base_marker = null;
    end_base_marker = null;
    all_features = null;
  }

  /**
   *  A Vector containing the Feature objects that this selection currently
   *  holds.
   **/
  private FeatureVector features = new FeatureVector ();

  /**
   *  A Vector containing the FeatureSegment objects that this selection
   *  currently holds.
   **/
  private FeatureSegmentVector segments = new FeatureSegmentVector ();

  /**
   *  This is a cache used by getAllFeatures ().  This will be set to null
   *  anytime the selection changes.
   **/
  private FeatureVector all_features = null;

  /**
   *  Each Selection object can hold one MarkerRange.
   **/
  private MarkerRange marker_range = null;

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

  /**
   *  The system clipboard as passed to the constructor.
   **/
  final private Clipboard clipboard;

  /**
   *  The cached value that getLowestBaseOfSelection () returns/sets.
   **/
  private Marker lowest_base_marker = null;

  /**
   *  The cached value that getHighestBaseOfSelection () returns/sets.
   **/
  private Marker highest_base_marker = null;

  /**
   *  The cached value that getStartBaseOfSelection () returns/sets.
   **/
  private Marker start_base_marker = null;

  /**
   *  The cached value that getEndBaseOfSelection () returns/sets.
   **/
  private Marker end_base_marker = null;
}
