/* Options.java
 *
 * created: Thu Dec 10 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/Options.java,v 1.50 2000/11/10 15:30:29 kmr Exp $
 **/

package diana;

import AppGlobal;

import uk.ac.sanger.pathogens.*;
import uk.ac.sanger.pathogens.embl.Key;
import uk.ac.sanger.pathogens.embl.KeyVector;
import uk.ac.sanger.pathogens.embl.QualifierInfo;
import uk.ac.sanger.pathogens.embl.QualifierInfoVector;
import uk.ac.sanger.pathogens.embl.QualifierInfoException;
import uk.ac.sanger.pathogens.embl.EntryInformation;
import uk.ac.sanger.pathogens.embl.SimpleEntryInformation;

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

/**
 *  An object of this class is used to read, write and store all the
 *  configurable options in Diana.  The method Properties.load () is used to
 *  read in new options.
 *
 *  @author Kim Rutherford
 *  @version $Id: Options.java,v 1.50 2000/11/10 15:30:29 kmr Exp $
 **/

public class Options extends Properties {
  /**
   *  This is the object that will be returned by getOptions ().
   **/
  static private Options options = new Options ();

  /**
   *  Create a new Options object with default settings for the options.
   **/
  public Options () {
    try {
      put ("font_size", "14");
      reset ();

    } catch (Throwable e) {
      e.printStackTrace ();
    }
  }

  /**
   *  Returns true if and only if the given String is not "no", "false", "n"
   *  or "f".
   **/
  private boolean getPropertyTruthValueInternal (final String value) {
    if (value == null) {
      return false;
    }

    final String lowercase_value = value.toLowerCase ();

    if (lowercase_value.equals ("false") ||
        lowercase_value.equals ("f") || lowercase_value.equals ("no") ||
        lowercase_value.equals ("n"))  {
      return false;
    } else {
      return true;
    }
  }

  /**
   *  Returns true if and only if the given property is set and is not "no",
   *  "false", "n" or "f".
   **/
  public boolean getPropertyTruthValue (final String name) {
    return getPropertyTruthValueInternal (getProperty (name));
  }

  /**
   *  Read the options from the options file and diana.ini
   **/
  private void readOptions () {
    try {
      final InputStream options_input_stream =
        AppGlobal.getFileFromJar ("etc/options");

      load (options_input_stream);

      final boolean run_quietly =
        getPropertyTruthValueInternal (System.getProperty ("run_quietly"));

      if (isStandAlone ()) {
        final String user_home = System.getProperty ("user.home");

        // properties are read in order from these files.
        // the null is replaced by the file name given by the extra_options
        // system property (if it is set)
        final String [] option_file_names = {
          null,
          "diana.ini",
          "options",
          "options.txt",
          "options.text",
          user_home + File.separator + ".artemis_options"
        };

        final Properties system_properties = System.getProperties ();

        if (system_properties != null) {
          final String extra_options_prop =
            system_properties.getProperty ("extra_options");

          if (extra_options_prop !=null) {
            if (new File (extra_options_prop).canRead ()) {
              option_file_names[0] = extra_options_prop;
            } else {
              System.err.println ("warning: could not read options from \"" +
                                  extra_options_prop + "\"");
            }
          }
        }

        for (int i = 0 ; i < option_file_names.length ; ++i) {
          final String this_options_file = option_file_names[i];

          if (this_options_file == null) {
            continue;
          }

          final Document options_document =
            new FileDocument (new File (this_options_file));

          // read the "diana.ini" file from the current directory if it exists
          if (options_document.readable ()) {

            final InputStream options_document_stream =
              options_document.getInputStream ();

            if (!run_quietly) {
              System.err.println ("reading options from \"" +
                                  this_options_file + "\"");
            }

            load (options_document_stream);
          }
        }
      }
    } catch (IOException e) {
      System.err.println ("could not read an options file : " + e);
    }
  }

