/* FeatureEdit.java
 *
 * created: Tue Dec  1 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/FeatureEdit.java,v 1.47 2000/08/10 10:33:32 kmr Exp $
 **/

package diana.components;

import uk.ac.sanger.pathogens.*;
import diana.*;
import diana.sequence.*;

import uk.ac.sanger.pathogens.embl.OutOfDateException;
import uk.ac.sanger.pathogens.embl.LocationParseException;
import uk.ac.sanger.pathogens.embl.InvalidRelationException;
import uk.ac.sanger.pathogens.embl.QualifierParseException;
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.Location;
import uk.ac.sanger.pathogens.embl.Qualifier;
import uk.ac.sanger.pathogens.embl.QualifierVector;
import uk.ac.sanger.pathogens.embl.EntryInformation;
import uk.ac.sanger.pathogens.embl.EntryInformationException;
import uk.ac.sanger.pathogens.embl.StreamQualifier;
import uk.ac.sanger.pathogens.embl.QualifierInfo;
import uk.ac.sanger.pathogens.embl.EmblStreamFeature;

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.Date;

/**
 *  FeatureEdit class
 *
 *  @author Kim Rutherford
 *  @version $Id: FeatureEdit.java,v 1.47 2000/08/10 10:33:32 kmr Exp $
 **/

