package ca.ucalgary.seahawk.gui;

import ca.ucalgary.seahawk.services.MobyClient;
import ca.ucalgary.seahawk.util.HTMLUtils;
import org.biomoby.shared.data.*;
import org.biomoby.shared.*;
import java.awt.*;
import javax.swing.*;
import javax.swing.text.JTextComponent;
import java.awt.event.*;
import java.util.*;

/**
 * A class that generates a GUI to fill in (instantiate or change) the values of Secondary Input to MOBY services.
 */

public class MobySecondaryInputGUI extends JDialog implements ActionListener, Runnable {
    public final static int MAX_TOOLTIP_WIDTH = 40;
    public final static int INT_CHOICE_MAX = 100;
    public final static int DEFAULT_TEXT_SIZE = 12;
    public final static String UNBOUNDED_INT_RANGE_DESC = "[integer]";
    public final static String UNBOUNDED_FLOAT_RANGE_DESC = "[decimal number]";
    public final static String TITLE = "Inputs for Moby Service";
    public final static String OK_BUTTON_NAME = "MSIGexecuteButton";

    private MobyPrimaryData[] inputsPrimary = null;
    private MobyDataSecondaryInstance[] inputs = null;
    private JButton cancelButton;
    private String cancelMessage = "Cancel";
    private JButton confirmButton;
    private String confirmMessage = "Execute Service";
    private JPanel optionsPanel;

    private Map<MobyDataSecondaryInstance,JComponent> data2widget;
    private ActionListener listener;
    private int handlerCode = 0;  // callback event ID provided by caller to c-tor
    private boolean showingNew = false;
    private MobyClient mobyClient;

    private static org.apache.log4j.Logger logger = org.apache.log4j.Logger.getLogger(MobySecondaryInputGUI.class);

    public MobySecondaryInputGUI(ActionListener al, Frame owner, boolean modal, MobyClient client){
	this(al, owner, modal, 0, client);
    }

    /**
     * @param al the object to receive the "all done" callback 
     * @param actionCommandID the id to give back as the event ID during callback 
     */
    public MobySecondaryInputGUI(ActionListener al, Frame owner, boolean modal, int actionCommandID, MobyClient client){
	super(owner, modal);
 	setup();
	listener = al;
	handlerCode = actionCommandID;
	mobyClient = client;
	setVisible(false);
        showingNew = false;
	setName(TITLE);
    }

    public MobySecondaryInputGUI(ActionListener al, MobyClient client){
	this(al, 0, client);
    }

    public MobySecondaryInputGUI(ActionListener al, int actionCommandID, MobyClient client){
	super();
	setup();
	listener = al;
	mobyClient = client;
	handlerCode = actionCommandID;
	setVisible(false);
    }

    protected void setup(){
	setTitle(TITLE);
	setDefaultCloseOperation(DISPOSE_ON_CLOSE);

	data2widget = new HashMap<MobyDataSecondaryInstance,JComponent>();

	optionsPanel = new JPanel();
	// Layout widgets top to bottom in one column
	optionsPanel.setLayout(new GridLayout(0,1));
	Container mainPane = getContentPane();
	mainPane.setLayout(new BorderLayout());
	mainPane.add(optionsPanel, BorderLayout.NORTH);
	JPanel okCancelPanel = new JPanel();
	BoxLayout bl = new BoxLayout(okCancelPanel, BoxLayout.LINE_AXIS);
	cancelButton = new JButton(cancelMessage);
	cancelButton.addActionListener(this);
	okCancelPanel.add(cancelButton);
	confirmButton = new JButton(confirmMessage);
	confirmButton.addActionListener(this);
	confirmButton.setName(OK_BUTTON_NAME);
	okCancelPanel.add(confirmButton);
	mainPane.add(okCancelPanel, BorderLayout.SOUTH);

	// Make OK button get the focus whenever frame is activated.
	addWindowListener(new WindowAdapter() {
		public void windowActivated(WindowEvent e) {
		    confirmButton.requestFocusInWindow();
		}
	    });
	// Hitting 'enter' causes OK button to be pressed
	getRootPane().setDefaultButton(confirmButton);
    }

    /**
     * Attempts to fill in all default values for automated submissions
     * @return true if all defaults are valid, otherwise false (should show GUI)
     */
    public boolean defaultFillIn(MobyDataSecondaryInstance[] secondaryInputs){
	fillIn(secondaryInputs, false);
	return populateDataInstances();
    }

