package org.greenstone.gatherer.cdm;

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;

import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Map;
import java.util.Iterator;

import org.greenstone.gatherer.Configuration;
import org.greenstone.gatherer.DebugStream;
import org.greenstone.gatherer.Dictionary;
import org.greenstone.gatherer.Gatherer;
import org.greenstone.gatherer.gui.GComboBox;
import org.greenstone.gatherer.util.StaticStrings;
import org.greenstone.gatherer.util.Utility;
import org.greenstone.gatherer.metadata.MetadataElement;
import org.greenstone.gatherer.metadata.MetadataSet;
import org.greenstone.gatherer.metadata.MetadataSetManager;
import org.greenstone.gatherer.metadata.MetadataTools;

/** This class encapsulates all the technical difficulty of creating a specific control based on an Argument. */
public class ArgumentControl
  extends JPanel {

  static protected Dimension LABEL_SIZE = new Dimension(175, 25);
  static protected Dimension ROW_SIZE = new Dimension(400, 30);
  /** The Argument this control will be based on. */
  private Argument argument = null;

  /** A checkbox to allow enabling or diabling of this Argument. */
  private JCheckBox enabled = null;
  /** Some form of editor component, such as a JComboBox or JTextField, used to set parameters to an Argument. */
  private JComponent value_control = null;

  /** Constructor.
   */
  public ArgumentControl(Argument argument, boolean is_enabled, String preset_value) {
    this.setComponentOrientation(Dictionary.getOrientation());
    this.argument = argument;

    String tip = "<html>" + argument.getName()+": "+argument.getDescription() + "</html>";
    tip = Utility.formatHTMLWidth(tip, 80);

    setBackground(Configuration.getColor("coloring.collection_tree_background", false));
    setBorder(BorderFactory.createEmptyBorder(2,0,2,0));
    setLayout(new BorderLayout());
    setPreferredSize(ROW_SIZE);
		
    // display the name set in the disp option if there is one
    // otherwise display name option value instead
    String dispName = argument.getDisplayName();
    if(dispName.equals("")){
      dispName = argument.getName();
    }

    if (argument.isRequired()) {		
      //JLabel label = new JLabel(argument.getName());
      JLabel label = new JLabel(dispName);
      label.setComponentOrientation(Dictionary.getOrientation());
      label.setOpaque(false);
      label.setPreferredSize(LABEL_SIZE);
      label.setToolTipText(tip);
      add(label, BorderLayout.LINE_START);
    } else {
      //enabled = new JCheckBox(argument.getName());
      enabled = new JCheckBox(dispName);
      enabled.setComponentOrientation(Dictionary.getOrientation());
      enabled.setOpaque(false);
      enabled.setPreferredSize(LABEL_SIZE);
      enabled.setToolTipText(tip);
      add(enabled, BorderLayout.LINE_START);
    }

    String initial_value;
    if (preset_value != null && !preset_value.equals("")) {
      initial_value = preset_value;
    }
    else {
      initial_value = argument.getValue();
    }
    if (initial_value == null || initial_value.equals("")) {
      initial_value = argument.getDefaultValue();
    }
    if (initial_value == null) {
      initial_value = "";
    }

    switch(argument.getType()) {
    case Argument.ENUM_STRING:
    case Argument.ENUM:
      ArrayList option_list = argument.getOptions();
      boolean editable = false;
      if (argument.getType() == Argument.ENUM_STRING) {
	editable = true;
      }
      value_control = new GComboBox(option_list.toArray(), editable, false);
      value_control.setComponentOrientation(Dictionary.getOrientation());
			
      boolean found = selectValue((JComboBox)value_control, initial_value); // also sets the tooltip
      if (!found && argument.getType() == Argument.ENUM_STRING) {
	// Its a custom item
	((JComboBox) value_control).addItem(initial_value);
	((JComboBox) value_control).setSelectedItem(initial_value);
      }

      ((JComboBox)value_control).addActionListener(new ToolTipUpdater());
      break;

    case Argument.FLAG:
      // Only need the check box.
      break;

    case Argument.INTEGER:
      // Build a spinner
      int initial_int=0;
      // If there was an original value, set it.
      try {
	initial_int = Integer.parseInt(initial_value);
      } catch (Exception error) {
	DebugStream.println("ArgumentControl Error: "+error);
      }
      if (initial_int < argument.getMinimum()) {
	initial_int = argument.getMinimum();
      } else if (initial_int > argument.getMaximum()) {
	initial_int = argument.getMaximum();
      }

      JSpinner spinner = new JSpinner(new SpinnerNumberModel(initial_int, argument.getMinimum(), argument.getMaximum(), 1));
      spinner.setComponentOrientation(Dictionary.getOrientation());
      // And remember it
      value_control = spinner;
      break;

    case Argument.REGEXP:
    case Argument.STRING:
      value_control = new JTextField(initial_value);
      value_control.setComponentOrientation(Dictionary.getOrientation());
      break;

    case Argument.LANGUAGE:
      value_control = new GComboBox(CollectionDesignManager.language_manager.getLanguageCodes().toArray(), false);
      value_control.setComponentOrientation(Dictionary.getOrientation());
      // we want to display the language name not the code
      ((JComboBox)value_control).setRenderer(new LanguageListCellRenderer());
      // Now ensure we have the existing value or default value selected if either exist in our known languages
      String lang_name = CollectionDesignManager.language_manager.getLanguageName(initial_value);
      if (lang_name != null) {
	((JComboBox)value_control).setSelectedItem(initial_value);
      }
      break;

    case Argument.METADATA:
      value_control = new GComboBox(MetadataSetManager.getEveryMetadataSetElement(), false);
      value_control.setComponentOrientation(Dictionary.getOrientation());
      // Editable for advanced modes (allows things like dc.Title,ex.Title)
      if (Configuration.getMode() > Configuration.ASSISTANT_MODE) {
	((JComboBox) value_control).setEditable(true);
      }
      // Now ensure we have the existing value or default value selected if either exist.
      String existing_value = preset_value;
      if (existing_value == null || existing_value.equals("")) {
	existing_value = argument.getValue();
      }
      if (existing_value == null || existing_value.equals("")) {
	// try default value
	String default_value = argument.getDefaultValue();
	if (default_value != null) {
	  // if no namespace for default value, add ex.
	  // won't work if we want to set a non-metadata value
	  if (MetadataTools.getMetadataSetNamespace(default_value).equals("")) {
	    default_value = StaticStrings.EXTRACTED_NAMESPACE+default_value;
	  }
	  existing_value = default_value;
	}
      }
	
      if (existing_value != null && !existing_value.equals("")) {
	
	found = selectValue((JComboBox) value_control, existing_value);
	// It's possible that this is a custom value and so doesn't exist in the combobox
	if (!found) {
	  // If so, add it then select it
	  ((JComboBox) value_control).addItem(existing_value);
	  ((JComboBox) value_control).setSelectedItem(existing_value);
	}
      }
      break;

    case Argument.METADATA_SET_NAMESPACE:
      value_control = new JComboBox();
      value_control.setComponentOrientation(Dictionary.getOrientation());
      // !! Hack for exploding metadata databases: add the (empty) exploded metadata set
      File exploded_metadata_set_file = new File(Gatherer.getGLIMetadataDirectoryPath() + "exp" + StaticStrings.METADATA_SET_EXTENSION);
      MetadataSet exploded_metadata_set = new MetadataSet(exploded_metadata_set_file);
      Gatherer.c_man.importMetadataSet(exploded_metadata_set);

      // All the loaded metadata sets except the extracted metadata set are applicable
      ArrayList metadata_sets = MetadataSetManager.getMetadataSets();
      for (int i = metadata_sets.size() - 1; i >= 0; i--) {
	MetadataSet metadata_set = (MetadataSet) metadata_sets.get(i);
	if (!(metadata_set.getNamespace().equals(MetadataSetManager.EXTRACTED_METADATA_NAMESPACE))) {
	  ((JComboBox)value_control).addItem(metadata_set);
	}
      }

      selectValue((JComboBox) value_control, initial_value);

    } // end of switch

    // Enable or disable as necessary.
    if(argument.isRequired() || argument.isAssigned() || is_enabled) {
      if (enabled != null) {
	enabled.setSelected(true);
      }
      if(value_control != null) {
	value_control.setOpaque(true);
	value_control.setBackground(Color.white);
	value_control.setEnabled(true);
	if(value_control instanceof JSpinner) {
	  // Set enabled
	  JComponent c = ((JSpinner)value_control).getEditor();
	  if ( c instanceof JSpinner.DefaultEditor ) {
	    JSpinner.DefaultEditor editor = (JSpinner.DefaultEditor) c;
	    JFormattedTextField field = editor.getTextField();
	    field.setEditable(true);
	    field.setBackground(Color.white);
	  }
	}
      }
    }
    else {
      if (enabled != null) {
	enabled.setSelected(false);
      }
      if(value_control != null) {
	value_control.setOpaque(true);
	value_control.setBackground(Color.lightGray);
	value_control.setEnabled(false);
	if(value_control instanceof JSpinner) {
	  // Set enabled
	  JComponent c = ((JSpinner)value_control).getEditor();
	  if ( c instanceof JSpinner.DefaultEditor ) {
	    JSpinner.DefaultEditor editor = (JSpinner.DefaultEditor) c;
	    JFormattedTextField field = editor.getTextField();
	    field.setEditable(false);
	    field.setBackground(Color.lightGray);
	  }
	}
      }
    }

    // Listener
    if(value_control != null) {
      if (argument.getType() != Argument.ENUM && argument.getType() != Argument.ENUM_STRING) {
	// enums have already set tooltips based on option value
	value_control.setToolTipText(tip);
      }
      add(value_control, BorderLayout.CENTER);
      if (!argument.isRequired()) {
	enabled.addActionListener(new EnabledListener(value_control));
      }
    }
  }

  public Argument getArgument() {
    return argument;
  }

  public String getArgumentName() {
    return argument.getName();
  }

  public String getValue() {
    if(value_control == null) {
      return null;
    }
    if (value_control instanceof JSpinner) {
      return ((JSpinner)value_control).getValue().toString();
    }
    if(value_control instanceof JComboBox) {
      Object selected_item = ((JComboBox)value_control).getSelectedItem();
      if (selected_item != null) {
	if (argument.getType() == Argument.METADATA_SET_NAMESPACE) {
	  return ((MetadataSet) selected_item).getNamespace();
	}
	if (selected_item instanceof Argument.ArgumentOption) {
	  return ((Argument.ArgumentOption)selected_item).name;
	}
	if (selected_item instanceof MetadataElement) {
	  return ((MetadataElement) selected_item).getFullName();
	}
	return selected_item.toString();
      }    
      return null;
    }
    if(value_control instanceof JTextField) {
      return ((JTextField)value_control).getText();
    }
    return null;
  }
  /** Retrieve the control used for storing values.
   * @return JComponent
   */
  public JComponent getValueControl() {
    return value_control;
  }

  public boolean isEnabled() {
    if (enabled == null) {
      return true; // always enabled
    } 
    return enabled.isSelected();
  }


  /** Identifies this control by returning the name of the Argument it is based on.
   * @return The name of the Argument as a <strong>String</strong>.
   * @see org.greenstone.gatherer.cdm.Argument
   */
  public String toString() {
    return argument.getName();
  }
  /** Updates the enwrapped Argument using the values provided by the controls.
   * @return <i>true</i> if the update was successful, <i>false</i> otherwise.
   * @see org.greenstone.gatherer.cdm.ArgumentConfiguration.Argument.ArgumentOption
   * @see org.greenstone.gatherer.cdm.Language
   */
  public boolean updateArgument() {
    if(argument.isRequired() || enabled.isSelected() ) {
      argument.setAssigned(false);
      String result = null;
      switch(argument.getType()) {
      case Argument.ENUM:
	Argument.ArgumentOption option = (Argument.ArgumentOption)((JComboBox)value_control).getSelectedItem();
	// its impossible not to choose an entry
	argument.setValue(option.name);
	argument.setAssigned(true);
	return true;
      case Argument.ENUM_STRING:
	Object new_value_raw = ((JComboBox)value_control).getSelectedItem();
	if (new_value_raw instanceof Argument.ArgumentOption) {
	  argument.setValue(((Argument.ArgumentOption)new_value_raw).name);
	} else {
	  // have entered a new string
	  String new_value = new_value_raw.toString();
	  if (new_value.length() > 0) {
	    argument.setValue(new_value);
	  }
	  else {
	    String args[] = new String[1];
	    args[0] = argument.getName();
	    if(argument.isRequired()) {
	      JOptionPane.showMessageDialog(this, Dictionary.get("CDM.ArgumentConfiguration.Required_Argument", args), Dictionary.get("CDM.ArgumentConfiguration.Error_Title"), JOptionPane.ERROR_MESSAGE);
	    }
	    // They've left the field blank
	    else {
	      JOptionPane.showMessageDialog(this, Dictionary.get("CDM.ArgumentConfiguration.No_Value", args), Dictionary.get("CDM.ArgumentConfiguration.Error_Title"), JOptionPane.ERROR_MESSAGE);
	      argument.setValue(null);
	    }
	    args = null;
	    return false;
	  }
	}
	argument.setAssigned(true);
	return true;
      case Argument.FLAG:
	// Should have already been handled above.
	argument.setAssigned(true);
	return true;
      case Argument.INTEGER:
	result = ((JSpinner)value_control).getValue().toString();
	if(result.length() > 0) {
	  // Test if the value entered is a valid int.
	  try {
	    int x = Integer.parseInt(result);
	  }
	  catch(NumberFormatException nfe) {
	    String args[] = new String[2];
	    args[0] = argument.getName();
	    args[1] = result;
	    JOptionPane.showMessageDialog(this, Dictionary.get("CDM.ArgumentConfiguration.Bad_Integer", args), Dictionary.get("CDM.ArgumentConfiguration.Error_Title"), JOptionPane.ERROR_MESSAGE);
	    args = null;
	    return false;
	  }
	  argument.setValue(result);
	}
	else {
	  String args[] = new String[1];
	  args[0] = argument.getName();
	  if(argument.isRequired()) {
	    JOptionPane.showMessageDialog(this, Dictionary.get("CDM.ArgumentConfiguration.Required_Argument", args), Dictionary.get("CDM.ArgumentConfiguration.Error_Title"), JOptionPane.ERROR_MESSAGE);
	  }
	  // They've left the field blank
	  else {
	    JOptionPane.showMessageDialog(this, Dictionary.get("CDM.ArgumentConfiguration.No_Value", args), Dictionary.get("CDM.ArgumentConfiguration.Error_Title"), JOptionPane.ERROR_MESSAGE);
	    argument.setValue(null);
	  }
	  args = null;
	  return false;
	}
	argument.setAssigned(true);
	return true;
      case Argument.LANGUAGE:
	String language = (((JComboBox)value_control).getSelectedItem()).toString();
	argument.setValue(language);
	// Kinda lucked out here. Its impossible not to choose an entry from these comboboxes as they are restricted.
	argument.setAssigned(true);
	return true;
      case Argument.METADATA:
	  String meta_value = (((JComboBox)value_control).getSelectedItem()).toString();
	  if(meta_value.length() > 0) {
	      
	      argument.setValue(meta_value);
	  }
	  else {
	    String args[] = new String[1];
	    args[0] = argument.getName();
	    if(argument.isRequired()) {
	      JOptionPane.showMessageDialog(this, Dictionary.get("CDM.ArgumentConfiguration.Required_Argument", args), Dictionary.get("CDM.ArgumentConfiguration.Error_Title"), JOptionPane.ERROR_MESSAGE);
	    }
	    // They've left the field blank
	    else {
	      JOptionPane.showMessageDialog(this, Dictionary.get("CDM.ArgumentConfiguration.No_Value", args), Dictionary.get("CDM.ArgumentConfiguration.Error_Title"), JOptionPane.ERROR_MESSAGE);
	      argument.setValue(null);
	    }
	    args = null;
	    return false;
	  }
      
	argument.setAssigned(true);
	return true;
      case Argument.REGEXP:
      case Argument.STRING:
	result = ((JTextField)value_control).getText();
	if(result.length() > 0) {
	  argument.setValue(result);
	}
	else {
	  String args[] = new String[1];
	  args[0] = argument.getName();
	  if(argument.isRequired()) {
	    JOptionPane.showMessageDialog(this, Dictionary.get("CDM.ArgumentConfiguration.Required_Argument", args), Dictionary.get("CDM.ArgumentConfiguration.Error_Title"), JOptionPane.ERROR_MESSAGE);
	  }
	  // They've left the field blank
	  else {
	    JOptionPane.showMessageDialog(this, Dictionary.get("CDM.ArgumentConfiguration.No_Value", args), Dictionary.get("CDM.ArgumentConfiguration.Error_Title"), JOptionPane.ERROR_MESSAGE);
	    argument.setValue(null);
	  }
	  args = null;
	  return false;
	}
	argument.setAssigned(true);
	return true;
      }
      return false;
    }
    else {
      argument.setAssigned(false);
      return true;
    }
  }


  public boolean updateArgument(boolean checkRequired) {


    if (checkRequired){
      return updateArgument();
    }
    else{
      if (argument.getType() == Argument.STRING){
	String result = ((JTextField)value_control).getText();

	if(result.length() > 0) {
	  argument.setValue(result);
	  argument.setAssigned(true);                    
	}
      }	
    }

    return true;

  }



  /** Method to ensure that a certain value is selected, if it exists within that combobox to begin with.
   * @param combobox The <strong>JComboBox</strong> whose selection we are trying to preset.
   * @param target The desired value of the selection as a <strong>String</strong>.
   * @return true if the item was found and selected, false otherwise
   * @see org.greenstone.gatherer.cdm.ArgumentConfiguration.Argument.ArgumentOption
   */
  public static boolean selectValue(JComboBox combobox, String target)
  {
    for (int i = 0; i < combobox.getItemCount(); i++) {
      Object object = combobox.getItemAt(i);

      if (object instanceof Argument.ArgumentOption) {
	Argument.ArgumentOption opt = (Argument.ArgumentOption) object;
	if (opt.name.startsWith(target)) {
	  combobox.setSelectedIndex(i);
	  combobox.setToolTipText(opt.getToolTip());
	  return true;
	}
      }
      else if (object instanceof MetadataElement) {
	if(((MetadataElement)object).getFullName().equals(target)) {
	  combobox.setSelectedIndex(i);
	  return true;
	}
      }
      else if (object.toString().equals(target)) {
	combobox.setSelectedIndex(i);
	return true;
      }
    }

    return false;
  }


  /** Forces the control into an 'enabled' mode. */
  public void setEnabled() {
    enabled.setSelected(true);
  }
  /** Explicitly sets the value of a JTextField type control to the given String.
   * @param value_str The new value of the control as a <strong>String</strong>.
   */
  public void setValue(String value_str) {
    ((JTextField)value_control).setText(value_str);
  }

  /** Listens for actions apon the enable checkbox, and if detected enables or diables control appropriately. */
  private class EnabledListener
    implements ActionListener {
    /** An editor component, such as a JComboBox or JTextField, that might have its enabled state changed by this listener. */
    private JComponent target = null;

    /** Constructor. */
    public EnabledListener(JComponent target) {
      this.target = target;
    }

    /** Any implementation of ActionListener must include this method so that we can be informed when an action has been performed on or registered check box, prompting us to change the state of the other controls as per the users request.
     * @param event An <strong>ActionEvent</strong> containing information about the click.
     */
    public void actionPerformed(ActionEvent event) {
      if (this.target == null) {
	return;
      }
      JCheckBox source = (JCheckBox)event.getSource();
      if(source.isSelected()) {
	target.setBackground(Color.white);
	target.setEnabled(true);
      }
      else {
	target.setBackground(Color.lightGray);
	target.setEnabled(false);
      }
      // Special case of stupid JSpinners who don't let their backgrounds change properly.
      if(target instanceof JSpinner) {
	JSpinner spinner = (JSpinner) target;
	JComponent c = spinner.getEditor();
	if ( c instanceof JSpinner.DefaultEditor ) {
	  JSpinner.DefaultEditor editor = (JSpinner.DefaultEditor) c;
	  JFormattedTextField field = editor.getTextField();
	  field.setEditable(source.isSelected());
	  if(source.isSelected()) {
	    field.setBackground(Color.white);
	  }
	  else {
	    field.setBackground(Color.lightGray);
	  }
	}
      }
    }
  }


  /** Listener that sets the tooltip associated to a combobox to the tooltip relevant to the selected item. */
  private class ToolTipUpdater
    implements ActionListener {
    /** Any implementation of an ActionListener must include this method so that we can be informed when the selection in a combobox has changed and update the tooltip accordingly.
     * @param event An <strong>ActionEvent</strong> containing information about the action that fired this call.
     */
    public void actionPerformed(ActionEvent event) {
      JComboBox source = (JComboBox)event.getSource();
      Object object = source.getSelectedItem();
      if(object instanceof Argument.ArgumentOption) {
	Argument.ArgumentOption opt = (Argument.ArgumentOption)object;
	if(opt != null) {
	  source.setToolTipText(opt.getToolTip());
	}
	else {
	  source.setToolTipText(StaticStrings.EMPTY_STR);
	}
      }
    }
  }
}
