/* SelectMenu.java
 *
 * created: Thu Jan 14 1999
 *
 * 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/components/SelectMenu.java,v 1.39 2000/09/20 14:16:15 kmr Exp $
 **/

package diana.components;

import diana.*;
import diana.sequence.*;

import uk.ac.sanger.pathogens.OutOfRangeException;
import uk.ac.sanger.pathogens.embl.Range;
import uk.ac.sanger.pathogens.embl.Key;
import uk.ac.sanger.pathogens.embl.KeyVector;
import uk.ac.sanger.pathogens.embl.Qualifier;
import uk.ac.sanger.pathogens.embl.InvalidKeyException;
import uk.ac.sanger.pathogens.embl.EntryInformation;

import java.awt.*;
import java.awt.event.*;

/**
 *  This menu has contains items such a "Select all", "Select none" and
 *  "Select by key".
 *
 *  @author Kim Rutherford
 *  @version $Id: SelectMenu.java,v 1.39 2000/09/20 14:16:15 kmr Exp $
 **/

public class SelectMenu extends SelectionMenu {
  /**
   *  Create a new SelectMenu object and all it's menu items.
   *  @param frame The Frame that owns this Menu.
   *  @param selection The Selection that the commands in the menu will
   *    operate on.
   *  @param goto_event_source The object the we will call makeBaseVisible ()
   *    on.
   *  @param entry_group The EntryGroup object where features and exons are
   *    selected from.
   **/
  public SelectMenu (final Frame frame,
                     final Selection selection,
                     final GotoEventSource goto_event_source,
                     final EntryGroup entry_group) {
    super (frame, "Select", selection);

    this.entry_group = entry_group;
    this.goto_event_source = goto_event_source;

    selector_item = new MenuItem ("Feature Selector ...");
    selector_item.addActionListener (new ActionListener () {
      public void actionPerformed (ActionEvent event) {
        new Selector (selection, goto_event_source, getEntryGroup ());
      }
    });

    add (selector_item);

    addSeparator ();

    all_item = new MenuItem ("All", new MenuShortcut (KeyEvent.VK_A));
    all_item.addActionListener (new ActionListener () {
      public void actionPerformed (ActionEvent event) {
        selectAll ();
      }
    });

    add (all_item);

    all_bases_item =
      new MenuItem ("All Bases");
    all_bases_item.addActionListener (new ActionListener () {
      public void actionPerformed (ActionEvent event) {
        selectAllBases ();
      }
    });

    add (all_bases_item);

    none_item = new MenuItem ("None", new MenuShortcut (KeyEvent.VK_N));
    none_item.addActionListener (new ActionListener () {
      public void actionPerformed (ActionEvent event) {
        clearSelection ();
      }
    });

    add (none_item);

    select_by_key_item = new MenuItem ("By Key");
    select_by_key_item.addActionListener (new ActionListener () {
      public void actionPerformed (ActionEvent event) {
        selectByKeyPopup ();
      }
    });

    add (select_by_key_item);

    select_cds_item = new MenuItem ("CDS Features");
    select_cds_item.addActionListener (new ActionListener () {
      public void actionPerformed (ActionEvent event) {
        // select all CDS features that do not have the /pseudo qualifier
        final FeatureKeyQualifierPredicate predicate =
          new FeatureKeyQualifierPredicate (Key.CDS, "pseudo", false);

        clearSelection ();

        selectFeaturesByPredicate (predicate);
      }
    });

    add (select_cds_item);

    select_same_type_item = new MenuItem ("Same Key");
    select_same_type_item.addActionListener (new ActionListener () {
      public void actionPerformed (ActionEvent event) {
        selectSameKey ();
      }
    });

    add (select_same_type_item);

    select_orf_item = new MenuItem ("Open Reading Frame");
    select_orf_item.addActionListener (new ActionListener () {
      public void actionPerformed (ActionEvent event) {
        selectOrf ();
      }
    });

    add (select_orf_item);

    select_features_in_range_item = new MenuItem ("Features in Range");
    select_features_in_range_item.addActionListener (new ActionListener () {
      public void actionPerformed (ActionEvent event) {
        selectFeaturesInRange ();
      }
    });

    add (select_features_in_range_item);

    select_base_range_item = new MenuItem ("Base Range ...");
    select_base_range_item.addActionListener (new ActionListener () {
      public void actionPerformed (ActionEvent event) {
        selectBaseRange ();
      }
    });

    add (select_base_range_item);

    select_aa_range_in_feature_item = new MenuItem ("Feature AA Range ...");
    select_aa_range_in_feature_item.addActionListener (new ActionListener () {
      public void actionPerformed (ActionEvent event) {
        selectFeatureAARange ();
      }
    });

    add (select_aa_range_in_feature_item);

    addSeparator ();

    selection_toggle_item = new MenuItem ("Toggle Selection",
                                          new MenuShortcut (KeyEvent.VK_T));
    selection_toggle_item.addActionListener (new ActionListener () {
      public void actionPerformed (ActionEvent event) {
        toggleSelection ();
      }
    });

    add (selection_toggle_item);
  }