    /**
     * Calling this method makes a GUI pop up that corresponds to the required input parameters.
     * This method will perform a callback to the MOBY service requester if the user fills in all 
     * required values.
     *
     * @param secondaryInputs the Moby Data Instances whose values should be filled in
     */
    public void fillIn(MobyDataSecondaryInstance[] secondaryInputs){
	fillIn(secondaryInputs, true);
    }

    public void fillIn(MobyDataSecondaryInstance[] secondaryInputs, boolean showGUI){
	fillIn(null, secondaryInputs, showGUI);
    }

    public void fillIn(MobyPrimaryData[] primaryInputs, MobyDataSecondaryInstance[] secondaryInputs, boolean showGUI){
	// If more than 1 primary input parameter, need to disable
	// submission until these fields are filled in too.
	if(!setupPrimaryInput(primaryInputs) && secondaryInputs == null){
	    run();
	    //should do callback
	    return;
	}

	inputs = secondaryInputs;

	// Create a thread here to prevent possible AWT-EventQueue deadlock
	// when widgets are added and the thread of caller of the method was
	// the AWT-EventQueue.
	WidgetCreatorThread widgetMaker = new WidgetCreatorThread(showGUI, true);
	if(!showGUI){
	    // Call synchronous to ensure data is present before returning
	    widgetMaker.run();
	}
	else{
	    // Call ansynchronous to avoid deadlock
	    widgetMaker.start();
	}
	
    }

    /**
     * Is there more than one primary parameter?  Do we need to populate it?
     * @return true if we can submit the job as is, otherwise false (data still needs to be filled in)
     */
    protected boolean setupPrimaryInput(MobyPrimaryData[] primaryInputs){
	confirmButton.setEnabled(true);  //unless otherwise noted, we can submit
	if(primaryInputs != null){
	    inputsPrimary = primaryInputs;
	    for(MobyPrimaryData primary: inputsPrimary){
		if(!(primary instanceof MobyDataInstance)){
		    // a primary parameter has not yet been instantiated
		    confirmButton.setEnabled(false);
		}
	    }
	}
	return confirmButton.isEnabled();
    }

    class WidgetCreatorThread extends Thread{
	private boolean showGUI;
	private boolean showSinglePrimaryInput;

	public WidgetCreatorThread(boolean gui, boolean single){
	    showGUI = gui;
	    showSinglePrimaryInput = single;
	}

	public void run(){
	    // Clear the current GUI
	    optionsPanel.removeAll();
	    if(showGUI){
		setVisible(true);
	    }

	    if(!confirmButton.isEnabled() || showSinglePrimaryInput){
		//java.util.Arrays.sort(inputsPrimary);
		for(MobyPrimaryData input: inputsPrimary){ 
		    optionsPanel.add(makeWidget(input));
		}

		// Visually separate the primary and secondary inputs
		optionsPanel.add(new JSeparator());
	    }

	    // Sort the parameters alphabetically
	    Arrays.sort(inputs); // MobyDataSecondaryInstance implements Comparable

	    // Add a widget for each secondary param
	    for(MobyDataSecondaryInstance input: inputs){ 
		optionsPanel.add(makeWidget(input));
	    }

	    // Make the window the preferred size of its widget contents
	    if(showGUI){
		pack();
		toFront();
		requestFocus();
		showingNew = true;
		confirmButton.requestFocusInWindow();
	    }
        }
    }

    public boolean isNewShowing(){
	boolean ret = showingNew;
	if(ret){
	    showingNew = false;
	}
	return ret;
    }

    public Component makeWidget(MobyPrimaryData data){
	boolean DATA_EDITABLE = true;
	MobyDataObjectWidget primaryDataWidget = null;
	// Choose c-tor based on if param is instantiated or not
	if(data instanceof MobyDataInstance){
	    primaryDataWidget = new MobyDataObjectWidget(data.getName(), mobyClient, (MobyDataInstance) data, DATA_EDITABLE);
	}
	else{
	    primaryDataWidget = new MobyDataObjectWidget(data.getName(), mobyClient, data);
	}
	// We want to get informed of updates to the data, as this
	// may change whether the dialog should be submittable or not
	primaryDataWidget.addActionListener(this);
	return primaryDataWidget;
    }

