package semorg.gui.util;

import java.sql.Time;
import java.sql.Timestamp;
import java.util.Date;
import java.util.Iterator;
import java.util.Vector;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.layout.FormLayout;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.ToolBar;
import org.eclipse.swt.widgets.ToolItem;

import semorg.sql.util.DBColumn;
import semorg.sql.util.DBConstraint;
import semorg.sql.util.Utility;

/**
 * Provides a search control widget which can apply several conditions to the
 * filter of a list window. This list of conditions can by expanded by the user
 * to apply more and more conditions to the list window.
 */
public class ExtensibleSearchControl extends Composite {

    /** Conjunctions constants whose values depend on the selected language. */
    private static final String[] conjunctions = {
	    Messages.getString("GUIText.ExtensibleSearchControlAndText"),
	    Messages.getString("GUIText.ExtensibleSearchControlOrText") };

    /** Comparator constants which can be applied to strings. */
    private static final String[] stringComparators = { "=", "<", ">", "!=",
	    "~" };

    /** Comparator constants which can be applied to integers. */
    private static final String[] intComparators = { "=", "<", ">", "!=" };

    /** Comparator constants which can be applied to floar numbers. */
    private static final String[] floatComparators = { "=", "<", ">", "!=" };

    /** Comparator constants which can be applied to dates. */
    private static final String[] dateComparators = { "=", "<", ">", "!=" };

    /** Comparator constants which can be applied to timestamps. */
    private static final String[] timestampComparators = dateComparators;

    /** Comparator constants which can be applied to times. */
    private static final String[] timeComparators = dateComparators;

    /** Comparator constants which can be applied to boolean values. */
    private static final String[] booleanComparators = { "=" };

    /** Columns on which the conditions get applied. */
    private Vector<DBColumn> columns = null;

    /** The number of the applied conditions. */
    private int filterExtensionCounter;

    /** The filter extensions of the filter control. */
    private final Composite[] filterExtensions;

    /** The buttons which extends the filter.. */
    private final ToolItem[] extenders;

    /** The conjunction combos. */
    private final Combo[] conjunctionCombos;

    /**
     * The combo fields containing the names of the columns which are used for
     * the filtering.
     */
    private final Combo[] columnCombos;

    /** The combo fields containing the applicable comparators. */
    private final Combo[] comparatorCombos;

    /** The text fields of the filter control. */
    private final Text[] filterTexts;

    /** The window containing the filter control. */
    private Composite window;