  /**
   *  Warn if the user tries to select more than this number of features.
   **/
  final private int MAX_FEATURE_TO_SELECT_COUNT = 1000;

  /**
   *  Select all the features in the active entries.  If there are more than
   *  1000 features the user is asked for confirmation.
   **/
  private void selectAll () {
    if (getEntryGroup ().getAllFeaturesCount () >
        MAX_FEATURE_TO_SELECT_COUNT) {
      final YesNoDialog dialog =
        new YesNoDialog (getParentFrame (),
                         "Are you sure you want to select " +
                          getEntryGroup ().getAllFeaturesCount () +
                         " features?");

      if (!dialog.getResult ()) {
        return;
      }
    }

    final FeatureVector all_features = getEntryGroup ().getAllFeatures ();

    if (getEntryGroup () instanceof SimpleEntryGroup) {
      // special case for speed
      getSelection ().set (all_features);
    } else {
      clearSelection ();

      getSelection ().add (all_features);
    }
  }

  /**
   *  Select all the bases.
   **/
  private void selectAllBases () {
    try {
      final Strand strand = getEntryGroup ().getBases ().getForwardStrand ();

      final MarkerRange new_range =
        strand.makeMarkerRangeFromPositions (1, strand.getSequenceLength ());

      getSelection ().setMarkerRange (new_range);
    } catch (OutOfRangeException e) {
      throw new Error ("internal error - unexpected exception: " + e);
    }
  }