    public Component makeWidget(MobyDataSecondaryInstance msdi){
	String dataType = msdi.getDataType();

	String[] enumeratedValues = msdi.getAllowedValues();
	// An enumeration trumps all other input types
	if(enumeratedValues != null && enumeratedValues.length != 0){
	    return makeEnumWidget(msdi);
	}
	// Otherwise build a widget based on the data type
	else if(MobySecondaryData.INTEGER_TYPE.equals(dataType)){
	    return makeIntWidget(msdi);
	}
	else if(MobySecondaryData.FLOAT_TYPE.equals(dataType)){
	    return makeFloatWidget(msdi);
	}
	else if(MobySecondaryData.STRING_TYPE.equals(dataType)){
	    return makeStringWidget(msdi);
	}
	else if(MobySecondaryData.DATETIME_TYPE.equals(dataType)){
	    return makeDateTimeWidget(msdi);
	}
	else if(MobySecondaryData.BOOLEAN_TYPE.equals(dataType)){
	    return makeBooleanWidget(msdi);
	}
	else{
	    logger.warn("Unrecognized secondary input data type (" + dataType + ") in " + getClass());
	    return null;
	}
    }

    public Component makeEnumWidget(MobyDataSecondaryInstance msdi){
	String defaultValue = msdi.getDefaultValue();
	if(defaultValue == null){
	    defaultValue = "";
	}

	JPanel widget = new JPanel();
	String[] values = msdi.getAllowedValues();
	
	widget.add(makeJLabel(msdi));
	JComboBox options = new JComboBox();
	options.setSelectedIndex(-1);  // None selected at the start
	for(int i = 0; i < values.length; i++){
	    options.addItem(values[i]);
	    if(defaultValue.equals(values[i])){
		options.setSelectedIndex(i);
	    }
	}
	data2widget.put(msdi, options);
	widget.add(options);

	return widget;
    }

    public Component makeBooleanWidget(MobyDataSecondaryInstance msdi){
	String defaultValue = msdi.getDefaultValue();
	if(defaultValue == null){
	    defaultValue = "false";
	}

	JPanel widget = new JPanel();	
	widget.add(makeJLabel(msdi));
	JCheckBox checkbox = new JCheckBox();
	checkbox.setSelected(defaultValue.toLowerCase().equals("true") || 
			     defaultValue.toLowerCase().equals("yes") || 
			     defaultValue.equals("1") || 
			     defaultValue.equals("T") || 
			     defaultValue.equals("Y"));  
	data2widget.put(msdi, checkbox);
	widget.add(checkbox);

	return widget;
    }

    public Component makeStringWidget(MobyDataSecondaryInstance msdi){
	String defaultValue = msdi.getDefaultValue();
	if(defaultValue == null){
	    defaultValue = "";
	}

	JPanel widget = new JPanel();
	widget.add(makeJLabel(msdi));
	JTextField stringGUI = new JTextField(defaultValue, 
					      defaultValue.length() > 0 ? 
					      defaultValue.length() : 
					      DEFAULT_TEXT_SIZE);
	data2widget.put(msdi, stringGUI);
	widget.add(stringGUI);
	return widget;
    }