    /**
     * Instantiates a new extensible search control.
     * 
     * @param parent
     *                the parent composite
     * @param numberExtensions
     *                the number of extensions
     */
    public ExtensibleSearchControl(Composite parent, int numberExtensions) {
	super(parent, SWT.NONE);

	window = new Composite(this, SWT.NONE);
	window.setLayout(new FormLayout());
	window.setLayoutData(Utility.getFormData(0, 0, 0,
		((filterExtensionCounter + 1) * 26) - 1, 0, 1, 100, -1));

	filterExtensionCounter = 0;
	filterExtensions = new Composite[numberExtensions];
	extenders = new ToolItem[numberExtensions];

	conjunctionCombos = new Combo[numberExtensions];
	columnCombos = new Combo[numberExtensions + 1];
	comparatorCombos = new Combo[numberExtensions + 1];
	filterTexts = new Text[numberExtensions + 1];

	setLayout(new FormLayout());

	final Label searchLabel = new Label(window, SWT.READ_ONLY);
	searchLabel.setLayoutData(Utility.getFormData(0, 6, null, 0, 0, 2,
		null, 0));
	searchLabel.setText(Messages
		.getString("GUIText.ExtensibleSearchControlText"));

	columnCombos[0] = new Combo(window, SWT.DROP_DOWN | SWT.READ_ONLY);
	columnCombos[0].setLayoutData(Utility.getFormData(0, 3, null, 0,
		searchLabel, 20, searchLabel, 179));

	comparatorCombos[0] = new Combo(window, SWT.DROP_DOWN | SWT.READ_ONLY);
	comparatorCombos[0].setLayoutData(Utility.getFormData(0, 3, null, 0,
		columnCombos[0], 3, columnCombos[0], 167));

	filterTexts[0] = new Text(window, SWT.SINGLE | SWT.BORDER);
	filterTexts[0].setLayoutData(Utility.getFormData(0, 3, 0, 24,
		comparatorCombos[0], 3, comparatorCombos[0], 167));

	final ToolBar toolBar = new ToolBar(window, SWT.FLAT);
	toolBar.setLayoutData(Utility.getFormData(0, 3, null, 0,
		filterTexts[0], 1, null, 0));

	extenders[0] = new ToolItem(toolBar, SWT.PUSH);
	extenders[0].setImage(Utility.getImage("down.png"));
	extenders[0].addSelectionListener(new SelectionListener() {
	    public void widgetSelected(SelectionEvent event) {
		if (filterExtensionCounter == 0) {
		    // open
		    extenders[0].setImage(Utility.getImage("up.png"));
		    extenders[1].setImage(Utility.getImage("down.png"));
		    ++filterExtensionCounter;
		} else {
		    // collapse
		    extenders[0].setImage(Utility.getImage("down.png"));
		    filterExtensionCounter = 0;
		}
		window
			.setLayoutData(Utility.getFormData(0, 0, 0,
				((filterExtensionCounter + 1) * 26) - 1, 0, 1,
				100, -1));
		pack();
	    }

	    public void widgetDefaultSelected(SelectionEvent event) {
		widgetSelected(event);
	    }
	});

	for (int i = 0; i < numberExtensions; i++)
	    filterExtensions[i] = createFilterExtension(i);
	pack();
    }

    /**
     * Creates a filter extension, i.e. it creates widgets in the array
     * {@link #conjunctionCombos}, {@link #columnCombos},
     * {@link #comparatorCombos} and {@link #filterTexts} at the specified
     * position.
     * 
     * @param number
     *                the index of the extension to create.
     * 
     * @return the composite
     */
    private Composite createFilterExtension(final int number) {
	Composite filterExtension = new Composite(window, SWT.NONE);
	filterExtension.setLayout(new FormLayout());

	conjunctionCombos[number] = new Combo(filterExtension, SWT.DROP_DOWN
		| SWT.READ_ONLY);
	conjunctionCombos[number].setLayoutData(Utility.getFormData(0,
		((number + 1) * 26) + 3, null, 0, 0, 2, 0, 46));

	columnCombos[number + 1] = new Combo(filterExtension, SWT.DROP_DOWN
		| SWT.READ_ONLY);
	columnCombos[number + 1].setLayoutData(Utility.getFormData(0,
		((number + 1) * 26) + 3, null, 0, conjunctionCombos[number], 7,
		conjunctionCombos[number], 179));

	comparatorCombos[number + 1] = new Combo(filterExtension, SWT.DROP_DOWN
		| SWT.READ_ONLY);
	comparatorCombos[number + 1].setLayoutData(Utility.getFormData(0,
		((number + 1) * 26) + 3, null, 0, columnCombos[number + 1], 3,
		columnCombos[number + 1], 167));

	filterTexts[number + 1] = new Text(filterExtension, SWT.SINGLE
		| SWT.BORDER);
	filterTexts[number + 1].setLayoutData(Utility.getFormData(0,
		((number + 1) * 26) + 3, 0, ((number + 1) * 26) + 24,
		comparatorCombos[number + 1], 3, comparatorCombos[number + 1],
		167));

	final ToolBar viewToolBar = new ToolBar(filterExtension, SWT.FLAT);
	viewToolBar.setLayoutData(Utility.getFormData(0,
		((number + 1) * 26) + 3, null, 0, filterTexts[number + 1], 1,
		null, 0));

	if (number < (filterExtensions.length - 1)) {
	    extenders[number + 1] = new ToolItem(viewToolBar, SWT.PUSH);
	    extenders[number + 1].setImage(Utility.getImage("down.png"));

	    extenders[number + 1].addSelectionListener(new SelectionListener() {

		public void widgetSelected(SelectionEvent event) {
		    if (filterExtensionCounter <= number + 1) {
			// open
			extenders[number + 1].setImage(Utility
				.getImage("up.png"));
			if (number != extenders.length - 2)
			    extenders[number + 2].setImage(Utility
				    .getImage("down.png"));
			++filterExtensionCounter;
		    } else {
			// collapse
			extenders[number + 1].setImage(Utility
				.getImage("down.png"));
			filterExtensionCounter = number + 1;
		    }
		    window.setLayoutData(Utility.getFormData(0, 0, 0,
			    ((filterExtensionCounter + 1) * 26) - 1, 0, 1, 100,
			    -1));
		    pack();
		}

		public void widgetDefaultSelected(SelectionEvent event) {
		    widgetSelected(event);
		}
	    });
	}

	return filterExtension;
    }