  /**
   *  Reset this Options object by setting all the options to their default
   *  values and then call readOptions ().
   **/
  public void reset () {
    clear ();

    setDefaultFeatureColours ();
    setDefaultColourMap ();

    readOptions ();

    readSystemOptions ();

    resetCachedValues ();
  }

  /**
   *  Call load () from the super class and then reset the cached values for
   *  Font and Color objects.
   **/
  public void load (InputStream in_stream)
      throws IOException {
    super.load (in_stream);

    resetCachedValues ();
  }

  /**
   *  Return the reference of the global Options object.
   **/
  public static Options getOptions () {
    return options;
  }

  /**
   *  Return true if and only if we are running on a Unix machine.
   **/
  public static boolean isUnixHost () {
    if (System.getProperty ("artemis.environment") != null &&
        System.getProperty ("artemis.environment").equals ("UNIX")) {
      return true;
    } else {
      return false;
    }
  }

  /**
   *  Return true if and only if Artemis is in "Noddy mode", meaning there
   *  will be lots of requesters asking for confirmation when delete/editing.
   **/
  public static boolean isNoddyMode () {
    return !isBlackBeltMode ();
  }

  /**
   *  Return true if and only if isNoddyMode () will return false.
   **/
  public static boolean isBlackBeltMode () {
    return getOptions ().getPropertyTruthValue ("black_belt_mode");
  }

  /**
   *  Return true if and only if we are running in an applet.
   **/
  public static boolean isApplet () {
    return AppGlobal.isApplet ();
  }

  /**
   *  Return true if and only if we are not running in an applet.
   **/
  public static boolean isStandAlone () {
    return !isApplet ();
  }

  /**
   *  Return a StringVector containing the values of the given option or null
   *  if there is no such option.  The values are separated by spaces in the
   *  properties file.
   *
   *  @see #getPropertyValues ()
   **/
  public StringVector getOptionValues (final String option) {
    return getPropertyValues (this, option);
  }

  /**
   *  Return a StringVector containing the values of the given option in the
   *  given Properties object or null if there is no such option.  The values
   *  are separated by spaces in the properties file.  For example, asking for
   *  the property foo in a Properties object where foo has the value: "thing1
   *  thing2 thing3", will return a vector containing three String objects:
   *  "thing1", "thing2", "thing3"
   **/
  public static StringVector getPropertyValues (final Properties properties,
                                                final String option) {
    final String option_value = properties.getProperty (option);

    if (option_value == null) {
      return null;
    }

    return StringVector.getStrings (option_value);
  }

  /**
   *  Return a QualifierInfoVector object containing one object for each
   *  qualifier given in extra_qualifiers option.
   **/
  public QualifierInfoVector getExtraQualifiers () {
    final QualifierInfoVector return_vector =
      new QualifierInfoVector ();

    final StringVector extra_qualifiers_strings =
      getOptionValues ("extra_qualifiers");

    for (int i = 0 ; i < extra_qualifiers_strings.size () / 2 ; ++i) {
      final String name = extra_qualifiers_strings.elementAt (i * 2);
      final String type_string =
        extra_qualifiers_strings.elementAt (i * 2 + 1);
      final int type = QualifierInfo.getQualifierTypeID (type_string);

      return_vector.add (new QualifierInfo (name, type, null, null, false));
    }

    return return_vector;
  }

  /**
   *  Return the path of the default sequence file or null if there is no
   *  default.  This will usually have been set in the diana.ini file,
   *  eg. SEQUENCE_FILE='c5H10.seq'
   **/
  public String getDefaultSequenceFileName () {
    final String property_string = getProperty ("SEQUENCE_FILE");

    if (property_string == null) {
      return null;
    } else {
      if (property_string.length () == 0) {
        return null;
      }

      // trim the quotes from the name
      if (property_string.startsWith ("'") &&
          property_string.endsWith ("'") &&
          property_string.length () > 2) {
        return property_string.substring (1, property_string.length () - 1);
      } else {
        return property_string;
      }
    }
  }