public class FeatureEdit
    extends Frame
    implements EntryChangeListener, FeatureChangeListener {
  /**
   *  Create a new FeatureEdit object from the given Feature.
   **/
  public FeatureEdit (final Feature edit_feature,
                      final Selection selection) {
    super ("Artemis Feature Edit: " + edit_feature.getIDString ());

    this.edit_feature = edit_feature;
    this.edit_entry = edit_feature.getEntry ();
    this.selection = selection;

    final Font font = Options.getOptions ().getFont ();

    setFont (font);

    createComponents ();

    updateFromFeature ();

    edit_feature.getEntry ().addEntryChangeListener (this);
    edit_feature.addFeatureChangeListener (this);

    addWindowListener (new WindowAdapter () {
      public void windowClosing (WindowEvent event) {
        stopListening ();
        dispose ();
      }
    });

    pack ();

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

  /**
   *  Remove this object as a feature and entry change listener.
   **/
  public void stopListening () {
    getEntry ().removeEntryChangeListener (this);
    getFeature ().removeFeatureChangeListener (this);
  }

  /**
   *  Implementation of the EntryChangeListener interface.  We listen to
   *  EntryChange events so we can notify the user if of this component if the
   *  feature gets deleted.
   **/
  public void entryChanged (EntryChangeEvent event) {
//    System.out.println ("FeatureEdit.entryChanged ()");
    switch (event.getType ()) {
    case EntryChangeEvent.FEATURE_DELETED:
      if (event.getFeature () == edit_feature) {
        stopListening ();
        dispose ();
      }
      break;
    default:
      // do nothing;
      break;
    }
  }

  /**
   *  Add an ActionListener to the Cancel Button of this FeatureEdit.
   **/
  public void addCancelActionListener (final ActionListener l) {
    cancel_button.addActionListener (l);
  }

  /**
   *  Remove an ActionListener from the Cancel Button of this FeatureEdit.
   **/
  public void removeCancelActionListener (final ActionListener l) {
    cancel_button.removeActionListener (l);
  }

  /**
   *  Add an ActionListener to the Apply Button of this FeatureEdit.
   **/
  public void addApplyActionListener (final ActionListener l) {
    apply_button.addActionListener (l);
  }

  /**
   *  Remove an ActionListener from the Apply Button of this FeatureEdit.
   **/
  public void removeApplyActionListener (final ActionListener l) {
    apply_button.removeActionListener (l);
  }

  /**
   *  Implementation of the FeatureChangeListener interface.  We need to
   *  listen to feature change events from the Features in this object so that
   *  we can update the display.
   *  @param event The change event.
   **/
  public void featureChanged (FeatureChangeEvent event) {

//      System.out.println ("FeatureEdit: feature change event type: " +
//                          event.getType () + " source " +
//                          event.getSource ());

    // re-read the information from the feature
    switch (event.getType ()) {
    case FeatureChangeEvent.LOCATION_CHANGED:
      updateLocation ();
      break;
    case FeatureChangeEvent.KEY_CHANGED:
      updateKey ();
      break;
    default:
      updateFromFeature ();
      break;
    }
  }


  /**
   *  Create all the components for this FeatureEdit component.
   **/
  private void createComponents () {
    key_choice =
      new KeyChoice (getEntryInformation (),getFeature ().getKey ());

    final Panel key_and_qualifier_panel = new Panel ();

    key_and_qualifier_panel.setFont (getFont ());
    location_text.setFont (getFont ());
    add_qualifier_button.setFont (getFont ());
    ok_button.setFont (getFont ());
    cancel_button.setFont (getFont ());
    apply_button.setFont (getFont ());

    final Panel key_panel = new Panel ();
    key_panel.add (new Label ("Key:"));
    key_panel.add (key_choice);

    key_and_qualifier_panel.setLayout (new BorderLayout ());
    key_and_qualifier_panel.add (key_panel, "West");

//     final Panel button_panel = new Panel ();
//     button_panel.setLayout (new FlowLayout (FlowLayout.CENTER));
//     key_and_qualifier_panel.add (button_panel, "Center");
//     button_panel.add (new Button ("boo"));

    qualifier_choice = new QualifierChoice (getEntryInformation (),
                                            key_choice.getSelectedItem ());

    final Panel qualifier_panel = new Panel ();

    qualifier_panel.add (new Label ("Add Qualifier: "));
    qualifier_panel.add (qualifier_choice);

    key_and_qualifier_panel.add (qualifier_panel, "East");

    key_choice.addItemListener (new ItemListener () {
      public void itemStateChanged (ItemEvent _) {
        qualifier_choice.setKey (key_choice.getSelectedItem ());
      }
    });

    qualifier_choice.addItemListener (new ItemListener () {
      public void itemStateChanged (ItemEvent _) {
        final String qualifier_name = qualifier_choice.getSelectedItem ();
        final QualifierInfo qualifier_info =
          getEntryInformation ().getQualifierInfo (qualifier_name);

        if (qualifier_info == null) {
          new MessageDialog (FeatureEdit.this, "internal error: no " +
                             "qualifier info for " + qualifier_name);
        } else {
          qualifier_text_area.append ("/" + qualifier_name);

          switch (qualifier_info.getType ()) {
          case QualifierInfo.QUOTED_TEXT:
            qualifier_text_area.append ("=\"\"");
            break;

          case QualifierInfo.NO_VALUE:
          case QualifierInfo.OPTIONAL_QUOTED_TEXT:
            break;

          default:
            qualifier_text_area.append ("=");
          }

          qualifier_text_area.append ("\n");
        }
      }
    });

    final Panel middle_panel = new Panel ();
    middle_panel.setLayout (new BorderLayout ());

    final Panel lower_panel = new Panel ();
    lower_panel.setLayout (new BorderLayout ());

    final Panel outer_location_button_panel = new Panel ();
    lower_panel.add (outer_location_button_panel, "North");
    outer_location_button_panel.setLayout (new BorderLayout ());

    final Panel location_button_panel = new Panel ();
    outer_location_button_panel.add (location_button_panel, "West");

    final Panel location_panel = new Panel ();
    location_panel.add (new Label ("location: "));
    location_panel.add (location_text);

    final Button complement_button = new Button ("Complement");
    location_button_panel.add (complement_button);
    complement_button.addActionListener (new ActionListener () {
      public void actionPerformed (ActionEvent e) {
        complementLocation ();
      }
    });

    final Button grab_button = new Button ("Grab Range");
    location_button_panel.add (grab_button);
    grab_button.addActionListener (new ActionListener () {
      public void actionPerformed (ActionEvent e) {
        grabSelectedRange ();
      }
    });

    if (Options.getOptions ().getProperty ("external_editor") != null) {
      final Button external_edit_button = new Button ("External Edit");
      location_button_panel.add (external_edit_button);
      external_edit_button.addActionListener (new ActionListener () {
        public void actionPerformed (ActionEvent e) {
          externalEdit ();
        }
      });
    }

    middle_panel.add (location_panel, "North");

    add (key_and_qualifier_panel, "North");

    cancel_button.addActionListener (new ActionListener () {
      public void actionPerformed (ActionEvent e) {
        stopListening ();
        dispose ();
      }
    });

    ok_button.addActionListener (new ActionListener () {
      public void actionPerformed (ActionEvent e) {
        if (setFeature ()) {
          stopListening ();
          dispose ();
        }
      }
    });

    apply_button.addActionListener (new ActionListener () {
      public void actionPerformed (ActionEvent e) {
        setFeature ();
      }
    });

    final FlowLayout flow_layout =
      new FlowLayout (FlowLayout.CENTER, 18, 5);

    final Panel ok_cancel_update_panel = new Panel (flow_layout);

    ok_cancel_update_panel.setFont (getFont ());

    ok_cancel_update_panel.add (ok_button);
    ok_cancel_update_panel.add (cancel_button);
    ok_cancel_update_panel.add (apply_button);

    add (ok_cancel_update_panel, "South");


    middle_panel.add (lower_panel, "Center");
    lower_panel.add (qualifier_text_area, "Center");
    qualifier_text_area.setFont (getFont ());

    add (middle_panel, "Center");
  }

  /**
   *  Read the key, location and qualifier information from the feature and
   *  update the components.
   **/
  private void updateFromFeature () {
    datestamp = getFeature ().getDatestamp ();

    updateKey ();

    updateLocation ();

    updateQualifiers ();
  }

  /**
   *  Read the location from the feature and update the location field.
   **/
  private void updateLocation () {
    location_text.setText (getFeature ().getLocation ().toStringJoinInner ());
  }

  /**
   *   Complement the current location_text.
   **/
  private void complementLocation () {
    if (rationalizeLocation ()) {
      if (location_text.getText ().startsWith ("complement(")) {
        final String new_text = location_text.getText ().substring (11);
        if (new_text.endsWith (")")) {
          final String new_text2 =
            new_text.substring (0, new_text.length () - 1);
          location_text.setText (new_text2);
        } else {
          location_text.setText (new_text);
        }
      } else {
        final String new_text = location_text.getText ();

        location_text.setText ("complement(" + new_text + ")");
      }
    } else {
      new MessageDialog (this, "complement failed - " +
                         "current location can't be parsed");
    }
  }

  /**
   *  Add the currently selected range to location_text.
   **/
  private void grabSelectedRange () {
    if (rationalizeLocation ()) {
      final Range selected_range = selection.getSelectionRange ();

      if (selected_range == null) {
        new MessageDialog (this, "grab failed - nothing is selected");
        return;
      }

      // save it in case it gets mangled
      final String old_location_text = location_text.getText ();

      final String current_text = location_text.getText ();

      if (current_text.endsWith ("))")) {
        final String new_text =
          current_text.substring (0, current_text.length () - 2);

        location_text.setText (new_text + "," + selected_range.getStart () +
                               ".." + selected_range.getEnd () + "))");
      } else {
        if (current_text.endsWith (")")) {
          final String new_text =
            current_text.substring (0, current_text.length () - 1);

          location_text.setText (new_text + "," + selected_range.getStart () +
                                 ".." + selected_range.getEnd () + ")");
        } else {
          location_text.setText (current_text + "," +
                                 selected_range.getStart () +
                                 ".." + selected_range.getEnd ());
        }
      }

      if (!rationalizeLocation ()) {
        location_text.setText (old_location_text);
        new MessageDialog (this,
                           "grab failed - location can't be parsed after " +
                           "grabbing");
      }
    } else {
      new MessageDialog (this,
                         "grab failed - current location can't be parsed");
    }
  }

  /**
   *  Attempt to parse the current location_text as a Location.  If it can be
   *  parsed it will be canonicalized (ie. the complement, if any, will be
   *  outermost).  Returns true if and only if the location_text could be
   *  parsed.
   **/
  private boolean rationalizeLocation () {
    try {
      final Location location = new Location (location_text.getText ());

      location_text.setText (location.toStringJoinInner ());

      return true;
    } catch (LocationParseException e) {
      return false;
    }
  }

  /**
   *  Used for getting the current time/date in externalEdit ().
   **/
  private static java.util.Calendar calendar =
    java.util.Calendar.getInstance ();

  /**
   *  Edit the qualifiers of this Feature in an external editor.  The
   *  qualifiers will be set when the editor finishes.
   **/
  private void externalEdit () {
    try {
      // write to a temporary file
      final java.util.Date current_time = calendar.getTime ();

      final File temp_file =
        new File ("/tmp/artemis_temp." + current_time.getTime ());

      final FileWriter out_writer = new FileWriter (temp_file);

      final PrintWriter print_writer = new PrintWriter (out_writer);

      print_writer.write (getQualifierString ());

      print_writer.close ();
      out_writer.close ();

      final String editor_path =
        Options.getOptions ().getProperty ("external_editor");

      final String [] args = new String [1];

      args[0] = temp_file.getCanonicalPath ();

      final Process process =
        ExternalProgram.startProgram (editor_path, args);

      final ProcessWatcher process_watcher =
        new ProcessWatcher (process, "editor", false);

      final Thread watcher_thread = new Thread (process_watcher);

      watcher_thread.start ();


      final ProcessWatcherListener listener =
        new ProcessWatcherListener () {
          public void processFinished (final ProcessWatcherEvent event) {
            try {
              final FileReader file_reader = new FileReader (temp_file);

              final BufferedReader buffered_reader =
                new BufferedReader (file_reader);

              final StringBuffer buffer = new StringBuffer ();

              while (true) {
                final String line = buffered_reader.readLine ();

                if (line == null) {
                  qualifier_text_area.setText (buffer.toString ());

                  temp_file.delete ();

                  return;
                } else {
                  buffer.append (line + "\n");
                }
              }
            } catch (IOException e) {
              new MessageDialog (FeatureEdit.this, "an error occured while " +
                                 "reading from the editor: " + e);
            }
          }
        };

      process_watcher.addProcessWatcherListener (listener);

      new Thread (process_watcher);
    } catch (IOException e) {
      new MessageDialog (this, "error while starting editor: " + e);
    } catch (ExternalProgramException e) {
      new MessageDialog (this, "error while starting editor: " + e);
    }
  }

  /**
   *  Read the qualifiers from the feature and update the qualifier TextArea.
   **/
  private void updateQualifiers () {
    qualifier_text_area.setText (getQualifierString ());
  }

  /**
   *  Return a string containing one qualifier per line.
   **/
  private String getQualifierString () {
    final StringBuffer buffer = new StringBuffer ();

    final QualifierVector qualifiers = getFeature ().getQualifiers ();

    for (int i = 0 ; i < qualifiers.size () ; ++i) {
      final Qualifier this_qualifier = qualifiers.elementAt (i);

      final QualifierInfo qualifier_info =
        getEntryInformation ().getQualifierInfo (this_qualifier.getName ());

      final StringVector qualifier_strings =
        StreamQualifier.toStringVector (qualifier_info,
                                        qualifiers.elementAt (i));

      for (int value_index = 0 ;
           value_index < qualifier_strings.size () ;
           ++value_index) {

        final String qualifier_string =
          qualifier_strings.elementAt (value_index);

        buffer.append (qualifier_string + "\n");
      }
    }

    return buffer.toString ();
  }

  /**
   *  Set the key, location and qualifiers of the feature to be the same as
   *  what values currently shown in the components.
   *  @return true if and only if action succeeds.  It may fail because of an
   *    illegal location or qualifier, in which case a message will be
   *    displayed before returning.
   **/
  private boolean setFeature () {
    final Key key = key_choice.getSelectedItem ();

    final KeyVector possible_keys = getEntryInformation ().getValidKeys ();

    if (possible_keys != null && !possible_keys.contains (key)) {
      final YesNoDialog dialog =
        new YesNoDialog (this, "Add this new key: " + key + "?");

      if (dialog.getResult ()) {
        // yes
        getEntryInformation ().addKey (key);
      } else {
        // no
        return false;
      }
    }

    final Location location;

    try {
      location = new Location (location_text.getText ());
    } catch (LocationParseException exception) {
      final String error_string = exception.getMessage ();
      System.out.println (error_string);
      new MessageDialog (this,
                         "Cannot apply changes because of location error: " +
                         error_string);

      return false;
    }


    final QualifierVector qualifiers;

    try {
      final String qualifier_string = qualifier_text_area.getText ();
      qualifiers = getQualifiersFromString (qualifier_string,
                                            getEntryInformation ());
    } catch (QualifierParseException exception) {
      final String error_string = exception.getMessage ();
      System.out.println (error_string);
      new MessageDialog (this,
                         "Cannot apply changes because of a qualifier " +
                         "error: " + error_string);

      return false;
    }


    dribble ();

    try {
      try {
        getFeature ().set (datestamp, key, location, qualifiers);
      } catch (OutOfDateException e) {
        final YesNoDialog dialog =
          new YesNoDialog (this,
                           "the feature has changed since the edit " +
                           "window was opened, continue?");
        if (dialog.getResult ()) {
          // yes - ignore the datestamp
          getFeature ().set (key, location, qualifiers);
        } else {
          // no
          return false;
        }
      }
    } catch (EntryInformationException e) {
      final String error_string = e.getMessage ();
      System.out.println (error_string);
      new MessageDialog (this, "Cannot apply changes: " + error_string);

      return false;
    } catch (OutOfRangeException e) {
      new MessageDialog (this,
                         "Cannot apply changes - the location is out of " +
                         "range for this sequence");
      return false;
    } catch (ReadOnlyException e) {
      new MessageDialog (this,
                         "Cannot apply changes - the feature is in a " +
                         "read only entry");
      return false;
    }

    return true;
  }

  /**
   *  Return the Feature we are editing.
   **/
  public Feature getFeature () {
    return edit_feature;
  }

  /**
   *  Return a QualifierVector containing the qualifiers from a String.
   *  @param qual_string contains the qualifiers to parse
   */
  private static QualifierVector
    getQualifiersFromString (final String qual_string,
                             final EntryInformation entry_information)
      throws QualifierParseException {

    try {
      final StringReader string_reader = new StringReader (qual_string);

      final QualifierVector embl_qualifiers =
        EmblStreamFeature.readQualifiers (string_reader,
                                          entry_information);

      string_reader.close ();

      return embl_qualifiers;
    } catch (IOException exception) {
      throw (new QualifierParseException (exception.getMessage ()));
    }
  }

  /**
   *  On Unix machines this method will append the text of the feature to a
   *  file in a current directory called .dribble
   **/
  private void dribble () {
    if (!Options.isUnixHost ()) {
      return;
    }

    final String dribble_file_name;

    if (getEntry ().getName () != null) {
      dribble_file_name = ".dribble." + getEntry ().getName ();      
    } else {
      dribble_file_name = ".dribble.no_name";
    }

    try {
      final Writer writer = new FileWriter (dribble_file_name, true);
      getFeature ().writeNative (writer);
      writer.flush ();
      writer.close ();
    } catch (IOException e) {
      System.err.println ("IO exception while accessing " + dribble_file_name +
                          ": " + e.getMessage ());
    }
  }

  /**
   *  Return the Entry that contains the Feature this object is displaying.
   **/
  private Entry getEntry () {
    return edit_entry;
  }

  /**
   *  Read the key from the feature and update the key chooser.
   **/
  private void updateKey () {
    final Key feature_key = getFeature ().getKey ();

    key_choice.setKey (feature_key);
  }

  /**
   *  Return the EntryInformation object of the entry containing the feature.
   **/
  public EntryInformation getEntryInformation () {
    return getEntry ().getEntryInformation ();
  }

  /**
   *  The choice of feature keys.
   **/
  private KeyChoice key_choice;

  private QualifierChoice qualifier_choice = null;

  private TextField location_text = new TextField (80);

  private Button add_qualifier_button = new Button ("Add qualifier");

  private Button ok_button = new Button ("OK");
  private Button cancel_button = new Button ("Cancel");
  private Button apply_button = new Button ("Apply");

  private TextArea qualifier_text_area =
    new TextArea ("", 18, 81, TextArea.SCROLLBARS_VERTICAL_ONLY);

  /**
   *  The Feature this object is displaying.
   **/
  private Feature edit_feature;

  /**
   *  The Entry that contains the Feature this object is displaying.
   **/
  private Entry edit_entry;

  /**
   *  The datestamp of the RWCorbaFeature when updateFromFeature () was last
   *  called or null if this is not a RWCorbaFeature.
   **/
  private Date datestamp = null;

  /**
   *  The Selection that was passed to the constructor.
   **/
  private Selection selection;
}