    public Component makeIntWidget(MobyDataSecondaryInstance msdi){
	String name = msdi.getName();
	// Should change to BigInteger eventually...
	int min = Integer.MIN_VALUE;
        int max = Integer.MAX_VALUE;
        if(msdi.getMinValue() != null && msdi.getMinValue().length() != 0){
          try{min = Integer.parseInt(msdi.getMinValue());}
          catch(NumberFormatException nfe){
              logger.error("NumberFormatException: Warning: minimum value for secondary input " + name +
                           " could not be parse into an integer as required, using " + Integer.MIN_VALUE);
          }
        }
        if(msdi.getMaxValue() != null && msdi.getMaxValue().length() != 0){
          try{max = Integer.parseInt(msdi.getMaxValue());}
          catch(NumberFormatException nfe){
              logger.error("NumberFormatException: Warning: maximum value for secondary input " + name +
                           " could not be parse into an integer as required, using " + Integer.MAX_VALUE);
          }
        }
	int defaultValue = 0;
	if(msdi.getDefaultValue() == null || msdi.getDefaultValue().length() == 0){
	    logger.warn("Warning: default value for secondary input " + name + 
			       " was blank, which is not allowed by the MOBY API, using default of 0");
	}
	try{
	    Integer.parseInt(msdi.getDefaultValue());  // assumes radix of 10 of course
	}
	catch(NumberFormatException nfe){
	    logger.error("NumberFormatException: Warning: default value for secondary input " + name + 
			       " could not be parse into an integer as required, using default of 0");
	}
	// Sanity checks
	if(min > max){
	    logger.warn("Warning: swapping min and max values for secondary input " + name +
			", since the specified min (" + min + 
			") is larger than the specified max (" + max + ")");
	    int tmp = min;
	    min = max;
	    max = tmp;
	}

	if(defaultValue < min || defaultValue > max){
	    logger.warn("Warning: ignoring default value " + defaultValue + " for secondary input " +
			name + ", it is outside the valid range of [" + min + "," + max + "]");
	    if(defaultValue < min){
		defaultValue = min;
	    }
	    if(defaultValue > max){
		defaultValue = max;
	    }
	}

	String rangedesc = "";
	if(min == Integer.MIN_VALUE && max == Integer.MAX_VALUE){
	    // No real range defined
	    rangedesc = UNBOUNDED_INT_RANGE_DESC;
	}
	else{
	    rangedesc = "["+min+","+max+"]";
	}
	
	JPanel widget = new JPanel();
	// Too many to display as a dropdown list
	if(max - min > INT_CHOICE_MAX || rangedesc.equals(UNBOUNDED_INT_RANGE_DESC)){
	    widget.add(makeJLabel(msdi, rangedesc));
	    JTextField stringGUI = new JTextField(""+defaultValue, (""+max).length());
	    data2widget.put(msdi, stringGUI);
	    widget.add(stringGUI);
	    // should add input checker
	}
	else{
	    widget.add(makeJLabel(msdi));
	    JComboBox options = new JComboBox();
	    for(int i = min; i <= max; i++){
		options.addItem(new Integer(i));
	    }
	    options.setSelectedIndex(defaultValue-min);
	    data2widget.put(msdi, options);
	    widget.add(options);
	}
	return widget;
    }

    public Component makeFloatWidget(MobyDataSecondaryInstance msdi){
	String name = msdi.getName();
	// Note: jMOBY API does not yet support floating point min and max values
	double min = Double.MIN_VALUE;
        if(msdi.getMinValue() != null && msdi.getMinValue().length() > 0){
          try{Double.parseDouble(msdi.getMinValue());}
          catch(NumberFormatException nfe){
            logger.error("NumberFormatException: Minimum value (" + msdi.getMinValue() +
                         ") for decimal-format MOBY Secondary Input '" +
                         name + "' was not a decimal number, setting minimum to " +
                         Double.MIN_VALUE + ". Error was: " + nfe);
          }
        }
	double max = Double.MAX_VALUE;
        if(msdi.getMaxValue() != null && msdi.getMaxValue().length() > 0){
          try{Double.parseDouble(msdi.getMaxValue());}
          catch(NumberFormatException nfe){
            logger.error("NumberFormatException: Maximum value (" + msdi.getMaxValue() +
                         ") for decimal-format MOBY Secondary Input '" +
                         name + "' was not a decimal number, setting minimum to " +
                         Double.MAX_VALUE + ". Error was: " + nfe);
          }
        }
	String defaultValueString = msdi.getDefaultValue();
	double defaultValue = 0.0;
	if(defaultValueString != null){
	    try{
		defaultValue = Double.parseDouble(defaultValueString);  // assumes radix of 10 of course
	    }
	    catch(NumberFormatException nfe){
		logger.error("NumberFormatException: Default value (" + defaultValueString + 
			    ") for decimal-format MOBY Secondary Input '" + 
			    name + "' was not a decimal number, setting default initially to " + 
			    defaultValue + ". Error was: " + nfe);
	    }
	}

	// Sanity checks
	if(min > max){
	    logger.warn("Warning: swapping min and max values for secondary input " + name +
			", since the specified min (" + min + 
			") is larger than the specified max (" + max + ")");
	    double tmp = min;
	    min = max;
	    max = tmp;
	}
	if(defaultValue < min || defaultValue > max){
	    logger.warn("Warning: ignoring default value " + defaultValue + " for secondary input " +
			name + ", it is outside the valid range of [" + min + "," + max + "]");
	    if(defaultValue < min){
		defaultValue = min;
	    }
	    if(defaultValue > max){
		defaultValue = max;
	    }
	}

	String rangedesc = "";
	if(min == Integer.MIN_VALUE && max == Integer.MAX_VALUE){
	    // No real range defined
	    rangedesc = UNBOUNDED_FLOAT_RANGE_DESC;
	}
	else{
	    rangedesc = "["+min+","+max+"]";
	}

	JPanel widget = new JPanel();
	widget.add(makeJLabel(msdi, rangedesc));
	JTextField stringGUI = new JTextField(""+defaultValue, (""+max).length());
	data2widget.put(msdi, stringGUI);
	widget.add(stringGUI);
	return widget;	
    }