  /**
   *  Return the path of the default feature file or null if there is no
   *  default.  This will usually have been set in the diana.ini file,
   *  eg. FEATURE_FILE='c5H10_embl.tab'
   **/
  public String getDefaultFeatureFileName () {
    final String property_string = getProperty ("FEATURE_FILE");

    if (property_string == null) {
      return null;
    } else {
      if (property_string.length () == 0) {
        return null;
      }

      // trim the quotes from the name
      if (property_string.startsWith ("'") &&
          property_string.endsWith ("'") &&
          property_string.length () > 2) {
        return property_string.substring (1, property_string.length () - 1);
      } else {
        return property_string;
      }
    }
  }

  /**
   *  Return the minimum open reading frame size used when automatically
   *  creating ORFs with the "Mark Open Reading Frames" function.
   **/
  public int getMinimumORFSize () {
    final Integer minimum_orf_size = getIntegerProperty ("minimum_orf_size");

    if (minimum_orf_size == null) {
      // default value
      return 100;
    } else {
      return minimum_orf_size.intValue ();
    }
  }

  /**
   *  Get the default colour for the given feature key, as specified in the
   *  options file.  If no colour is specified in the file then null is
   *  returned.
   *  @param key The feature key to get the colour of.
   **/
  public Color getDefaultFeatureColour (Key key) {
    final Integer colour_integer = getIntegerProperty ("colour_of_" + key);

    if (colour_integer == null) {
      return null;
    } else {
      return getColorFromColourNumber (colour_integer.intValue ());
    }
  }

  /**
   *  Given a colour number (perhaps from a /colour qualifier) return an
   *  appropriate Color object.
   **/
  public Color getColorFromColourNumber (int colour_number) {
    // first look up colour_map for speed.  if that fails we then try to look
    // for an appropriate property and turn into a Color object.
    if (colour_number < 0) {
      return null;
    }

    if (colour_number >= colour_map.size () ||
        colour_map.elementAt (colour_number) == null) {
      // there is no colour for this colour_number
      return null;
    } else {
      return (Color) colour_map.elementAt (colour_number);
    }
  }

  /**
   *  Given a String containing three space separated integers (0-255), return
   *  a Color object.  The three integers represent red, green and blue
   *  respectively.  If a Color can't be parsed null is returned.
   **/
  private Color parseColour (String colour_string) {
    try {
      // first get three integers from the String

      // trim any whitespace from the ends
      final StringVector value_strings =
        StringVector.getStrings (colour_string.trim ());

      final int first_int =
        Integer.valueOf (value_strings.elementAt (0)).intValue ();
      final int second_int =
        Integer.valueOf (value_strings.elementAt (1)).intValue ();
      final int third_int =
        Integer.valueOf (value_strings.elementAt (2)).intValue ();

      return new Color (first_int, second_int, third_int);

    } catch (NumberFormatException e) {
      return null;
    }
  }

  /**
   *  Get the default Color for a colour
   **/
  public void setDefaultColourMap () {
    put ("colour_0", "255 255 255"); // white
    put ("colour_1", "100 100 100"); // dark grey
    put ("colour_2", "255 0 0");     // red
    put ("colour_3", "0 255 0");     // green
    put ("colour_4", "0 0 255");     // blue
    put ("colour_5", "0 255 255");   // cyan
    put ("colour_6", "255 0 255");    // magenta
    put ("colour_7", "255 255 0");    // yellow
    put ("colour_8", "152 251 152");  // pale green
    put ("colour_9", "135 206 250");  // light sky blue
    put ("colour_10", "255 165 0");    // orange
    put ("colour_11", "200 150 100");  // brown
    put ("colour_12", "255 200 200");  // pink
  }

