/* FeatureList.java
 *
 * created: Fri Oct  9 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/components/FeatureList.java,v 1.82 2000/06/22 11:24:54 kmr Exp $
 */

package diana.components;

import diana.*;
import diana.sequence.*;
import uk.ac.sanger.pathogens.embl.Key;
import uk.ac.sanger.pathogens.StringVector;

import java.awt.event.*;
import java.awt.*;
import java.text.*;
import java.util.*;

/**
 *  This component gives the user a list containing the details the current
 *  Features.
 *
 *  @author Kim Rutherford
 *  @version $Id: FeatureList.java,v 1.82 2000/06/22 11:24:54 kmr Exp $
 *
 **/

public class FeatureList extends EntryGroupPanel
  implements EntryGroupChangeListener,
             EntryChangeListener, FeatureChangeListener,
             SelectionChangeListener, DisplayComponent
{
  /**
   *  Create a new FeatureList with the default number of rows.
   *  @param entry_group The EntryGroup that this component will display.
   *  @param selection The Selection object for this component.  Selected
   *    objects will be highlighted.
   *  @param goto_event_source The object to use when we need to call
   *    gotoBase ().
   **/
  public FeatureList (final EntryGroup entry_group,
                      final Selection selection,
                      final GotoEventSource goto_event_source) {
    super (entry_group, selection, goto_event_source);

    getCanvas ().addMouseListener (new MouseAdapter () {
      /**
       *  Listen for mouse press events so that we can do popup menus and
       *  selection.
       **/
      public void mousePressed (MouseEvent event) {
        if (event.isPopupTrigger () || event.isMetaDown ()) {
          final FeaturePopup popup =
            new FeaturePopup (FeatureList.this,
                              getEntryGroup (),
                              getSelection (),
                              getGotoEventSource ());
          final Canvas parent = (Canvas) event.getSource ();

          parent.add (popup);

          popup.show (parent, event.getX (), event.getY ());
        } else {
          handleCanvasMousePress (event);
        }
      }
    });

    createScrollbar ();

    addComponentListener (new ComponentAdapter () {
      public void componentShown (ComponentEvent e) {
        refresh ();
      }
      public void componentResized (ComponentEvent e) {
        refresh ();
      }
    });

    getSelection ().addSelectionChangeListener (this);

    // changes to the EntryGroup will be noticed by listening for EntryChange
    // and FeatureChange events.

    getEntryGroup ().addEntryGroupChangeListener (this);
    getEntryGroup ().addEntryChangeListener (this);
    getEntryGroup ().addFeatureChangeListener (this);

    refresh ();
  }

  /**
   *  Remove this component from all the listener lists it is on.
   **/
  void stopListening () {
    getSelection ().removeSelectionChangeListener (this);

    getEntryGroup ().removeEntryGroupChangeListener (this);
    getEntryGroup ().removeEntryChangeListener (this);
    getEntryGroup ().removeFeatureChangeListener (this);
  }

  /**
   *  Returns the value of a flag that indicates whether this component can be
   *  traversed using Tab or Shift-Tab keyboard focus traversal - returns true
   *  for FeatureDisplay components
   **/
  public boolean isFocusTraversable () {
    return true;
  }

  /**
   *  Set value of the show correlation scores flag.
   *  @param show_correlation_scores Show correlation scores in the list if
   *    and only if this argument is true.
   **/
  public void setCorrelationScores (final boolean show_correlation_scores) {
    if (this.show_correlation_scores != show_correlation_scores) {
      this.show_correlation_scores = show_correlation_scores;
      refresh ();
    } else {
      // do nothing
    }
  }

  /**
   *  Get the value of the "show correlation scores" flag.
   **/
  public boolean getCorrelationScores () {
    return show_correlation_scores;
  }

  /**
   *  Set value of the show /gene flag.
   *  @param show_gene_names If true this component will show the /gene (really
   *    Feature.getIDString ()) instead of the key.
   **/
  public void setShowGenes (final boolean show_gene_names) {
    if (this.show_gene_names != show_gene_names) {
      this.show_gene_names = show_gene_names;
      refresh ();
    } else {
      // do nothing
    }
  }

  /**
   *  Get the value of the "show genes" flag.
   **/
  public boolean getShowGenes () {
    return show_gene_names;
  }

  /**
   *  Set value of the show /product flag.
   *  @param show_products If true this component will show the /product
   *    qualifier instead of the /note.
   **/
  public void setShowProducts (final boolean show_products) {
    if (this.show_products != show_products) {
      this.show_products = show_products;
      refresh ();
    } else {
      // do nothing
    }
  }

  /**
   *  Get the value of the "show products" flag.
   **/
  public boolean getShowProducts () {
    return show_products;
  }

  /**
   *  Implementation of the EntryGroupChangeListener interface.  We listen to
   *  EntryGroupChange events so that we can update the display if entries
   *  are added or deleted.
   **/
  public void entryGroupChanged (EntryGroupChangeEvent event) {
    switch (event.getType ()) {
    case EntryGroupChangeEvent.ENTRY_ADDED:
    case EntryGroupChangeEvent.ENTRY_ACTIVE:
    case EntryGroupChangeEvent.ENTRY_DELETED:
    case EntryGroupChangeEvent.ENTRY_INACTIVE:
      refresh ();
      break;
    }
  }

  /**
   *  Implementation of the FeatureChangeListener interface.
   **/
  public void featureChanged (FeatureChangeEvent event) {
    if (!isVisible ()) {
      return;
    }

    refresh ();
  }

  /**
   *  Implementation of the EntryChangeListener interface.  We listen to
   *  EntryChange events so that we can update the list if features are added
   *  or deleted.
   **/
  public void entryChanged (EntryChangeEvent event) {
    if (!isVisible ()) {
      return;
    }

    refresh ();
  }

  /**
   *  Implementation of the SelectionChangeListener interface.  We listen to
   *  SelectionChange events so that we can update the list to reflect the
   *  current selection.
   **/
  public void selectionChanged (SelectionChangeEvent event) {
    if (!isVisible ()) {
      return;
    }

    if (event.getSource () == this) {
      // don't bother with events we sent ourself
      return;
    }

    selection_changed_flag = true;

    refresh ();
  }

  /**
   *  Return a vector containing the text that is shown in the list - one
   *  String per line.
   **/
  public StringVector getListStrings () {
    final StringVector return_vector = new StringVector ();

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

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

      return_vector.add (makeFeatureString (this_feature));
    }

    return return_vector;
  }

  /**
   *  Create the scroll bar.
   **/
  private void createScrollbar () {
    scrollbar = new Scrollbar (Scrollbar.VERTICAL);
    scrollbar.setValues (0, 1, 0,
                         getEntryGroup ().getAllFeaturesCount () - 1);
    scrollbar.setUnitIncrement (1);
    scrollbar.setBlockIncrement (1);
    scrollbar.addAdjustmentListener (new AdjustmentListener () {
      public void adjustmentValueChanged(AdjustmentEvent e) {
        setFirstIndex (e.getValue ());
        refresh ();
      }
    });

    add (scrollbar, "East");
  }

  public void setFirstIndex (final int first_index) {
    this.first_index = first_index;
  }

  /**
   *  Handle a mouse press event on the drawing canvas - select on click,
   *  select and broadcast it on double click.
   **/
  private void handleCanvasMousePress (final MouseEvent event) {
    if (event.getID() != MouseEvent.MOUSE_PRESSED) {
      return;
    }

    getCanvas ().requestFocus ();

    if (!event.isShiftDown ()) {
      getSelection ().clear ();
    }

    final int clicked_feature_index =
      scrollbar.getValue () + event.getY () / getFontHeight ();

    if (clicked_feature_index < getEntryGroup ().getAllFeaturesCount ()) {
      final FeatureVector selected_features =
        getSelection ().getAllFeatures ();

      final Feature clicked_feature =
        getEntryGroup ().featureAt (clicked_feature_index);

      if (selected_features.contains (clicked_feature)) {
        getSelection ().remove (clicked_feature);
        getSelection ().removeSegmentsOf (clicked_feature);
      } else {
        getSelection ().add (clicked_feature);
      }

      if (event.getClickCount () == 2) {
        makeSelectionVisible ();

        if ((event.getModifiers () & InputEvent.BUTTON2_MASK) != 0 ||
            event.isAltDown () || event.isControlDown ()) {
          if (Options.isStandAlone ()) {
            new FeatureEdit (clicked_feature, getSelection ()).show ();
          }
        }
      }
    }
  }

  /**
   *  The main paint function for the canvas.  An off screen image used for
   *  double buffering when drawing the canvas.
   *  @param g The Graphics object of the canvas.
   **/
  protected void paintCanvas (Graphics g) {
    if (!isVisible ()) {
      return;
    }

    if (selection_changed_flag) {
      selection_changed_flag = false;

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

      if (selected_features.size () > 0) {
        // set to true if any of the selected features is visible
        boolean a_selected_feature_is_visible = false;

        final int first_line_in_view = scrollbar.getValue ();

        final int feature_count = getEntryGroup ().getAllFeaturesCount ();

        for (int i = first_line_in_view ;
             i < feature_count && i < first_line_in_view + linesInView () ;
             ++i) {
          final Feature this_feature = getEntryGroup ().featureAt (i);
          if (selected_features.contains (this_feature)) {
            a_selected_feature_is_visible = true;
            break;
          }
        }

        if (!a_selected_feature_is_visible) {
          // make the first selected feature visible
          final Feature first_selected_feature =
            selected_features.elementAt (0);

          final int index_of_first_selected_feature =
            getEntryGroup ().indexOf (first_selected_feature);

          if (index_of_first_selected_feature < scrollbar.getValue () ||
              index_of_first_selected_feature >=
              scrollbar.getValue () + linesInView ()) {

            scrollbar.setValue (index_of_first_selected_feature);
          }
        }
      }
    }

    final int canvas_width = getCanvas ().getSize ().width;
    final int canvas_height = getCanvas ().getSize ().height;

    g.setColor (background_colour);

    g.fillRect (0, 0, canvas_width, canvas_height);

    g.setColor (Color.black);

    if (getEntryGroup ().size () != 0) {
      final int lines_in_view = linesInView ();

      final int first_index_in_view = scrollbar.getValue ();

      final int feature_count = getEntryGroup ().getAllFeaturesCount ();

      final int last_index_in_view;

      if (lines_in_view < feature_count - first_index_in_view) {
        last_index_in_view = first_index_in_view + lines_in_view;
      } else {
        last_index_in_view = feature_count - 1;
      }

      final FeatureVector features_in_view =
        getEntryGroup ().getFeaturesInIndexRange (first_index_in_view,
                                                  last_index_in_view);

      for (int i = 0 ;
           i <= last_index_in_view - first_index_in_view ;
           ++i) {
        drawFeatureLine (g, features_in_view.elementAt (i), i);
      }
    }
  }

  /**
   *  Return the number of visible text lines on canvas.
   **/
  private int linesInView () {
    return getCanvas ().getSize ().height / getFontHeight ();
  }

  /**
   *  Update the scrollbar and call refresh ().
   **/
  private void refresh () {
    scrollbar.setMaximum (getEntryGroup ().getAllFeaturesCount () +
                          linesInView () - 1);
    final int lines_in_view = linesInView ();
    scrollbar.setBlockIncrement (lines_in_view > 0 ? lines_in_view : 1);
    scrollbar.setUnitIncrement (1);
    scrollbar.setVisibleAmount (linesInView ());
    repaintCanvas ();
  }

  /**
   *  Draw the given Feature at the given line of the list, taking the
   *  selection into account.
   **/
  private void drawFeatureLine (final Graphics g,
                                final Feature feature,
                                final int line) {
    final int y_pos = line * getFontHeight ();

    // the width of the coloured blob at the left of the text
    final int BOX_WIDTH = getFontHeight ();

    final Color feature_colour = feature.getColour ();

    if (feature_colour == null) {
      // default colour is white
      g.setColor (Color.white);
    } else {
      g.setColor (feature_colour);
    }
    g.fillRect (1, y_pos + 1, BOX_WIDTH, getFontHeight () - 1);

    if (getSelection ().contains (feature)) {
      // draw in reverse
      g.setColor (Color.black);
      g.fillRect (BOX_WIDTH + 4, y_pos,
                  getCanvas ().getSize ().width, getFontHeight ());
      g.setXORMode (Color.black);
      g.setColor (background_colour);
    } else {
      g.setColor (Color.black);
    }

    g.drawString (makeFeatureString (feature), BOX_WIDTH + 5,
                  y_pos + getFontBaseLine ());

    g.setPaintMode ();
  }

  /**
   *  Call repaint () on the canvas object.
   **/
  private void repaintCanvas () {
    getCanvas ().repaint ();
  }

  /**
   *  Return the list index of a feature.
   **/
  private int indexOf (Feature feature) {
    return getEntryGroup ().indexOf (feature);
  }

  /**
   *  Return a String object suitable for displaying in the list of features.
   **/
  private String makeFeatureString (final Feature feature) {
    String key_string;

    if (show_gene_names) {
      key_string = feature.getIDString ();

      if (key_string.length () > 15) {
        key_string = key_string.substring (0, 15);
      }
    } else {
      key_string = feature.getKey ().toString ();
    }

    final Marker low_marker = feature.getFirstBaseMarker ();
    final Marker high_marker = feature.getLastBaseMarker ();
    String note_or_product = "";

    if (show_products) {
      final String product_string = feature.getProductString ();

      if (product_string == null) {
        if (feature.isCDS ()) {
          note_or_product = "[no /product]";
        } else {
          note_or_product = "";
        }
      } else {
        note_or_product = product_string;
      }
    } else {
      if (feature.getNote () != null) {
        // include only the start of the note
        note_or_product = feature.getNote ();

        if (note_or_product.length () > 160) {
          note_or_product = note_or_product.substring (0, 160);
        }
      }
    }

    final String low_pos;
    final String high_pos;

    if (low_marker == null || high_marker == null) {
      low_pos = "unknown";
      high_pos = "unknown";
    } else {
      if (low_marker.getRawPosition () < high_marker.getRawPosition ()) {
        low_pos = String.valueOf (low_marker.getRawPosition ());
        high_pos = String.valueOf (high_marker.getRawPosition ());
      } else {
        low_pos = String.valueOf (high_marker.getRawPosition ());
        high_pos = String.valueOf (low_marker.getRawPosition ());
      }
    }

    StringBuffer new_list_line = new StringBuffer ();

    // add some spaces
    new_list_line.append (padStringWithSpaces (key_string, 15));

    for (int i = 0 ; i < 8 - low_pos.length () ; ++i) {
      new_list_line.append (' ');
    }
    new_list_line.append (low_pos);

    for (int i = 0 ; i < 8 - high_pos.length () ; ++i) {
      new_list_line.append (' ');
    }
    new_list_line.append (high_pos);

    new_list_line.append (" ");

    if (feature.isForwardFeature ()) {
      new_list_line.append ("   ");
    } else {
      new_list_line.append ("c  ");
    }

    if (show_correlation_scores) {
      if (feature.isCDS ()) {
        new_list_line.append (getScoresString (feature));
        new_list_line.append ("  ");
      } else {
        new_list_line.append ("                         ");
      }
    }

    new_list_line.append (note_or_product);

    return new_list_line.toString ();
  }

  /**
   *  Return a String containing the correlation scores.
   **/
  public String getScoresString (final Feature feature) {
    final int base_total = feature.getTranslationBases ().length ();

    final int c_total = feature.getBaseCount (Bases.getIndexOfBase ('c'));
    final int g_total = feature.getBaseCount (Bases.getIndexOfBase ('g'));

    final int g1_count =
      feature.getPositionalBaseCount (0, Bases.getIndexOfBase ('g'));

    final int c3_count =
      feature.getPositionalBaseCount (2, Bases.getIndexOfBase ('c'));
    final int g3_count =
      feature.getPositionalBaseCount (2, Bases.getIndexOfBase ('g'));

    final double c3_score = 100.0 * (3 * c3_count - c_total) / c_total;
    final double g1_score = 100.0 * (3 * g1_count - g_total) / g_total;
    final double g3_score = 100.0 * (3 * g3_count - g_total) / g_total;

    final double cor1_2_score = feature.get12CorrelationScore ();

    final NumberFormat number_format = NumberFormat.getNumberInstance ();

    number_format.setMaximumFractionDigits (1);
    number_format.setMinimumFractionDigits (1);

    final String cor1_2_score_string = number_format.format (cor1_2_score);
    final String c3_score_string;
    final String g1_score_string;
    final String g3_score_string;


    if (c_total == 0) {
      c3_score_string = "ALL";
    } else {
      c3_score_string = number_format.format (c3_score);
    }

    if (g_total == 0) {
      g1_score_string = "ALL";
    } else {
      g1_score_string = number_format.format (g1_score);
    }

    if (g_total == 0) {
      g3_score_string = "ALL";
    } else {
      g3_score_string = number_format.format (g3_score);
    }

    return
      padStringWithSpaces (cor1_2_score_string, 5) + " " +
      padStringWithSpaces (c3_score_string, 5) + " " +
      padStringWithSpaces (g1_score_string, 5) + " " +
      padStringWithSpaces (g3_score_string, 5);
  }

  /**
   *  Return the given string padded with spaces to the given width.
   **/
  private String padStringWithSpaces (final String string, final int width) {
    if (string.length () == width) {
      return string;
    }

    final StringBuffer buffer = new StringBuffer (string);

    for (int i = 0 ; i < width - string.length () ; ++i) {
      buffer.append (' ');
    }

    return buffer.toString ();
  }

  /**
   *  This variable is true if correlation scores should be shown in the list.
   **/
  private boolean show_correlation_scores = false;

  /**
   *  The Scrollbar for this FeatureList object.
   **/
  private Scrollbar scrollbar = null;

  /**
   *  The index of the first visible feature in the list.
   **/
  private int first_index;

  /**
   *  This is set to true by selectionChanged () and used by paintCanvas ().
   **/
  private boolean selection_changed_flag = false;

  /**
   *  The colour used to draw the background.
   **/
  private Color background_colour = new Color (240, 240, 240);

  /**
   *  If true this component will show Feature.getIDString () (ie /gene or
   *  /label) instead of the key.
   **/
  private boolean show_gene_names = false;

  /**
   *  If true this component will show the /product qualifier instead of the
   *  /note field.
   **/
  private boolean show_products = false;
}