  /**
   *  Remove the all Features and FeatureSegments in the EntryGroup from the
   *  Selection.  If the current EntryGroup is a SimpleEntryGroup then this
   *  method just calls getSelection ().clear ();
   **/
  private void clearSelection () {
    if (getEntryGroup () instanceof SimpleEntryGroup) {
      // special case for speed
      getSelection ().clear ();
    } else {
      getSelection ().setMarkerRange (null);

      final FeatureEnumeration test_enumerator = getEntryGroup ().features ();

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

        getSelection ().remove (this_feature);
        getSelection ().removeSegmentsOf (this_feature);
      }
    }
  }

  /**
   *  Invert the selection - after calling this method the selection will
   *  contain only those features that were not in the selection before the
   *  call.
   **/
  private void toggleSelection () {
    final FeatureVector selected_features = getSelection ().getAllFeatures ();

    final FeatureVector all_features = getEntryGroup ().getAllFeatures ();

    final FeatureVector new_selection = new FeatureVector ();

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

      if (!selected_features.contains (this_feature)) {
        new_selection.add (this_feature);
      }
    }

    clearSelection ();

    getSelection ().add (new_selection);
  }


  /**
   *  Popup a TextRequester component and then select all the features that
   *  have the same key as the user types.
   **/
  private void selectByKeyPopup () {
    final KeyChooser key_chooser;

    final EntryInformation default_entry_information =
      Options.getArtemisEntryInformation ();

    key_chooser = new KeyChooser (default_entry_information,
                                  new Key ("misc_feature"));

    key_chooser.getKeyChoice ().addItemListener (new ItemListener () {
      public void itemStateChanged (ItemEvent _) {
        selectByKey (key_chooser.getKeyChoice ().getSelectedItem ());
        key_chooser.setVisible (false);
        key_chooser.dispose ();
      }
    });

    key_chooser.getOKButton ().addActionListener (new ActionListener () {
      public void actionPerformed (ActionEvent _) {
        selectByKey (key_chooser.getKeyChoice ().getSelectedItem ());
        key_chooser.setVisible (false);
        key_chooser.dispose ();
      }
    });

    key_chooser.setVisible (true);
  }

  /**
   *  Select all the features that have the given key.
   **/
  private void selectByKey (final Key key) {
    final FeatureKeyPredicate predicate = new FeatureKeyPredicate (key);

    selectFeaturesByPredicate (predicate);
  }

  /**
   *  Select all the features that have the same key as the currently selected
   *  features.
   **/
  private void selectSameKey () {
    if (! checkForSelectionFeatures ()) {
      return;
    }

    final KeyVector seen_keys = new KeyVector ();

    final FeatureVector selected_features = getSelection ().getAllFeatures ();

    for (int i = 0 ; i < selected_features.size () ; ++i) {
      final Feature current_feature = selected_features.elementAt (i);

      if (!seen_keys.contains (current_feature.getKey ())) {
        seen_keys.add (current_feature.getKey ());
      }
    }

    clearSelection ();

    for (int i = 0 ; i < seen_keys.size () ; ++i) {
      selectByKey (seen_keys.elementAt (i));
    }
  }

  /**
   *  Select all the features that match the given predicate.
   **/
  private void selectFeaturesByPredicate (final FeaturePredicate predicate) {
    final FeatureVector new_selection_features = new FeatureVector ();

    final FeatureEnumeration feature_enum = getEntryGroup ().features ();

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

//      System.out.println ("adding a feature: " + new_feature.getKey ());

      if (predicate.testPredicate (current_feature)) {
        new_selection_features.add (current_feature);
      }
    }

    clearSelection ();

    getSelection ().add (new_selection_features);
  }

  /**
   *  If a range of bases is selected, then select the ORF around those
   *  bases.
   **/
  private void selectOrf () {
    if (!checkForSelectionRange ()) {
      return;
    }

    final MarkerRange range = getSelection ().getMarkerRange ();

    final Strand strand = getEntryGroup ().getBases ().getForwardStrand ();

    final MarkerRange start_orf_range =
      strand.getORFAroundMarker (range.getStart ());

    /*final*/ Marker end_marker;

    // get the marker of the first base of the last codon in the range (we
    // want to keep in the same frame)
    try {
      final int start_end_diff = range.getCount () - 1;

      final int mod_length = start_end_diff - start_end_diff % 3;

      end_marker = range.getStart ().moveBy (mod_length);
    } catch (OutOfRangeException e) {
      end_marker = range.getStart ();
    }

    final MarkerRange end_orf_range = strand.getORFAroundMarker (end_marker);

    MarkerRange new_range = range;

    if (start_orf_range != null) {
      new_range = new_range.extendRange (start_orf_range);
    }

    if (end_orf_range != null) {
      new_range = new_range.extendRange (end_orf_range);
    }

    if (start_orf_range == null && end_orf_range == null &&
        range.getCount () <= 6) {   // 6 == two codons
      new MessageDialog (getParentFrame (),
                         "there is no open reading frame at the selected " +
                         "bases");
      return;
    }

    getSelection ().setMarkerRange (new_range);
  }

  /**
   *  This method will ask the user for a range of bases (such as 100..200),
   *  using a MarkerRangeRequester component, then selects that range.
   **/
  private void selectBaseRange () {
    final MarkerRangeRequester range_requester =
      new MarkerRangeRequester ("enter a range of bases (eg. 100..200):",
                                18, "");

    final MarkerRangeRequesterListener listener =
      new MarkerRangeRequesterListener () {
        public void actionPerformed (final MarkerRangeRequesterEvent event) {
          final MarkerRange marker_range =
            event.getMarkerRange (getEntryGroup ().getBases ());

          getSelection ().setMarkerRange (marker_range);

          makeBaseVisible (getSelection ().getStartBaseOfSelection ());
        }
      };

    range_requester.addMarkerRangeRequesterListener (listener);

    range_requester.setVisible (true);
  }

  /**
   *  This method will ask the user for a range of amino acids in a feature
   *  (such as 100..200), using a Requester component, then selects that
   *  range on the sequence.
   **/
  private void selectFeatureAARange () {
    if (!checkForSelectionFeatures (getParentFrame (), getSelection ())) {
      return;
    }

    final FeatureVector selected_features = getSelection ().getAllFeatures ();

    if (selected_features.size () > 1) {
      new MessageDialog (getParentFrame (), "select only one feature");
      return;
    }

    final Feature selected_feature = selected_features.elementAt (0);

    final MarkerRangeRequester range_requester =
      new MarkerRangeRequester ("enter a range of amino acids (eg. 100..200):",
                                18, "");

    final MarkerRangeRequesterListener listener =
      new MarkerRangeRequesterListener () {
        public void actionPerformed (final MarkerRangeRequesterEvent event) {
          final Range range = event.getRawRange ();

          if (range == null) {
            return;
          }

          final int start_position = range.getStart ();
          final int end_position = range.getEnd ();

          final int start_pos_in_feature = (start_position - 1) * 3 + 1;
          final int end_pos_in_feature = (end_position - 1) * 3 + 3;

          try {
            final Marker start_marker =
              selected_feature.getPositionInSequence (start_pos_in_feature);

            final Marker end_marker =
              selected_feature.getPositionInSequence (end_pos_in_feature);

            final MarkerRange marker_range =
              new MarkerRange (start_marker.getStrand (),
                               start_marker.getPosition (),
                               end_marker.getPosition ());

            getSelection ().setMarkerRange (marker_range);

            makeBaseVisible (getSelection ().getStartBaseOfSelection ());
          } catch (OutOfRangeException e) {
            new MessageDialog (getParentFrame (),
                               "range is out of range for this feature");
          }
        }
      };

    range_requester.addMarkerRangeRequesterListener (listener);

    range_requester.setVisible (true);
  }

  /**
   *  If there are some selected base, select the feature in that range
   *  (replacing the orginal selection).  If no bases are selected, display an
   *  error.
   **/
  private void selectFeaturesInRange () {
    final Range raw_range = getSelection ().getSelectionRange ();

    if (raw_range == null) {
      new MessageDialog (getParentFrame (), "nothing selected");
      return;
    }

    try {
      getSelection ().set (getEntryGroup ().getFeaturesInRange (raw_range));
    } catch (OutOfRangeException e) {
      throw new Error ("internal error - unexpected exception: " + e);
    }
  }

  /**
   *  This method sends an GotoEvent to all the GotoEvent listeners that will
   *  make the given base visible.
   **/
  private void makeBaseVisible (final Marker base_marker) {
    goto_event_source.gotoBase (base_marker);
  }

  /**
   *  Return the EntryGroup that was passed to the constructor.
   **/
  private EntryGroup getEntryGroup () {
    return entry_group;
  }

  /**
   *  The EntryGroup object that was passed to the constructor.
   **/
  private EntryGroup entry_group;

  /**
   *  The GotoEventSource object that was passed to the constructor.
   **/
  final GotoEventSource goto_event_source;

  private MenuItem selector_item = null;
  private MenuItem all_item = null;
  private MenuItem all_bases_item = null;
  private MenuItem none_item = null;
  private MenuItem selection_toggle_item = null;
  private MenuItem select_same_type_item = null;
  private MenuItem select_features_in_range_item = null;
  private MenuItem select_base_range_item = null;
  private MenuItem select_aa_range_in_feature_item = null;
  private MenuItem select_orf_item = null;
  private MenuItem select_by_key_item = null;
  private MenuItem select_cds_item = null;
}