  /**
   *  Return a vector containing the ExternalProgram objects of all the
   *  external programs that we can use.
   **/
  public ExternalProgramVector getExternalPrograms () {
    if (external_programs == null) {

      external_programs = new ExternalProgramVector ();

      final StringVector protein_value_strings =
        getOptionValues ("feature_protein_programs");

      if (protein_value_strings != null) {
        for (int i = 0 ; i < protein_value_strings.size () / 2 ; ++i ) {
          final String program_name =
            protein_value_strings.elementAt (i * 2);
          final String program_options =
            protein_value_strings.elementAt (i * 2 + 1);

          final ExternalSearchProgram program =
            new ExternalSearchProgram (program_name, program_options, false);

          external_programs.add (program);
        }
      }

      final StringVector dna_value_strings =
        getOptionValues ("feature_dna_programs");

      if (dna_value_strings != null) {

        for (int i = 0 ; i < dna_value_strings.size () / 2 ; ++i ) {
          final String program_name =
            dna_value_strings.elementAt (i * 2);
          final String program_options =
            dna_value_strings.elementAt (i * 2 + 1);

          final ExternalSearchProgram program =
            new ExternalSearchProgram (program_name, program_options, true);

          external_programs.add (program);
        }
      }

      final StringVector application_value_strings =
        getOptionValues ("application_programs");

      if (application_value_strings != null) {

        for (int i = 0 ; i < application_value_strings.size () ; ++i ) {
          final String program_name = application_value_strings.elementAt (i);

          final ExternalApplication program =
            new ExternalApplication (program_name);

          external_programs.add (program);
        }
      }
    }

    return external_programs;
  }

  /**
   *  Return a StringVector containing the bases of the possible start
   *  codons.  This is stored in the start_codons option in the options file.
   **/
  public StringVector getPossibleStartCodons () {
    final StringVector option_values = getOptionValues ("start_codons");

    for (int i = 0 ; i < option_values.size () ; ++i) {
      final String new_value = option_values.elementAt (i).toLowerCase ();
      option_values.setElementAt (new_value, i);
    }

    return option_values;
  }

  /**
   *  Return the default font that should be used for all windows.
   **/
  public Font getFont () {
    return font;
  }

  /**
   *  Read all the system properties and overwrite this Options object with
   *  those values.
   **/
  private void readSystemOptions () {
    if (isStandAlone ()) {
      final Properties system_properties = System.getProperties ();

      if (system_properties != null) {
        final Enumeration enumeration = system_properties.keys ();

        while (enumeration.hasMoreElements ()) {
          final String key = (String) enumeration.nextElement ();

          final String value = system_properties.getProperty (key);

          put (key, value);
        }
      }
    }
  }

  /**
   *  Set the default colour number for each a feature key.
   **/
  private void setDefaultFeatureColours () {
    final Object [] key_colour_map = {
      "CDS", "5",
      "cds?", "7",
      "BLASTCDS", "2",
      "BLASTN_HIT", "6",
      "source", "0",
      "prim_tran", "0",
      "stem_loop", "2",
      "misc_feature", "3",
      "delta", "3",
      "repeat_region", "9",
      "repeat_unit", "9",
      "terminator", "3",
      "promoter", "3",
      "intron", "1",
      "exon", "7",
      "mRNA", "1",
      "tRNA", "8",
      "TATA", "3",
      "bldA", "2"
    };

    for (int i = 0 ; i < key_colour_map.length / 2 ; ++i) {
      put ("colour_of_" + key_colour_map[i*2], key_colour_map[i*2+1]);
    }
  }

  /**
   *  Clear all cached values (such as the font) and then recalculate.
   **/
  private void resetCachedValues () {
    /*final*/ int font_size;

    try {

      final Integer font_size_integer = getIntegerProperty ("font_size");
      font_size = font_size_integer.intValue ();

    } catch (NumberFormatException e) {
      // XXX
      new diana.components.MessageFrame ("error in options file - " +
                                         "font_size should be an integer");
      // a default value
      font_size = 14;
      put ("font_size", String.valueOf (font_size));
    }

    font = new Font ("Monospaced", Font.PLAIN, font_size);

    colour_map = new Vector (25);

    int colour_number = 0;

    while (true) {

      final String colour_value_string =
        getProperty ("colour_" + colour_number);

      if (colour_value_string == null) {
        // we know nothing about this colour number so we assume that there
        // aren't any more
        break;
      } else {
        final Color new_colour = parseColour (colour_value_string);

        if (new_colour == null) {
          // we couldn't parse the colour
          new diana.components.MessageFrame ("could not understand this " +
                                             "number: " +
                                             colour_value_string);
        } else {
          if (colour_number >= colour_map.size ()) {
            colour_map.setSize (colour_number + 50);
          }
          colour_map.setElementAt (new_colour, colour_number);
        }
      }

      ++colour_number;
    }
  }