    /**
     * Adds the extension listener.
     * 
     * @param arg0
     *                the {@link SelectionListener} to add.
     */
    public void addExtensionListener(SelectionListener arg0) {
	for (int i = 0; i < filterExtensions.length; i++) {
	    extenders[i].addSelectionListener(arg0);
	}
    }

    /**
     * Adds the modify listener.
     * 
     * @param arg0
     *                the {@link ModifyListener} to add.
     */
    public void addModifyListener(ModifyListener arg0) {
	for (int i = 0; i < filterTexts.length; i++) {
	    filterTexts[i].addModifyListener(arg0);
	}
    }

    /**
     * Returns the constraints which are entered into the filter control by the
     * user. Note that the conditions entered into the control get converted
     * into a Vector of {@link DBConstraint} instances.
     * 
     * @return a Vector of {@link DBConstraint} instances according to the
     *         conditions of the filter control.
     * 
     * @throws Exception
     *                 if the parsing of dates, times, timestamps, integers or
     *                 float numbers fails for some reason.
     */
    public Vector<DBConstraint> getConstraints() throws Exception {
	Vector<DBConstraint> constraints = new Vector<DBConstraint>();

	for (int i = 0; i < filterExtensionCounter + 1; i++) {

	    DBColumn belongingColumn = getDBColumn(columnCombos[i].getText());

	    String colName = belongingColumn.getInternalColumnName();
	    int rel = DBConstraint.getRelation(comparatorCombos[i].getText());

	    int conj;

	    if (i == filterExtensionCounter)
		conj = DBConstraint.CONJ_END;
	    else
		conj = DBConstraint.getConjunction(conjunctionCombos[i]
			.getText());

	    Class valClass = belongingColumn.getColumnType();
	    String valueString = filterTexts[i].getText();
	    Object value;
	    if (valClass == Integer.class)
		value = new Integer(Integer.parseInt(valueString));
	    else if (valClass == Timestamp.class)
		value = new Timestamp(Utility.dateOnlyFormatter.parse(
			valueString).getTime());
	    else if (valClass == Time.class)
		value = new Time(Utility.timeOnlyFormatter.parse(valueString)
			.getTime());
	    else if (valClass == Date.class)
		// TODO: not parsed end should throw an exception
		value = Utility.dateOnlyFormatter.parse(valueString);
	    else if (valClass == Float.class)
		value = Utility.parseFloat(valueString);
	    else if (valClass == Boolean.class) {
		if (valueString
			.equalsIgnoreCase(Messages
				.getString("ProviderText.PresentationTableProviderIsCanceledText")))
		    // for canceled presentations
		    value = new Boolean(true);
		else
		    value = new Boolean(valueString);
	    }

	    else
		value = valueString;
	    constraints.add(new DBConstraint(colName, rel, value, conj));

	}

	return constraints;
    }

