/**
 *#########################################################################
 *
 * A component of the Greenstone Librarian Interface (GLI) application, 
 * part of the Greenstone digital library software suite from the New 
 * Zealand Digital Library Project at the University of Waikato, 
 * New Zealand.
 *
 * Author: John Thompson 
 *         Greenstone Project, New Zealand Digital Library
 *         University of Waikato
 *         http://www.nzdl.org
 *
 * Copyright (C) 2004 New Zealand Digital Library, University of Waikato
 *
 * 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., 675 Mass Ave, Cambridge, MA 02139, USA.
 *########################################################################
 */

package org.greenstone.gatherer.cdm;

import java.io.*;
import java.util.*;
import org.greenstone.gatherer.Configuration;
import org.greenstone.gatherer.DebugStream;
import org.greenstone.gatherer.Gatherer;
import org.greenstone.gatherer.collection.Collection;
import org.greenstone.gatherer.collection.CollectionManager;
import org.greenstone.gatherer.metadata.MetadataElement;
import org.greenstone.gatherer.metadata.MetadataTools;
import org.greenstone.gatherer.util.StaticStrings;
import org.greenstone.gatherer.util.Utility;
import org.greenstone.gatherer.util.XMLTools;
import org.w3c.dom.*;

/** This class contains all the details about a single argument that can be passed to this plugin, including option lists if the parameters are restricted.
 * @author John Thompson, Greenstone Project, New Zealand Digital Library, University of Waikato
 * @version 2.41 final
 */