  /**
   *  Return the value of a property/option as an Integer.  If the property
   *  does not exist or cannot be parsed as an Integer, the method returns
   *  null.
   **/
  public Integer getIntegerProperty (String property_name) {
    final String property_string = getProperty (property_name);

    if (property_string == null) {
      return null;
    } else {
      return Integer.valueOf (property_string);
    }
  }

  /**
   *  Enable direct editing if and only if the argument is true.  "Direct
   *  Editing" allows feature locations to be changed by dragging the ends of
   *  the features around with the mouse.
   **/
  public void setDirectEdit (final boolean direct_edit) {
    if (direct_edit) {
      put ("direct_edit", "true");
    } else {
      put ("direct_edit", "false");
    }
  }

  /**
   *  Returns true if and only if direct editing is enabled.
   **/
  public boolean canDirectEdit () {
    final String direct_edit_prop = getProperty ("direct_edit");

    if (direct_edit_prop == null || direct_edit_prop.equals ("false")) {
      return false;
    } else {
      return true;
    }
  }

  /**
   *  The EntryInformation object to use for EMBL and GENBANK entries.
   **/
  private static EntryInformation extended_entry_infomation;

  /**
   *  The EntryInformation object to use for EMBL and GENBANK entries.
   **/
  private static EntryInformation db_entry_infomation;

  /**
   *  Return the EntryInformation object to use for EMBL and GENBANK entries.
   **/
  public static EntryInformation getDBEntryInformation () {
    return db_entry_infomation;
  }

  /**
   *  The EntryInformation object that has all the information that
   *  getDBEntryInformation () would return, but which has the non-standard
   *  qualifiers and keys added.
   **/
  public static EntryInformation getArtemisEntryInformation () {
    return extended_entry_infomation;
  }

  /**
   *  Return an EntryInformation object that is suitable for EMBL and GENBANK
   *  entries.
   **/
  private static EntryInformation makeEntryInformation ()
      throws IOException, QualifierInfoException {
    final InputStream feature_keys_stream =
      AppGlobal.getFileFromJar ("etc/feature_keys");

    final InputStream qualifier_types_stream =
      AppGlobal.getFileFromJar ("etc/qualifier_types");

    QualifierInfoVector qualifier_info_vector =
      readQualifierInfo (qualifier_types_stream, feature_keys_stream);

    final EntryInformation entry_information = new SimpleEntryInformation ();

    for (int i = 0 ; i < qualifier_info_vector.size () ; ++i) {
      final QualifierInfo qualifier_info =
        qualifier_info_vector.elementAt (i);

      entry_information.addQualifierInfo (qualifier_info);
    }

    entry_information.setEMBLFormat (true);

    return entry_information;
  }

  /**
   *  Return an EntryInformation object that is suitable for EMBL and GENBANK
   *  entries, and has some useful non-standard additions (specified by the
   *  options file).
   **/
  private static EntryInformation
    makeArtemisEntryInformation (final EntryInformation standard_entry_info)
      throws QualifierInfoException {

    final StringVector extra_keys =
      getOptions ().getOptionValues ("extra_keys");

    final QualifierInfoVector extra_qualifiers =
      getOptions ().getExtraQualifiers ();

    final EntryInformation return_entry_information =
      new SimpleEntryInformation (standard_entry_info);

    for (int i = 0 ; i < extra_keys.size () ; ++i) {
      final Key new_key = new Key (extra_keys.elementAt (i));

      return_entry_information.addKey (new_key);
    }

    for (int i = 0 ; i < extra_qualifiers.size () ; ++i) {
      final QualifierInfo new_qualifier_info = extra_qualifiers.elementAt (i);

      return_entry_information.addQualifierInfo (new_qualifier_info);
    }

    return_entry_information.setEMBLFormat (false);

    return return_entry_information;
  }