    /**
     * Inserts the given columns into the filter controls.
     * 
     * @param columns
     *                vector of {@link DBColumn} instances which should be
     *                inserted into the filter
     */
    public void setColumns(Vector<DBColumn> columns) {
	this.columns = columns;
	//get the column names
	String[] columnStrings = new String[columns.size()];
	for (int i = 0; i < columnStrings.length; i++) {
	    columnStrings[i] = columns.get(i).getPublicColumnName();
	}

	for (int i = 0; i < columnCombos.length; i++) {
	    final int currentIndex = i;

	    if (i < conjunctionCombos.length) {
		conjunctionCombos[i].setItems(conjunctions);
		conjunctionCombos[i].setText(conjunctions[0]);
	    }

	    //set the column names
	    columnCombos[i].setItems(columnStrings);
	    //first column name is shown in the combo field
	    columnCombos[i].setText(columnCombos[i].getItem(0));
	    columnCombos[i].addModifyListener(new ModifyListener() {

		public void modifyText(ModifyEvent arg0) {
		    comparatorCombos[currentIndex]
			    .setItems(getComparatorString(columnCombos[currentIndex]
				    .getText()));
		    comparatorCombos[currentIndex]
			    .setText(comparatorCombos[currentIndex].getItem(0));
		}
	    });

	    comparatorCombos[i].setItems(getComparatorString(columnCombos[i]
		    .getText()));
	    comparatorCombos[i].setText(comparatorCombos[i].getItem(0));
	}
    }

    /**
     * Firstly this function resolves the {@link DBColumn} instance for the
     * given public name and then returns the correct class of comparators
     * depending on the type of the {@link DBColumn}.
     * 
     * @param publicColumnName
     *                the public name of the column
     * 
     * @return string array containing all comparators applicable the
     *         {@link DBColumn} instance.
     */
    private String[] getComparatorString(String publicColumnName) {

	DBColumn selectedColumn = getDBColumn(publicColumnName);

	if (selectedColumn.getColumnType() == Integer.class)
	    return intComparators;
	else if (selectedColumn.getColumnType() == String.class)
	    return stringComparators;
	else if (selectedColumn.getColumnType() == Time.class)
	    return timeComparators;
	else if (selectedColumn.getColumnType() == Timestamp.class)
	    return timestampComparators;
	else if (selectedColumn.getColumnType() == Date.class)
	    return dateComparators;
	else if (selectedColumn.getColumnType() == Float.class)
	    return floatComparators;
	else if (selectedColumn.getColumnType() == Boolean.class)
	    return booleanComparators;

	// should never happen
	else
	    return null;
    }

    /**
     * Returns a {@link DBColumn} for a given public name.
     * 
     * @param publicColumnName
     *                the public name of the column.
     * 
     * @return {@link DBColumn} instance corresponding to the given public name.
     */
    private DBColumn getDBColumn(String publicColumnName) {

	Iterator<DBColumn> it = columns.iterator();
	DBColumn selectedColumn;
	do {
	    selectedColumn = it.next();
	} while (!(selectedColumn.getPublicColumnName()
		.equals(publicColumnName)));
	return selectedColumn;
    }

    /**
     * Sets a pair of key and value to this control needed for testing with
     * ATOSj.
     * 
     * @param key
     *                the key (type) of the value set
     * @param value
     *                the value set to the control
     */
    public void setData(String key, String value) {
	super.setData(key, value);
	if (key.equals(Utility.ATOSJ_COMPONENT_NAME_KEY)) {
	    columnCombos[0].setData(key, value + "_ColumnCombo");
	    comparatorCombos[0].setData(key, value + "_ComparatorCombo");
	    filterTexts[0].setData(key, value + "_FilterText");
	    for (int i = 0; i < extenders.length; i++) {
		conjunctionCombos[i].setData(key, value + "_ConjunctionCombo"
			+ (i + 1));
		columnCombos[i + 1].setData(key, value + "_ColumnCombo"
			+ (i + 1));
		comparatorCombos[i + 1].setData(key, value + "_ComparatorCombo"
			+ (i + 1));
		filterTexts[i + 1]
			.setData(key, value + "_FilterText" + (i + 1));
		extenders[i].setData(key, value + "_Extender" + (i + 1));
	    }
	}
    }

}
