/* EntryEdit.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/EntryEdit.java,v 1.151 2000/09/18 09:53:48 kmr Exp $
 */

package diana.components;

import AppGlobal;

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

import uk.ac.sanger.pathogens.*;
import uk.ac.sanger.pathogens.embl.DocumentEntryFactory;
import uk.ac.sanger.pathogens.embl.InvalidKeyException;
import uk.ac.sanger.pathogens.embl.EntryInformationException;
import uk.ac.sanger.pathogens.embl.EntryInformation;
import uk.ac.sanger.pathogens.embl.SimpleEntryInformation;

import java.awt.*;
import java.awt.event.*;
import java.util.Vector;
import java.util.EventObject;
import java.io.IOException;
import java.io.File;
import java.io.FileReader;

/**
 *  Each object of this class is used to edit an EntryGroup object.
 *
 *  @author Kim Rutherford
 *  @version $Id: EntryEdit.java,v 1.151 2000/09/18 09:53:48 kmr Exp $
 *
 */

public class EntryEdit extends Frame
    implements FeatureDisplayOwner,
               EntryGroupChangeListener, EntryChangeListener {
  /**
   *  Create a new EntryEdit object and Frame.
   *  @param entry_group The EntryGroup object that this component is editing.
   */
  public EntryEdit (final EntryGroup entry_group) {
    super ("Artemis Entry Edit");

    this.entry_group = entry_group;

    // XXX add a InputStreamProgressListener
    this.entry_sources = AppGlobal.getEntrySources (this, null);

    this.goto_event_source = new SimpleGotoEventSource (getEntryGroup ());

    selection = new Selection (null);

    getEntryGroup ().addFeatureChangeListener (selection);
    getEntryGroup ().addEntryChangeListener (selection);

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

    if (getEntryGroup ().getDefaultEntry () != null) {
      final String name = getEntryGroup ().getDefaultEntry ().getName ();

      if (name != null) {
        setTitle ("Artemis Entry Edit: " + name);
      }
    }

    final int font_height;
    final int font_base_line;

    final Font default_font = getDefaultFont ();

    setFont (default_font);

    if (default_font != null) {
      FontMetrics fm = getFontMetrics (default_font);
      font_height = fm.getHeight ();
      font_base_line = fm.getMaxAscent ();
    }
    else {
      font_height = -1;
      font_base_line = -1;
    }

    addWindowListener (new WindowAdapter () {
      public void windowClosing (WindowEvent event) {
        if (getEntryGroup ().hasUnsavedChanges ()) {
          final YesNoDialog yes_no_dialog =
            new YesNoDialog (EntryEdit.this,
                             "there are unsaved changes - really close?");

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

        entryEditFinished ();
      }
    });

    GridBagLayout gridbag = new GridBagLayout();
    setLayout (gridbag);

    menu_bar.setFont (default_font);

    GridBagConstraints c = new GridBagConstraints();

    c.gridwidth  = GridBagConstraints.REMAINDER;
    c.fill       = GridBagConstraints.HORIZONTAL;
    c.anchor     = GridBagConstraints.NORTH;
    c.gridheight = 1;
    c.weightx    = 1;
    c.weighty    = 0;

    // Create and add the stats_label.
    selection_info =
      new SelectionInfoDisplay (getEntryGroup (), getSelection ());
    gridbag.setConstraints (selection_info, c);
    add (selection_info);

    group_display = new EntryGroupDisplay (this);

    gridbag.setConstraints (group_display, c);
    add (group_display);

    group_display.setVisible (true);
    
    base_plot_group =
      new BasePlotGroup (getEntryGroup (), this, getSelection (),
                         getGotoEventSource ());
    gridbag.setConstraints (base_plot_group, c);
    add (base_plot_group);

    base_plot_group.setVisible (true);

    feature_display =
      new FeatureDisplay (getEntryGroup (), getSelection (),
                          getGotoEventSource (), this);

    feature_display.addDisplayAdjustmentListener (base_plot_group);

    gridbag.setConstraints (feature_display, c);
    add (feature_display);

    feature_display.setVisible (true);

    base_display =
      new FeatureDisplay (getEntryGroup (), getSelection (),
                          getGotoEventSource (), this);
    base_display.setShowLabels (false);
    base_display.setScaleFactor (0);
    gridbag.setConstraints (base_display, c);
    add (base_display);

    base_display.setVisible (true);

    feature_list =
      new FeatureList (getEntryGroup (), getSelection (),
                       getGotoEventSource ());
    feature_list.setFont (default_font);
    c.fill = GridBagConstraints.BOTH;
    c.weighty = 1;
    gridbag.setConstraints (feature_list, c);

    final String list_option_string =
      Options.getOptions ().getProperty ("show_list");

    final boolean list_option_value =
      Options.getOptions ().getPropertyTruthValue ("show_list");

    if (list_option_string == null || list_option_value) {
      feature_list.setVisible (true);
    } else {
      feature_list.setVisible (false);
    }

    add (feature_list);

    makeMenus ();

    pack ();

    setSize (900,750);

    final Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
    setLocation (new Point ((screen.width - getSize ().width) / 2,
                            (screen.height - getSize ().height) / 2));
  }

  /**
   *  Redraw this component.  This method is public so that other classes can
   *  force an update if, for example, the options files is re-read.
   **/
  public void redisplay () {
    feature_display.redisplay ();
    base_display.redisplay ();
  }

  /**
   *  This method sends an GotoEvent to all the GotoEvent listeners that will
   *  make the start of the first selected Feature or FeatureSegment visible.
   **/
  public void makeSelectionVisible () {
    final Marker first_base = getSelection ().getStartBaseOfSelection ();

    final GotoEvent new_event = new GotoEvent (this, first_base);

    getGotoEventSource ().sendGotoEvent (new_event);
  }

  /**
   *  Return an object that implements the GotoEventSource interface, and is
   *  the controlling object for Goto events associated with this object.
   **/
  public GotoEventSource getGotoEventSource () {
    return goto_event_source;
  }

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

  /**
   *  Returns a Selection object containing the selected features/exons.
   **/
  public Selection getSelection () {
    return selection;
  }

  /**
   *  Implementation of the EntryGroupChangeListener interface.  We listen to
   *  EntryGroupChange events so that we can update the File menu when the
   *  EntryGroup changes.
   **/
  public void entryGroupChanged (final EntryGroupChangeEvent event) {
    switch (event.getType ()) {
    case EntryGroupChangeEvent.ENTRY_ADDED:
    case EntryGroupChangeEvent.ENTRY_DELETED:
      makeFileMenu ();
      break;
    }
  }

  /**
   *  Implementation of the EntryChangeListener interface.  We listen to
   *  EntryChange events so that we can update the File menu when an Entry
   *  changes.
   **/
  public void entryChanged (final EntryChangeEvent event) {
    if (event.getType () == EntryChangeEvent.NAME_CHANGED) {
      makeFileMenu ();
    }
  }

  /* private members: */

  /**
   *  This method arranges for the EntryEdit Frame to go away.  This EntryEdit
   *  object was created by the main program, so the main program must be the
   *  one to delete us.
   **/
  private void entryEditFinished () {
    setVisible (false);
    dispose ();
  }


  /**
   *  Write the default Entry in the EntryGroup to the file it came from.
   **/
  private void saveDefaultEntry () {
    if (getEntryGroup ().getDefaultEntry () == null) {
      new MessageDialog (EntryEdit.this, "There is no default entry");
    } else {
      saveEntry (entry_group.getDefaultEntry (), true, false, true,
                 DocumentEntryFactory.ANY_FORMAT);
    }
  }

  /**
   *  Save the given entry, prompting for a file name if necessary.
   *  @param include_diana_extensions If true then the any diana additions to
   *    the embl file format will be included in the output, otherwise they
   *    will be removed.  Also possible problems that would cause an entry to
   *    bounce from the EMBL submission process will be flagged if this is
   *    true.
   *  @param ask_for_name If true then always prompt for a new filename,
   *    otherwise prompt only when the entry name is not set.
   *  @param keep_new_name If ask_for_name is true a file will be written with
   *    the new name the user selects - if keep_new_name is true as well, then
   *    the entry will have it's name set to the new name, otherwise it will
   *    be used for this save and then discarded.
   *  @param destination_type Should be one of EMBL_FORMAT, GENBANK_FORMAT or
   *    ANY_FORMAT.  If ANY_FORMAT then the Entry will be saved in the
   *    same format it was created, otherwise it will be saved in the given
   *    format.
   **/
  void saveEntry (final Entry entry,
                  final boolean include_diana_extensions,
                  final boolean ask_for_name,
                  final boolean keep_new_name,
                  final int destination_type) {

    if (!include_diana_extensions) {
      if (displaySaveWarnings (entry)) {
        return;
      }
    }

    if (destination_type != DocumentEntryFactory.ANY_FORMAT &&
        entry.getHeaderText () != null) {
      final YesNoDialog yes_no_dialog =
        new YesNoDialog (this, "header section will be lost.  continue?");

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

    try {
      if (ask_for_name || entry.getName () == null) {
        // prompt for a name
        final FileDialog dialog =
          new FileDialog (this,
                          "Select an file name...",
                          FileDialog.SAVE);

        dialog.setVisible (true);

        if (dialog.getFile () == null || dialog.getFile ().length () == 0) {
          new MessageDialog (this, "no file name given for save");
          return;
        } else {
          final String file_name = dialog.getDirectory () +
            java.io.File.separator + dialog.getFile ();

          if (new File (file_name).exists ()) {
            final YesNoDialog yes_no_dialog =
              new YesNoDialog (this, "this file exists: " + file_name +
                               " overwrite it?");

            if (yes_no_dialog.getResult ()) {
              // yes - continue
            } else {
              // no
              return;
            }
          }

          final MessageDialog message;

          message = new MessageDialog (this,
                                       "saving to " + file_name + " ...",
                                       false);

          try {
            if (include_diana_extensions) {
              entry.save (file_name, destination_type, false);
            } else {
              entry.saveStandardOnly (file_name, destination_type, true);
            }
          } catch (EntryInformationException e) {
            final YesNoDialog yes_no_dialog =
              new YesNoDialog (this, "destination format can't handle all " +
                               "keys/qualifiers continue?");
            if (yes_no_dialog.getResult ()) {
              try {
                if (include_diana_extensions) {
                  entry.save (file_name, destination_type, true);
                } else {
                  entry.saveStandardOnly (file_name, destination_type, true);
                }
              } catch (EntryInformationException e2) {
                throw new Error ("internal error - unexpected exception: "+ e);
              }
            } else {
              return;
            }
          }

          if (keep_new_name) {
            entry.setName (file_name);
          }

          if (message != null) {
            message.dispose ();
          }
        }
      } else {
        final MessageDialog message =
          new MessageDialog (this,
                             "saving to " + entry.getName () + " ...",
                             false);

        if (include_diana_extensions) {
          entry.save (destination_type);
        } else {
          entry.saveStandardOnly (destination_type);
        }

        message.dispose ();
      }
    } catch (ReadOnlyException e) {
      new MessageDialog (this, "this entry is read only");
      return;
    } catch (IOException e) {
      new MessageDialog (this, "error while saving: " + e);
      return;
    } catch (EntryInformationException e) {
      new MessageDialog (this, "error while saving: " + e);
      return;
    }
  }

  /**
   *  Save the changes to all the Entry objects in the entry_group back to
   *  where the they came from.
   **/
  public void saveAllEntries () {
    for (int entry_index = 0 ;
         entry_index < entry_group.size () ;
         ++entry_index) {
      saveEntry (entry_group.elementAt (entry_index), true, false, true,
                 DocumentEntryFactory.ANY_FORMAT);
    }
  }

  /**
   *  Check the given Entry for invalid EMBL features (such as CDS features
   *  without a stop codon) then display a FeatureListFrame list the problem
   *  features.
   *  @return true if and only if the save should be aborted.
   **/
  private boolean displaySaveWarnings (final Entry entry) {
    final FeatureVector invalid_starts = entry.checkFeatureStartCodons ();
    final FeatureVector invalid_stops = entry.checkFeatureStopCodons ();
    final FeatureVector invalid_keys = entry.checkForNonEMBLKeys ();
    final FeatureVector duplicate_features = entry.checkForEMBLDuplicates ();
    final FeatureVector overlapping_cds_features =
      entry.checkForOverlappingCDSs ();
    final FeatureVector missing_qualifier_features =
      entry.checkForMissingQualifiers ();

    // this predicate will filter out those features that aren't in the
    // entry we are trying to save
    final FeaturePredicate predicate =
      new FeaturePredicate () {
        public boolean testPredicate (final Feature feature) {
          if (feature.getEntry () == entry) {
            return true;
          } else {
            return false;
          }
        }
      };

    final FilteredEntryGroup filtered_entry_group =
      new FilteredEntryGroup (getEntryGroup (), predicate);

    if (invalid_starts.size () + invalid_stops.size () +
        invalid_keys.size () + duplicate_features.size () +
        overlapping_cds_features.size () > 0) {

      final YesNoDialog yes_no_dialog =
        new YesNoDialog (this,
                         "warning: some features may have problems. " +
                         "continue with save?");

      if (!yes_no_dialog.getResult ()) {
        getSelection ().clear ();

        if (invalid_starts.size () > 0) {
          getSelection ().add (invalid_starts);

          ViewMenu.showBadStartCodons (this,
                                       getSelection (),
                                       filtered_entry_group,
                                       getGotoEventSource ());
        }

        if (invalid_stops.size () > 0) {
          getSelection ().add (invalid_stops);

          ViewMenu.showBadStopCodons (this,
                                      getSelection (),
                                      filtered_entry_group,
                                      getGotoEventSource ());
        }

        if (invalid_keys.size () > 0) {
          getSelection ().add (invalid_keys);

          ViewMenu.showNonEMBLKeys (this,
                                    getSelection (),
                                    filtered_entry_group,
                                    getGotoEventSource ());
        }

        if (duplicate_features.size () > 0) {
          getSelection ().add (duplicate_features);

          ViewMenu.showDuplicatedFeatures (this,
                                           getSelection (),
                                           filtered_entry_group,
                                           getGotoEventSource ());
        }

        if (overlapping_cds_features.size () > 0) {
          getSelection ().add (overlapping_cds_features);

          ViewMenu.showOverlappingCDSs (this,
                                        getSelection (),
                                        filtered_entry_group,
                                        getGotoEventSource ());
        }

        if (missing_qualifier_features.size () > 0) {
          getSelection ().add (missing_qualifier_features);

          ViewMenu.showMissingQualifierFeatures (this,
                                                 getSelection (),
                                                 filtered_entry_group,
                                                 getGotoEventSource ());
        }

        return true;
      }
    }

    return false;
  }

  /**
   *  Copy the features from source_entry into destination_entry.
   **/
  public void copyFeatures (final Entry source_entry,
                            final Entry destination_entry)
      throws OutOfRangeException {
    final FeatureVector new_features = source_entry.getAllFeatures ();

    // features that can't be copied (because of EntryInformationExceptions)
    final FeatureVector lost_features = new FeatureVector ();

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

      try {
        this_feature.removeFromEntry ();
      } catch (ReadOnlyException e) {
        throw new Error ("internal error - unexpected exception: " + e);
      }

      try {
        destination_entry.add (this_feature, false);
      } catch (EntryInformationException e) {
        if (e instanceof InvalidKeyException) {
          // the feature can't be read
          lost_features.add (this_feature);
        } else {
          try {
            // add it anyway (qualifiers will be lost)
            destination_entry.add (this_feature, true);
          } catch (EntryInformationException e2) {
            throw new Error ("internal error - unexpected exception: " + e2);
          } catch (ReadOnlyException e2) {
            new MessageDialog (EntryEdit.this, "the entry is read only");
          }
        }
      } catch (ReadOnlyException e) {
        new MessageDialog (EntryEdit.this, "the entry is read only");
      }
    }

    if (lost_features.size () > 0) {
      final StringBuffer buffer =
        new StringBuffer ("some features types could not be read: ");

      for (int i = 0 ; i < lost_features.size () ; ++i) {
        buffer.append (lost_features.elementAt (0).getIDString ());
        if (i == 2) {
          buffer.append (", ...");
          break;
        }
        if (i < lost_features.size () - 1) {
          buffer.append (", ");
        }
      }

      new MessageDialog (EntryEdit.this, buffer.toString ());
    }
  }

  /**
   *  This method will call pack () and then move the Frame to the centre of
   *  the screen.  (Implementation of the FeatureDisplayOwner interface).
   **/
  public void packme () {
    pack ();

    final Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();

    final int x_position = (screen.width - getSize ().width) / 2;
    int y_position = (screen.height - getSize ().height) / 2;

    if (y_position < 10) {
      y_position = 10;
    }

    setLocation (new Point (x_position, y_position));
  }

  /**
   *  Make and add the menus for this component.
   **/
  private void makeMenus () {
    final Font default_font = getDefaultFont ();

    final BaseAlgorithm [] algorithms = base_plot_group.getPlotAlgorithms ();

    final CheckboxMenuItem [] algorithm_menu_items =
      new CheckboxMenuItem [algorithms.length];

    setMenuBar (menu_bar);

    makeFileMenu ();

    menu_bar.add (file_menu);

    final CheckboxMenuItem show_entry_buttons_item =
      new CheckboxMenuItem ("Show Entry Buttons");
    show_entry_buttons_item.setState (true);
    show_entry_buttons_item.addItemListener (new ItemListener () {
      public void itemStateChanged(ItemEvent event) {
        group_display.setVisible (show_entry_buttons_item.getState ());
        packme ();
      }
    });
    display_menu.add (show_entry_buttons_item);

    final CheckboxMenuItem show_base_display_item =
      new CheckboxMenuItem ("Show Base View");
    show_base_display_item.setState (base_display.isVisible ());
    show_base_display_item.addItemListener (new ItemListener () {
      public void itemStateChanged(ItemEvent event) {
        base_display.setVisible (show_base_display_item.getState ());
        packme ();
      }
    });
    display_menu.add (show_base_display_item);

    final CheckboxMenuItem show_feature_list_item =
      new CheckboxMenuItem ("Show Feature List");
    show_feature_list_item.setState (feature_list.isVisible ());
    show_feature_list_item.addItemListener (new ItemListener () {
      public void itemStateChanged(ItemEvent event) {
        feature_list.setVisible (show_feature_list_item.getState ());
        packme ();
      }
    });
    display_menu.add (show_feature_list_item);


    display_menu.addSeparator ();

    final MenuItem hide_all_graphs_item = new MenuItem ("Hide All Graphs");
    hide_all_graphs_item.addActionListener (new ActionListener () {
      public void actionPerformed (ActionEvent event) {
        for (int i = 0 ; i < algorithms.length ; ++i) {
          final BaseAlgorithm this_algorithm = algorithms[i];

          base_plot_group.setVisibleByAlgorithm (this_algorithm, false);

          final CheckboxMenuItem this_menu_item = algorithm_menu_items[i];

          this_menu_item.setState (false);
        }
        packme ();
      }
    });
    display_menu.add (hide_all_graphs_item);

    display_menu.addSeparator ();

    if (Options.isStandAlone ()) {
      final MenuItem usage_plot_item = new MenuItem ("Add Usage Plots ...");
      usage_plot_item.addActionListener (new ActionListener () {
        public void actionPerformed (ActionEvent event) {
          addUsagePlot ();
        }
      });
      display_menu.add (usage_plot_item);

      if (Options.getOptions ().getProperty ("sanger_options") != null) {
        final MenuItem user_plot_item = new MenuItem ("Add User Plot ...");
        user_plot_item.addActionListener (new ActionListener () {
          public void actionPerformed (ActionEvent event) {
            addUserPlot ();
          }
        });

        display_menu.add (user_plot_item);
      }

      display_menu.addSeparator ();
    }

    for (int i = 0 ; i < algorithms.length ; ++i) {
      final BaseAlgorithm this_algorithm = algorithms[i];

      algorithm_menu_items[i] = addDisplayMenuAlgorithm (this_algorithm);
    }

    entry_group_menu = new EntryGroupMenu (this, getEntryGroup ());
    menu_bar.add (entry_group_menu);

    select_menu =
      new SelectMenu (this, getSelection (),
                      getGotoEventSource (), getEntryGroup ());
    menu_bar.add (select_menu);

    view_menu = new ViewMenu (this, getSelection (),
                              getGotoEventSource (), getEntryGroup ());
    menu_bar.add (view_menu);

    goto_menu = new GotoMenu (this, getSelection (),
                              getGotoEventSource (), getEntryGroup ());
    menu_bar.add (goto_menu);

    if (Options.isStandAlone ()) {
      edit_menu = new EditMenu (this, getSelection (),
                                getGotoEventSource (), getEntryGroup ());
      menu_bar.add (edit_menu);

      add_menu = new AddMenu (this, getSelection (), getEntryGroup ());
      menu_bar.add (add_menu);

      write_menu = new WriteMenu (this, getSelection (), getEntryGroup ());
      menu_bar.add (write_menu);

      if (Options.isUnixHost ()) {
        run_menu = new RunMenu (this, getSelection ());
        menu_bar.add (run_menu);
      }
    }

    menu_bar.add (display_menu);

    if (Options.getOptions ().getProperty ("codon_usage_file") != null) {
      final String codon_usage_file_name =
        Options.getOptions ().getProperty ("codon_usage_file");

      try {
        addUsagePlot (codon_usage_file_name, true, false);
        addUsagePlot (codon_usage_file_name, false, false);

        packme ();
      } catch (IOException e) {
        new MessageDialog (this, "error while reading usage data: " + e);
      }
    }

    if (Options.getOptions ().getPropertyTruthValue ("sanger_options") &&
        Options.getOptions ().getProperty ("black_belt_mode") != null) {
      makeBlackBeltToggleMenu ();
    }
  }

  /**
   *  Make a menu for toggling black_belt_mode.
   **/
  private void makeBlackBeltToggleMenu () {
    final Menu menu;

    if (Options.getOptions ().isNoddyMode ()) {
      menu = new Menu ("(Noddy Mode)");
    } else {
      menu = new Menu ("(Black Belt Mode)");
    }

    menu_bar.add (menu);

    final MenuItem item = new MenuItem ("Toggle");
    item.addActionListener (new ActionListener () {
      public void actionPerformed (ActionEvent event) {
        menu_bar.remove (menu);
        if (Options.isBlackBeltMode ()) {
          Options.getOptions ().put ("black_belt_mode", "false");
        } else {
          Options.getOptions ().put ("black_belt_mode", "true");
        }
        makeBlackBeltToggleMenu ();
      }
    });

    menu.add (item);
  }

  /**
   *  Make a new File menureplacing the current one (if any).
   **/
  private void makeFileMenu () {
    file_menu.removeAll ();

    if (Options.isStandAlone ()) {
      // only the standalone version can save or read

      EntrySource filesystem_entry_source = null;

      for (int source_index = 0 ;
           source_index < entry_sources.size () ;
           ++source_index) {
        final EntrySource this_source =
          entry_sources.elementAt (source_index);

        if (this_source.isFullEntrySource ()) {
          continue;
        }

        if (this_source.getSourceName ().equals ("Filesystem")) {
          filesystem_entry_source = this_source;
        }

        String entry_source_name = this_source.getSourceName ();

        String menu_item_name = null;

        if (entry_source_name.equals ("Filesystem")) {
          menu_item_name = "Read An Entry ...";
        } else {
          menu_item_name = "Read An Entry From " + entry_source_name + " ...";
        }

        final MenuItem read_entry = new MenuItem (menu_item_name);

        read_entry.addActionListener (new ActionListener () {
          public void actionPerformed (ActionEvent event) {
            try {
              final Entry new_entry =
                this_source.getEntry (entry_group.getBases ());

              if (new_entry != null) {
                getEntryGroup ().add (new_entry);
              }
            } catch (OutOfRangeException e) {
              new MessageDialog (EntryEdit.this,
                                 "read failed: one of the features in the" +
                                 " entry has an out of range location: " +
                                 e.getMessage ());
              return;
            } catch (IOException e) {
              new MessageDialog (EntryEdit.this,
                                 "read failed due to an IO error: " +
                                 e.getMessage ());
              return;
            }
          }
        });

        file_menu.add (read_entry);
      }

      Menu read_features_menu = null;

      if (filesystem_entry_source != null &&
          entry_group != null && entry_group.size () > 0) {
        read_features_menu = new Menu ("Read Features Into");

        file_menu.add (read_features_menu);
      }

      file_menu.addSeparator ();

      final MenuItem save_default =
        new MenuItem ("Save Default Entry", new MenuShortcut (KeyEvent.VK_S));
      save_default.addActionListener (new ActionListener () {
        public void actionPerformed (ActionEvent event) {
          saveDefaultEntry ();
        }
      });

      file_menu.add (save_default);

      if (entry_group == null || entry_group.size () == 0) {
//        read_features_menu.add (new MenuItem ("(No Entries Currently)"));
//        save_entry_menu.add (new MenuItem ("(No Entries Currently)"));
//        save_as_menu.add (new MenuItem ("(No Entries Currently)"));
      } else {

        final Menu save_entry_menu = new Menu ("Save An Entry");

        final Menu save_as_menu = new Menu ("Save An Entry As");

        final Menu save_as = new Menu ("New File");
        final Menu save_as_embl = new Menu ("EMBL Format");
        final Menu save_as_genbank = new Menu ("GENBANK Format");
        final Menu save_as_gff = new Menu ("GFF Format");
        final Menu save_embl_only = new Menu ("EMBL Submission Format");

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

          String entry_name = this_entry.getName ();

          if (entry_name == null) {
            entry_name = "no name";
          }

          final ActionListener save_entry_listener =
            new SaveEntryActionListener (this, this_entry);

          final MenuItem save_entry_item = new MenuItem (entry_name);

          save_entry_item.addActionListener (save_entry_listener);

          final ActionListener save_as_listener =
            new SaveEntryAsActionListener (this, this_entry);

          final MenuItem save_as_item = new MenuItem (entry_name);

          save_as_item.addActionListener (save_as_listener);

          final ActionListener save_as_embl_listener =
            new SaveEntryAsEMBLActionListener (this, this_entry);

          final MenuItem save_as_embl_item = new MenuItem (entry_name);

          save_as_embl_item.addActionListener (save_as_embl_listener);

          final ActionListener save_as_genbank_listener =
            new SaveEntryAsGenbankActionListener (this, this_entry);

          final MenuItem save_as_genbank_item = new MenuItem (entry_name);

          save_as_genbank_item.addActionListener (save_as_genbank_listener);

          final ActionListener save_as_gff_listener =
            new SaveEntryAsGFFActionListener (this, this_entry);

          final MenuItem save_as_gff_item = new MenuItem (entry_name);

          save_as_gff_item.addActionListener (save_as_gff_listener);

          final ActionListener save_embl_only_listener =
            new SaveEntryAsSubmissionActionListener (this, this_entry);

          final MenuItem save_embl_only_item = new MenuItem (entry_name);

          save_embl_only_item.addActionListener (save_embl_only_listener);

          if (read_features_menu != null) {
            final ActionListener read_into_listener =
              new ReadFeaturesActionListener (this, filesystem_entry_source,
                                              this_entry);

            final MenuItem read_into_item = new MenuItem (entry_name);

            read_into_item.addActionListener (read_into_listener);

            read_features_menu.add (read_into_item);
          }

          save_entry_menu.add (save_entry_item);

          save_as.add (save_as_item);
          save_as_embl.add (save_as_embl_item);
          save_as_genbank.add (save_as_genbank_item);
          save_as_gff.add (save_as_gff_item);
          save_embl_only.add (save_embl_only_item);
        }

        save_as_menu.add (save_as);
        save_as_menu.add (save_as_embl);
        save_as_menu.add (save_as_genbank);
        save_as_menu.add (save_as_gff);
        save_as_menu.addSeparator ();
        save_as_menu.add (save_embl_only);

        file_menu.add (save_entry_menu);

        file_menu.add (save_as_menu);
      }

      final MenuItem save_all = new MenuItem ("Save All Entries");
      save_all.addActionListener (new ActionListener () {
        public void actionPerformed (ActionEvent event) {
          saveAllEntries ();
        }
      });

      file_menu.add (save_all);
    }


    file_menu.addSeparator ();

    final MenuItem clone_entry_edit = new MenuItem ("Clone This Window");
    clone_entry_edit.addActionListener (new ActionListener () {
      public void actionPerformed (ActionEvent event) {
        new EntryEdit (getEntryGroup ()).show ();
      }
    });

    file_menu.add (clone_entry_edit);

    file_menu.addSeparator ();


    final MenuItem close = new MenuItem ("Close");
    close.addActionListener (new ActionListener () {
      public void actionPerformed (ActionEvent event) {
        if (getEntryGroup ().hasUnsavedChanges ()) {
          final YesNoDialog yes_no_dialog =
            new YesNoDialog (EntryEdit.this,
                             "there are unsaved changes - really close?");

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

        entryEditFinished ();
      }
    });

    file_menu.add (close);
  }

  /**
   *  Add a UserDataAlgorithm to the display.
   **/
  private void addUserPlot () {
    final FileDialog dialog =
      new FileDialog (this, "Select a data file name ...", FileDialog.LOAD);

    dialog.setVisible (true);

    if (dialog.getFile () != null && dialog.getFile ().length () != 0) {
      final String data_file_name = dialog.getDirectory () + dialog.getFile ();

      final uk.ac.sanger.pathogens.Document document =
        new uk.ac.sanger.pathogens.FileDocument (new File (data_file_name));

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

      try {
        final UserDataAlgorithm new_algorithm =
          new UserDataAlgorithm (forward_strand, document);

        final BasePlot new_base_plot =
          base_plot_group.addAlgorithm (new_algorithm);

        base_plot_group.setVisibleByAlgorithm (new_algorithm, true);

        addDisplayMenuAlgorithm (new_algorithm);

        // hack to force the BasePlot to initialise
        feature_display.fireAdjustmentEvent ();

        packme ();
      } catch (IOException e) {
        new MessageDialog (this, "error while reading user data: " + e);
      }
    }
  }

  /**
   *  Ask the user for a file name, then read the codon usage data from that
   *  file, then make and add forward and a reverse BasePlot component using
   *  the data.
   **/
  private void addUsagePlot () {
    final FileDialog dialog =
      new FileDialog (this,
                      "Select a codon usage data file name ...",
                      FileDialog.LOAD);

    dialog.setVisible (true);

    if (dialog.getFile () != null && dialog.getFile ().length () != 0) {
      final String codon_usage_file_name =
        dialog.getDirectory () + java.io.File.separator + dialog.getFile ();

      try {
        final BasePlot new_forward_plot =
          addUsagePlot (codon_usage_file_name, true, true);
        final BasePlot new_reverse_plot =
          addUsagePlot (codon_usage_file_name, false, true);

        final Algorithm forward_algorithm = new_forward_plot.getAlgorithm ();
        final Algorithm reverse_algorithm = new_reverse_plot.getAlgorithm ();

        base_plot_group.setVisibleByAlgorithm (forward_algorithm, true);
        base_plot_group.setVisibleByAlgorithm (reverse_algorithm, true);

        packme ();
      } catch (IOException e) {
        new MessageDialog (this, "error while reading usage data: " + e);
      }
    }
  }


  /**
   *  Read the codon usage data from the given file, then make and add a
   *  BasePlot component using the data.
   *  @param use_forward_strand The plot will be a forward plot if and only if
   *    this is true.
   *  @param is_visible The plot will start off visible if and only if this is
   *    true.
   *  @return The BasePlot that was added.
   **/
  private BasePlot addUsagePlot (final String codon_usage_file_name,
                                 final boolean use_forward_strand,
                                 final boolean is_visible)
      throws IOException {
    final CodonUsageAlgorithm new_algorithm;

    if (use_forward_strand) {
      final Strand forward_strand =
        getEntryGroup ().getBases ().getForwardStrand ();

      final CodonUsageWeight usage_weights =
        new CodonUsageWeight (codon_usage_file_name, forward_strand);

      new_algorithm =
        new CodonUsageAlgorithm (forward_strand, usage_weights);
    } else {
      final Strand backward_strand =
        getEntryGroup ().getBases ().getReverseStrand ();

      final CodonUsageWeight usage_weights =
        new CodonUsageWeight (codon_usage_file_name, backward_strand);

      new_algorithm =
        new CodonUsageAlgorithm (backward_strand, usage_weights);
    }

    view_menu.setCodonUsageAlgorithm (new_algorithm);

    add_menu.setCodonUsageAlgorithm (new_algorithm);

    final BasePlot new_base_plot =
      base_plot_group.addAlgorithm (new_algorithm);

    base_plot_group.setVisibleByAlgorithm (new_algorithm, is_visible);

    addDisplayMenuAlgorithm (new_algorithm);

    // hack to force the BasePlot to initialise
    feature_display.fireAdjustmentEvent ();

    return new_base_plot;
  }

  /**
   *  Add a menu item for the given algorithm to the Display menu.
   *  @return The new CheckboxMenuItem
   **/
  private CheckboxMenuItem
    addDisplayMenuAlgorithm (final BaseAlgorithm algorithm) {

    final CheckboxMenuItem new_item =
      new CheckboxMenuItem (algorithm.getAlgorithmName ());

    new_item.setState (base_plot_group.basePlotIsVisible (algorithm));

    new_item.addItemListener (new ItemListener () {
      public void itemStateChanged(ItemEvent event) {
        base_plot_group.setVisibleByAlgorithm (algorithm,
                                               new_item.getState ());
        packme ();
      }
    });

    display_menu.add (new_item);

    return new_item;
  }

  /**
   *  Return the current default font (from Diana.options).
   **/
  private Font getDefaultFont () {
    return Options.getOptions ().getFont ();
  }

  /**
   *  A vector containing the Entry objects that this EntryEdit object knows
   *  about.
   **/
  private EntryGroup entry_group;

  /**
   *  Created by the constructor to pass to those objects that are interested
   *  in GotoEvents.
   **/
  private GotoEventSource goto_event_source;

  private final Panel   top_panel    = new Panel ();
  private final MenuBar menu_bar     = new MenuBar ();
  private final Menu    file_menu    = new Menu ("File");
  private Menu          select_menu;
  private ViewMenu      view_menu;
  private Menu          goto_menu;
  private Menu          entry_group_menu;
  private Menu          edit_menu;
  private AddMenu       add_menu;
  private Menu          write_menu;
//  private Menu          options_menu;
  private Menu          run_menu;
  private Menu          graph_menu;
  private final Menu    display_menu = new Menu ("Display");

  private EntryGroupDisplay    group_display;
  private FeatureDisplay       feature_display;
  private FeatureDisplay       base_display;
  private BasePlotGroup        base_plot_group;
  private SelectionInfoDisplay selection_info;

  private FeatureList feature_list;

  /**
   *  This Object contains the current selection.
   **/
  private Selection selection = null;

 /**
   *  The EntrySourceVector reference that is created in the constructor.
   **/
  final EntrySourceVector entry_sources;
}

/**
 *  This is an EntryActionListener that will call copyFeatures () when
 *  actionPerformed () is called.
 **/
class ReadFeaturesActionListener extends EntryActionListener {
  ReadFeaturesActionListener (final EntryEdit entry_edit,
                              final EntrySource entry_source,
                              final Entry destination_entry) {
    super (entry_edit, destination_entry);
    this.entry_source = entry_source;
  }

  public void actionPerformed (final ActionEvent event) {
    try {
      final Entry source_entry =
        entry_source.getEntry (entry_edit.getEntryGroup ().getBases ());
      getEntryEdit ().copyFeatures (source_entry, getEntry ());
    } catch (OutOfRangeException e) {
      new MessageDialog (entry_edit,
                         "read failed: one of the features in " +
                         "the source entry has an out of range location");
    } catch (IOException e) {
      new MessageDialog (entry_edit,
                         "read failed due to an IO error: " +
                         e.getMessage ());
    }
  }

  final EntrySource entry_source;
};

/**
 *  This is an EntryActionListener that will call saveEntry () when
 *  actionPerformed () is called.
 **/
class SaveEntryActionListener extends EntryActionListener {
  SaveEntryActionListener (final EntryEdit entry_edit,
                           final Entry entry) {
    super (entry_edit, entry);
  }

  public void actionPerformed (final ActionEvent event) {
    getEntryEdit ().saveEntry (getEntry (), true, false, true,
                               DocumentEntryFactory.ANY_FORMAT);
  }
};

/**
 *  This is an EntryActionListener that will call saveEntry () when
 *  actionPerformed () is called.
 **/
class SaveEntryAsActionListener extends EntryActionListener {
  SaveEntryAsActionListener (final EntryEdit entry_edit,
                             final Entry entry) {
    super (entry_edit, entry);
  }

  public void actionPerformed (final ActionEvent event) {
    getEntryEdit ().saveEntry (getEntry (), true, true, true,
                               DocumentEntryFactory.ANY_FORMAT);
  }
};

/**
 *  This is an EntryActionListener that will call saveEntry () when
 *  actionPerformed () is called.  The output file type will be EMBL.
 **/
class SaveEntryAsEMBLActionListener extends EntryActionListener {
  SaveEntryAsEMBLActionListener (final EntryEdit entry_edit,
                                 final Entry entry) {
    super (entry_edit, entry);
  }

  public void actionPerformed (final ActionEvent event) {
    getEntryEdit ().saveEntry (getEntry (), true, true, false,
                               DocumentEntryFactory.EMBL_FORMAT);
  }
};

/**
 *  This is an EntryActionListener that will call saveEntry () when
 *  actionPerformed () is called.  The output file type will be GENBANK.
 **/
class SaveEntryAsGenbankActionListener extends EntryActionListener {
  SaveEntryAsGenbankActionListener (final EntryEdit entry_edit,
                                    final Entry entry) {
    super (entry_edit, entry);
  }

  public void actionPerformed (final ActionEvent event) {
    getEntryEdit ().saveEntry (getEntry (), true, true, false,
                               DocumentEntryFactory.GENBANK_FORMAT);
  }
};

/**
 *  This is an EntryActionListener that will call saveEntry () when
 *  actionPerformed () is called.  The output file type will be GFF, with the
 *  sequence (if any) in FASTA format.
 **/
class SaveEntryAsGFFActionListener extends EntryActionListener {
  SaveEntryAsGFFActionListener (final EntryEdit entry_edit,
                                    final Entry entry) {
    super (entry_edit, entry);
  }

  public void actionPerformed (final ActionEvent event) {
    getEntryEdit ().saveEntry (getEntry (), true, true, false,
                               DocumentEntryFactory.GFF_FORMAT);
  }
};

/**
 *  This is an EntryActionListener that will call saveEntry () when
 *  actionPerformed () is called.
 **/
class SaveEntryAsSubmissionActionListener extends EntryActionListener {
  SaveEntryAsSubmissionActionListener (final EntryEdit entry_edit,
                                       final Entry entry) {
    super (entry_edit, entry);
  }

  public void actionPerformed (final ActionEvent event) {
    getEntryEdit ().saveEntry (getEntry (), false, true, false,
                               DocumentEntryFactory.ANY_FORMAT);
  }
};