public class Argument
  implements Comparable, Serializable {
  /** An element of the argument type enumeration specifying a combobox control. */
  static final public byte ENUM = 0;
  /** An element of the argument type enumeration specifying a checkbox control. */
  static final public byte FLAG = 1;
  /** An element of the argument type enumeration specifying a tree control. */
  static final public byte HIERARCHY = 2;
  /** An element of the argument type enumeration specifying a spinner control. */
  static final public byte INTEGER = 3;
  /** An element of the argument type enumeration specifying a language combobox control. */
  static final public byte LANGUAGE = 4;
  /** An element of the argument type enumeration specifying a metadata combobox control. */
  static final public byte METADATA = 5;
  /** An element of the argument type enumeration specifying a text field. */
  static final public byte STRING = 6;
  /** An element of the argument type enumeration specifying a regular expression text field. */
  static final public byte REGEXP = 7;
  /** An element of the argument type enumeration specifying a metadata set combobox control. */
  static final public byte METADATA_SET_NAMESPACE = 8;
  /** An element of the argument type enumeration specifying a text field. */
  static final public byte URL = 9;
  /** An editable combo box */
  static final public byte ENUM_STRING = 10;

  /////////////////////////////////////////////////////////////////

  /** true if this argument should actually be hidden within the GLI. This is important for arguments such as import dir or other location critical arguments. */
  private boolean hidden_gli = false;
  /** <i>true</i> if this argument is required for the applicable script to work properly, <i>false</i> otherwise. */
  private boolean required = false;
  /** The type of this argument. Used to be an int, but bytes are cheaper. */
  private byte type = STRING;
  /** The maximum value an integer based control can have. */
  private int maximum = Integer.MAX_VALUE;
  /** The minimum value an integer based control can have. */
  private int minimum = Integer.MIN_VALUE;
  /** Every argument has a detail mode level at which it becomes available to the user to edit. 
   * @see org.greenstone.gatherer.Configuration
   */
  private int mode_level = Configuration.LIBRARIAN_MODE;
  /** The DOM element this argument is built around, if any. */
  private Element element;
  /** If the argument is of type ENUM or ENUM_STRING then this map holds all the various options. Each entry is an &lt;option value&gt; -&gt; &lt;description&gt; mapping. */
  private ArrayList option_list = null;
  /** A default value for parameter-type arguments. May be a Perl pattern. */
  private String default_value = null;
  /** The text description of this argument parsed from the pluginfo output. */
  private String description = null;
  /** The argument flag as it appears in the command. Also used as the unique identifier of an argument. */
  private String name = null;
    /** The value of the arg, stored for metadata type options */
    private String stored_value = null;
  /** The plugin that owns this argument, for the purposes of visualising inheritance. */
  private String owner = null;

  private String display_name = null;

  /** Default Constructor. */
  public Argument() {
  }

  /** Another constructor but this one is a little more interesting as it takes a DOM element.
   * @param element the Element this argument is based around
   */
  public Argument(Element element) {
    this.element = element;
  }

  /** Method to add an element to the option_list.
   * @param name the name value of the option as a String
   * @param desc the description of this options as a String
   */
  public void addOption(String name, String desc) {
    if((type == ENUM || type == ENUM_STRING) && name != null) {
      if(desc == null) {
	desc = "";
      }
      if(option_list == null) {
	option_list = new ArrayList();
      }
      option_list.add(new ArgumentOption(name, desc));
    }
  }

  /** Method to compare two arguments for ordering.
   * @param object the argument we are comparing to, as an Object
   * @return an int specifying the argument order, using values as set out in String
   * @see org.greenstone.gatherer.cdm.Argument
   */
  public int compareTo(Object object) {
    if(object instanceof Argument) {
      return getName().compareTo(((Argument)object).getName());
    }
    else {
      return toString().compareTo(object.toString());
    }
  }

  /** Create a copy of this argument.
   * @return a newly created Argument with the same details as this one
   */
  public Argument copy() {
    Argument copy = new Argument();
    copy.setDefaultValue(default_value);
    copy.setDescription(description);
    copy.setOptions(option_list);
    copy.setOwner(owner);
    copy.setName(name);		
    copy.setDisplayName(display_name);
    copy.setRequired(required);
    copy.setType(type);
    copy.setMinimum(minimum);
    copy.setMaximum(maximum);
    copy.setModeLevel(mode_level);
    copy.setHiddenGLI(hidden_gli);
    return copy;
  }

  /** Method to determine if two arguments are equal.
   * @param object the argument to test against, as an Object
   * @return true if the arguments names match, false otherwise
   */
  public boolean equals(Object object) {
    return (compareTo(object) == 0);
  }

  /** Method to retrieve the value of default_value.
   * @return a String containing the default value
   */
  public String getDefaultValue() {
    return default_value;
  }

  /** Method to retrieve this arguments description.
   * @return a String containing the description
   */
  public String getDescription() {
    return description;
  }

  public Element getElement() {
    return element;
  }
  /** Retrieve the upper bound of a range based argument.
   * @return the maximum as an int
   */
  public int getMaximum() {
    return maximum;
  }

  /** Retrieve the lower bound of a range based argument.
   * @return the minimum as an int
   */
  public int getMinimum() {
    return minimum;
  }

  /** Retrieves the mode level at which this argument should become available. Any higher levels should also see this argument.
   * @return the mode level as an int
   */
  public int getModeLevel() {
    return mode_level;
  }

  /** Method to retrieve the value of name.
   * @return a String containing the argument name
   * @see org.greenstone.gatherer.util.StaticStrings#NAME_ATTRIBUTE
   */
  public String getName() {
    if(name == null && element != null) {
      name = element.getAttribute(StaticStrings.NAME_ATTRIBUTE);
    }
    return name;
  }

  public String getDisplayName() {
    if(display_name==null)
      return "";
    return display_name;
  }

  /** Method to retrieve the option list for this argument.
   * @return a HashMap containing &lt;option value&gt; -&gt; &lt;description&gt; entries
   */
  public ArrayList getOptions() {
    return option_list;
  }

  /** Retrieve the name of the owner of this argument.
   * @return the owners name as a String
   */
  public String getOwner() {
    return owner;
  }

  /** Method to determine the type of this argument.
   * @return a byte specifying the type
   */
  public byte getType() {
    return type;
  }

  /** Method to retrieve the value of value.
   * @return the value of value as a String
   * @see org.greenstone.gatherer.Gatherer#c_man
   * @see org.greenstone.gatherer.collection.CollectionManager#getCollection
   */
  public String getValue()
  {
    // Only assigned arguments have values.
    if (element == null) {
      return null;
    }
    if (stored_value != null) {
	return stored_value;
    }
    String value = XMLTools.getValue(element);
    if (type == METADATA) {
	// We display using metadata display name, but store in the XML using
	// full name (canonical name)
	stored_value = MetadataTools.convertMetadataElementListNames(value, MetadataTools.TO_DISPLAY_NAMES);
	return stored_value;
    }
    return value; 
  }


  /** Method to determine if this argument has been assigned.
   * @return true if it has, false otherwise
   * @see org.greenstone.gatherer.util.StaticStrings#ASSIGNED_ATTRIBUTE
   * @see org.greenstone.gatherer.util.StaticStrings#TRUE_STR
   */
  public boolean isAssigned() {
    return (element != null && element.getAttribute(StaticStrings.ASSIGNED_ATTRIBUTE).equals(StaticStrings.TRUE_STR));
  }

  /** Determine if this is a custom argument ie one that has been parsed from the config file but doesn't have a matching entry in the argument library.
   * @return true if this argument is a custom, false otherwise
   * @see org.greenstone.gatherer.util.StaticStrings#CUSTOM_ATTRIBUTE
   * @see org.greenstone.gatherer.util.StaticStrings#TRUE_STR
   */
  public boolean isCustomArgument() {
    return (element != null && element.getAttribute(StaticStrings.CUSTOM_ATTRIBUTE).equals(StaticStrings.TRUE_STR));
  }

  /** Determine if this argument is hidden in GLI
   * @return true if the argument is hidden, false otherwise
   */
  public boolean isHiddenGLI() {
    return hidden_gli;
  }

  /** Method to determine of this argument is required for the associated script to work.
   * @return true if this argument is required, false otherwise
   */
  public boolean isRequired() {
    return required;
  }

  /** Method to allow for the activation of arguments that might never have their setValue() method called.
   * @param assigned the desired state as a boolean
   * @see org.greenstone.gatherer.util.StaticStrings#ASSIGNED_ATTRIBUTE
   * @see org.greenstone.gatherer.util.StaticStrings#FALSE_STR
   * @see org.greenstone.gatherer.util.StaticStrings#TRUE_STR
   */
  public void setAssigned(boolean assigned) {
    if(element != null) {
      element.setAttribute(StaticStrings.ASSIGNED_ATTRIBUTE, (assigned ? StaticStrings.TRUE_STR : StaticStrings.FALSE_STR));
    }
  }

  /** Sets the value of default_value.
   * @param default_value The new value for default_value as a <strong>String</strong>.
   */
  public void setDefaultValue(String default_value) {
    this.default_value = default_value;
  }

  /** Set the value of desc.
   * @param description the new value of desc as a String
   */
  public void setDescription(String description) {
    this.description = description;
  }

  /** Set the element this argument should be based upon.
   * @param element the Element
   */
  public void setElement(Element element) {
    this.element = element;
  }

  /** Mark this argument as being hidden in GLI. */
  public void setHiddenGLI(boolean hidden) {
    this.hidden_gli = hidden;
  }

  /** Set the upper bound for a range type argument.
   * @param maximum the maximum as an int
   */
  public void setMaximum(int maximum) {
    this.maximum = maximum;
  }

  /** Set the lower bound for a range type argument.
   * @param minimum the minimum as an int
   */
  public void setMinimum(int minimum) {
    this.minimum = minimum;
  }

  /** Set the detail mode level where this argument will become available.
   * @param mode_level the mode level as an int
   */
  public void setModeLevel(int mode_level) {
    this.mode_level = mode_level;
  }

  /** Set the value of name.
   * @param name the new value of name as a String
   */
  public void setName(String name) {
    this.name = name;
  }
	
  public void setDisplayName(String name) {		
    this.display_name = name; 
  }	

  /** Sets the value of the options list.
   * @param list the new options list as a HashMap
   */
  public void setOptions(ArrayList list) {
    this.option_list = list;
  }

  /** Set the owner of this argument.
   * @param owner the name of the owner of this argument as a String
   */
  public void setOwner(String owner) {
    this.owner = owner;
  }

  /** Set the value of required.
   * @param required the new value of required as a boolean
   */
  public void setRequired(boolean required) {
    this.required = required;
  }

  /** Set the value of type.
   * @param type the new value of type as an byte
   */
  public void setType(byte type) {
    this.type = type;
  }

  /** Set the value of type, by matching a type to the given string.
   * @param new_type a String which contains the name of a certain argument type
   * @see org.greenstone.gatherer.util.StaticStrings#ENUM_STR
   * @see org.greenstone.gatherer.util.StaticStrings#FLAG_STR
   * @see org.greenstone.gatherer.util.StaticStrings#HIERARCHY_STR
   * @see org.greenstone.gatherer.util.StaticStrings#INT_STR
   * @see org.greenstone.gatherer.util.StaticStrings#LANGUAGE_STR
   * @see org.greenstone.gatherer.util.StaticStrings#METADATA_TYPE_STR
   * @see org.greenstone.gatherer.util.StaticStrings#REGEXP_STR
   */
  public void setType(String new_type) {
    if(new_type.equalsIgnoreCase(StaticStrings.ENUM_STR)) {
      this.type = ENUM;
      option_list = new ArrayList();
    }
    else if(new_type.equalsIgnoreCase(StaticStrings.ENUM_STRING_STR)) {
      this.type = ENUM_STRING;
      option_list = new ArrayList();
    }
    else if(new_type.equalsIgnoreCase(StaticStrings.FLAG_STR)) {
      this.type = FLAG;
    }
    else if(new_type.equalsIgnoreCase(StaticStrings.HIERARCHY_STR)) {
      this.type = HIERARCHY;
    }
    else if(new_type.equalsIgnoreCase(StaticStrings.INT_STR)) {
      this.type = INTEGER;
    }
    else if(new_type.equalsIgnoreCase(StaticStrings.LANGUAGE_STR)) {
      this.type = LANGUAGE;
    }
    else if(new_type.equalsIgnoreCase(StaticStrings.METADATA_TYPE_STR)) {
      this.type = METADATA;
    }
    else if(new_type.equalsIgnoreCase(StaticStrings.REGEXP_STR)) {
      this.type = REGEXP;
    }
    else {
      this.type = STRING;
    }
  }

  /** Method to set the value of this argument.
   * @param value the new value for the argument
   * @see org.greenstone.gatherer.Gatherer#println
   */
  public void setValue(String value) {
    if(element != null) {
	if (type == METADATA) {
	    value = MetadataTools.convertMetadataElementListNames(value, MetadataTools.FROM_DISPLAY_NAMES);
	    stored_value = value;
	}
	XMLTools.setValue(element, value);
    }
    else {
      DebugStream.println("Argument.setValue(" + value + ") called on a base Argument.");
    }
  }

  /** Set the values vector to the given values. Currently I just assign the new values, whereas I may later want to implement a deep clone.
   * @param values an ArrayList of values
   * @see org.greenstone.gatherer.Gatherer#println
   */
  public void setValues(ArrayList values) {
    if(element != null) {
      StringBuffer value = new StringBuffer();
      int value_length = values.size();
      for(int i = 0; i < value_length; i++) {
	value.append(values.get(i));
	value.append(StaticStrings.COMMA_CHARACTER);
      }
      value.deleteCharAt(value.length() - 1); // Remove last ','
      XMLTools.setValue(element, value.toString());
    }
    else {
      DebugStream.println("Argument.setValues([" + values.size() + " items]) called on a base Argument.");
    }
  }

  /** Method for translating the data of this class into a string.
   * @return a String containing a fragment of the total arguments string
   * @see org.greenstone.gatherer.Gatherer#c_man
   * @see org.greenstone.gatherer.collection.CollectionManager#getCollection
   * @see org.greenstone.gatherer.util.StaticStrings#COMMA_CHARACTER
   * @see org.greenstone.gatherer.util.StaticStrings#NAME_ATTRIBUTE
   * @see org.greenstone.gatherer.util.StaticStrings#SPACE_CHARACTER
   * @see org.greenstone.gatherer.util.StaticStrings#SPEECH_CHARACTER
   */
  public String toString()
  {
    StringBuffer text = new StringBuffer("-");

    if (element == null) {
      return text.toString();
    }

    if (name == null) {
      name = element.getAttribute(StaticStrings.NAME_ATTRIBUTE);
    }
    text.append(name);

    // getValue returns display name for metadata args
    String value = getValue(); 
    if (value.length() == 0) {
      return text.toString();
    }

    text.append(StaticStrings.SPACE_CHARACTER);

    // If the value contains a space, add speech marks
    //   (Except for metadata elements, which won't have spaces when written out to collect.cfg)
    if (value.indexOf(StaticStrings.SPACE_CHARACTER) != -1 && type != METADATA) {
      value = StaticStrings.SPEECH_CHARACTER + value + StaticStrings.SPEECH_CHARACTER;
    }

    text.append(value);
    return text.toString();
  } 

  /** parse the <Option> XML from eg import.pl -xml or pluginfo.pl -xml */
  public void parseXML(Element option) {

    for(Node node = option.getFirstChild(); node != null; node = node.getNextSibling()) {
      String node_name = node.getNodeName();
      if(node_name.equals("Name")) {
	setName(XMLTools.getValue(node));
      }
      else if(node_name.equals("DisplayName")) {
	setDisplayName(XMLTools.getValue(node));
      }
      else if(node_name.equals("Desc")) {
	setDescription(XMLTools.getValue(node));
      }
      else if(node_name.equals("Type")) {
	setType(XMLTools.getValue(node));
      }
      else if(node_name.equals("Default")) {
	setDefaultValue(XMLTools.getValue(node));
      }
      else if(node_name.equals("Required")) {
	String v = XMLTools.getValue(node);
	if(v != null && v.equals("yes")) {
	  setRequired(true);
	}
      }
      else if(node_name.equals("List")) {
	// Two final loops are required to parse lists.
	for(Node value = node.getFirstChild(); value != null; value = value.getNextSibling()) {
	  if(value.getNodeName().equals("Value")) {
	    String key = null;
	    String desc = "";
	    for(Node subvalue = value.getFirstChild(); subvalue != null; subvalue = subvalue.getNextSibling()) {
	      node_name = subvalue.getNodeName();
	      if(node_name.equals("Name")) {
		key = XMLTools.getValue(subvalue);
	      }
	      else if(node_name.equals("Desc")) {
		desc = XMLTools.getValue(subvalue);
	      }
	    }
	    if(key != null) {
	      addOption(key, desc);
	    }
	  }
	}
      }
      else if(node_name.equals("Range")) {
	String range_raw = XMLTools.getValue(node);
	int index = -1;
	if((index = range_raw.indexOf(StaticStrings.COMMA_CHARACTER)) != -1) {
	  if(index > 0) {
	    try {
	      String first_number = range_raw.substring(0, index);
	      setMinimum(Integer.parseInt(first_number));
	      first_number = null;
	    }
	    catch(Exception exception) {
	    }
	  }

	  if(index + 1 < range_raw.length()) {
	    try {
	      String second_number = range_raw.substring(index + 1);
	      setMaximum(Integer.parseInt(second_number));
	      second_number = null;    
	    }
	    catch(Exception exception) {
	    }
	  }
	}
	// Else it wasn't a valid range anyway, so ignore it
      }
      else if(node_name.equals("HiddenGLI")) {
	setHiddenGLI(true);
      }
      else if(node_name.equals("ModeGLI")) {
	String mode_level_str = XMLTools.getValue(node);
	try {
	  int mode_level = Integer.parseInt(mode_level_str);
	  setModeLevel(mode_level);
	}
	catch(Exception exception) {
	  DebugStream.println("Exception in Argument.parseXML() - Unexpected but non-fatal");
	  DebugStream.printStackTrace(exception);
	}
      }

    } // for each option

  } // parseXML

  public class ArgumentOption 
    implements Comparable {
    public String name;
    public String description;
    private String text; // cached version

    public ArgumentOption(String name, String desc) {
      this.name = name;
      this.description = desc;
    }

    public int compareTo(Object obj) {
      return toString().compareTo(obj.toString());
    }

    public boolean equals(Object obj) {	    
      return (obj != null && compareTo(obj) == 0);
    }

    public String toString() {
      return name + " "+ StaticStrings.MINUS_CHARACTER + " "+ description;
    }

    public String getToolTip() {
      return Utility.formatHTMLWidth(name+": "+description, 80);
    }
  }
}