  /**
   *  Read the possible feature key and qualifier names and types from the two
   *  given streams (see etc/feature_keys and etc/qualifier_types for details
   *  on the formats).
   **/
  private static QualifierInfoVector
    readQualifierInfo (final InputStream qualifier_types_stream,
                       final InputStream feature_keys_stream)
      throws IOException {

    final QualifierInfoVector return_vector = new QualifierInfoVector ();

    Properties feature_properties = new Properties ();
    final Properties qualifier_properties = new Properties ();

    feature_properties.load (feature_keys_stream);
    qualifier_properties.load (qualifier_types_stream);

    // parse the feature_properties

    {
      final Properties new_feature_properties = new Properties ();

      final Enumeration feature_enum = feature_properties.propertyNames ();

      while (feature_enum.hasMoreElements ()) {
        String current_feature_name = (String) feature_enum.nextElement ();

        final StringVector property_values =
          Options.getPropertyValues (feature_properties, current_feature_name);

        new_feature_properties.put (current_feature_name, property_values);
      }

      feature_properties = new_feature_properties;
    }

    final Enumeration qualifier_enum = qualifier_properties.propertyNames ();

    while (qualifier_enum.hasMoreElements ()) {

      String current_qualifier_name = (String) qualifier_enum.nextElement ();

      final StringVector current_qualifier_values =
        Options.getPropertyValues (qualifier_properties,
                                   current_qualifier_name);


      final boolean once_only =
        current_qualifier_values.elementAt (0).equals ("yes");

      final String type_string = current_qualifier_values.elementAt (1);

      // find the keys for which this qualifier name is valid or required

      final KeyVector valid_keys = new KeyVector ();
      final KeyVector required_keys = new KeyVector ();

      final Enumeration features_enum = feature_properties.propertyNames ();

      while (features_enum.hasMoreElements ()) {
        final String current_key_string = (String)features_enum.nextElement ();

        final Key current_key = new Key (current_key_string);

        final StringVector current_feature_qualifiers =
          (StringVector) feature_properties.get (current_key_string);

        if (current_feature_qualifiers.contains (current_qualifier_name)) {
          valid_keys.add (current_key);
        } else {
          if (current_feature_qualifiers.contains ("@" +
                                                   current_qualifier_name)) {
            valid_keys.add (current_key);
            required_keys.add (current_key);
          }
        }
      }

      final int type = QualifierInfo.getQualifierTypeID (type_string);

      final QualifierInfo qualifier_info =
        new QualifierInfo (current_qualifier_name, type, valid_keys,
                           required_keys, once_only);

      return_vector.add (qualifier_info);
    }

    return return_vector;
  }

  static {
    try {
      db_entry_infomation = makeEntryInformation ();
      extended_entry_infomation =
        makeArtemisEntryInformation (db_entry_infomation);
    } catch (QualifierInfoException e) {
      System.err.println ("could not initialise the embl package: " +
                          e.getMessage ());
      System.exit (1);
    } catch (IOException e) {
      System.err.println ("could not initialise the embl package: " +
                          e.getMessage ());
      System.exit (1);
    }
  }

  /**
   *  The default font that should be used for all windows.
   **/
  private Font font = null;

  /**
   *  A map of colour numbers to Color object.  This is initialised by
   *  setDefaultColourMap ().
   **/
  private Vector colour_map = null;

  /**
   *  A vector containing the ExternalProgram objects that we can run.
   **/
  private ExternalProgramVector external_programs = null;
}