    public JLabel makeJLabel(MobyDataSecondaryInstance msdi){
	return makeJLabel(msdi, null);
    }

    public JLabel makeJLabel(MobyDataSecondaryInstance msdi, String range){
	JLabel label = new JLabel(msdi.getName()+(range == null ? "" : " " + range) + ":");
	String desc = msdi.getDescription();
	if(desc == null || desc.length() == 0){
	    desc = "Sorry, no description of this input is available from the service provider";
	}
	label.setToolTipText(HTMLUtils.htmlifyToolTipText(desc, MAX_TOOLTIP_WIDTH));
	return label;
    }

    public Component makeDateTimeWidget(MobyDataSecondaryInstance msdi){
	//TODO
	return null;
    }

    // Handle clicks on the Set and Cancel buttons.
    public void actionPerformed(ActionEvent e) {
	Object source = e.getSource();
	if(source == confirmButton){
	    // Take the values the user specified in the GUI and put them into 
	    // the MobyDataSecondaryInstances that were passed to us originally.
	    if(populateDataInstances()){
		// Do callback to the object that originally requested the 
		// MobyDataSecondaryInstances be filled out.  It is actually done in another
		// thread so the dialog can disappear immediately.
		doCallback();
		dispose();
	    }
	    else{
		// Show error of some sort
		JOptionPane.showConfirmDialog(this, "Could not set parameters as is, please ensure correctness", 
					     "Warning", JOptionPane.OK_OPTION, JOptionPane.WARNING_MESSAGE);
	    }
	}
	else if(source == cancelButton){
	    dispose();
	}
	// Primary input data update
	else if(source instanceof MobyDataObjectWidget){
	    MobyPrimaryData newData = ((MobyDataObjectWidget) source).getData();
	    String newDataName = ((MobyDataObjectWidget) source).getName();
	    boolean enableSubmit = true;

	    // Copy the new data value to the original array
	    for(int i = 0; i < inputsPrimary.length; i++){
		if(inputsPrimary[i].getName().equals(newDataName)){
		    newData.setName(newDataName);
		    inputsPrimary[i] = newData;
		}
		// Is there still some uninstantiated data?
		if(enableSubmit && !(inputsPrimary[i] instanceof MobyDataInstance)){
		    enableSubmit = false;
		}
	    }
	    confirmButton.setEnabled(enableSubmit);
	}
	else{
	    logger.warn("Unrecognized event source (" + source + ") in " + getClass());
	}
    }

    private boolean populateDataInstances(){

	// Go through the map we build while constructing the interface
	// and grab the current values for all the widgets as the new
	// secondary input data values.
	for(MobyDataSecondaryInstance msdi: data2widget.keySet()){
	    Object newValue = null;
	    try{
		JComponent widget = data2widget.get(msdi);
		if(widget instanceof JComboBox){ 
		    newValue = ((JComboBox) widget).getSelectedItem();
		    msdi.setValue(newValue.toString());
		}
		else if(widget instanceof JTextComponent){
		    newValue = ((JTextComponent) widget).getText();
		    msdi.setValue(newValue.toString());
		}
		else if(widget instanceof JCheckBox){
		    msdi.setValue(""+((JCheckBox) widget).isSelected());
		}
		else{
		    logger.error("IllegalArgumentException: GUI Component for " + msdi.getName() + 
				" was neither a JComboBox nor a " +
				"JTextComponent nor a JCheckBox as expected");
		    throw new IllegalArgumentException();
		}
	    }
	    catch(IllegalArgumentException iae){
		JOptionPane.showMessageDialog(null, "Could not set value of " + msdi.getName() + " to " +
				   newValue + ": " + iae, "Parameter Error", JOptionPane.ERROR_MESSAGE);
		return false;
	    }
	}
	// If we got this far, we set all the values correctly
	return true;
    }

    private void doCallback(){
	if(listener != null){
	    Thread thread = new Thread(this);
	    thread.start();
	}
    }

    public void run(){
	listener.actionPerformed(new ActionEvent(this, 0, ""+handlerCode));
    }
}
