/*******************************************************************************
 * Copyright (c) 2000, 2006 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *   IBM Corporation - initial API and implementation 
 *   Sebastian Davids <sdavids@gmx.de> - Fix for bug 19346 - Dialog font should be activated and used by other components.
 *******************************************************************************/
package org.eclipse.ui.dialogs;

import java.util.Comparator;
import java.util.HashSet;
import java.util.Set;
import java.util.Vector;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.viewers.ILabelProvider;
import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.accessibility.Accessible;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableItem;
import org.eclipse.ui.internal.WorkbenchMessages;
import org.eclipse.ui.internal.misc.StringMatcher;
import org.eclipse.ui.internal.util.Util;
import org.eclipse.ui.progress.WorkbenchJob;

/**
 * A composite widget which holds a list of elements for user selection. The
 * elements are sorted alphabetically. Optionally, the elements can be filtered
 * and duplicate entries can be hidden (folding).
 * 
 * @since 2.0
 */
public class FilteredList extends Composite {
	/**
	 * The FilterMatcher is the interface used to check filtering criterea.
	 */
	public interface FilterMatcher {
		/**
		 * Sets the filter.
		 * 
		 * @param pattern
		 *            the filter pattern.
		 * @param ignoreCase
		 *            a flag indicating whether pattern matching is case
		 *            insensitive or not.
		 * @param ignoreWildCards
		 *            a flag indicating whether wildcard characters are
		 *            interpreted or not.
		 */
		void setFilter(String pattern, boolean ignoreCase,
				boolean ignoreWildCards);

		/**
		 * @param element
		 *            The element to test against.
		 * @return <code>true</code> if the object matches the pattern,
		 *         <code>false</code> otherwise. <code>setFilter()</code>
		 *         must have been called at least once prior to a call to this
		 *         method.
		 */
		boolean match(Object element);
	}

	private class DefaultFilterMatcher implements FilterMatcher {
		private StringMatcher fMatcher;

		public void setFilter(String pattern, boolean ignoreCase,
				boolean ignoreWildCards) {
			fMatcher = new StringMatcher(pattern + '*', ignoreCase,
					ignoreWildCards);
		}

		public boolean match(Object element) {
			return fMatcher.match(fLabelProvider.getText(element));
		}
	}

	private Table fList;

	ILabelProvider fLabelProvider;

	private boolean fMatchEmptyString = true;

	private boolean fIgnoreCase;

	private boolean fAllowDuplicates;

	private String fFilter = ""; //$NON-NLS-1$

	private TwoArrayQuickSorter fSorter;

	Object[] fElements = new Object[0];

	Label[] fLabels;

	Vector fImages = new Vector();

	int[] fFoldedIndices;

	int fFoldedCount;

	int[] fFilteredIndices;

	int fFilteredCount;

	private FilterMatcher fFilterMatcher = new DefaultFilterMatcher();

	Comparator fComparator;

	TableUpdateJob fUpdateJob;

	/**
	 * Label is a private class used for comparing list objects
	 */
	private static class Label {
		/**
		 * The string portion of the label.
		 */
		public final String string;

		/**
		 * The image portion of the label.
		 */
		public final Image image;

		/**
		 * Create a new instance of label.
		 * 
		 * @param newString
		 * @param image
		 */
		public Label(String newString, Image image) {
			if (newString == null) {
				this.string = Util.ZERO_LENGTH_STRING;
			} else {
				this.string = newString;
			}
			this.image = image;
		}

		/**
		 * Return whether or not the receiver is the same as label.
		 * 
		 * @param label
		 * @return boolean
		 */
		public boolean equals(Label label) {
			if (label == null) {
				return false;
			}
			// If the string portions match (whether null or not), fall
			// through and check the image portion.
			if (string == null && label.string != null) {
				return false;
			}
			if ((string != null) && (!string.equals(label.string))) {
				return false;
			}
			if (image == null) {
				return label.image == null;
			}
			return image.equals(label.image);
		}
	}

	private final class LabelComparator implements Comparator {
		private boolean labelIgnoreCase;

		LabelComparator(boolean ignoreCase) {
			labelIgnoreCase = ignoreCase;
		}

		public int compare(Object left, Object right) {
			Label leftLabel = (Label) left;
			Label rightLabel = (Label) right;
			int value;
			if (fComparator == null) {
				value = labelIgnoreCase ? leftLabel.string
						.compareToIgnoreCase(rightLabel.string)
						: leftLabel.string.compareTo(rightLabel.string);
			} else {
				value = fComparator
						.compare(leftLabel.string, rightLabel.string);
			}
			if (value != 0) {
				return value;
			}
			// images are allowed to be null
			if (leftLabel.image == null) {
				return (rightLabel.image == null) ? 0 : -1;
			} else if (rightLabel.image == null) {
				return +1;
			} else {
				return fImages.indexOf(leftLabel.image)
						- fImages.indexOf(rightLabel.image);
			}
		}
	}

	/**
	 * Constructs a new filtered list.
	 * 
	 * @param parent
	 *            the parent composite
	 * @param style
	 *            the widget style
	 * @param labelProvider
	 *            the label renderer
	 * @param ignoreCase
	 *            specifies whether sorting and folding is case sensitive
	 * @param allowDuplicates
	 *            specifies whether folding of duplicates is desired
	 * @param matchEmptyString
	 *            specifies whether empty filter strings should filter
	 *            everything or nothing
	 */
	public FilteredList(Composite parent, int style,
			ILabelProvider labelProvider, boolean ignoreCase,
			boolean allowDuplicates, boolean matchEmptyString) {
		super(parent, SWT.NONE);
		GridLayout layout = new GridLayout();
		layout.marginHeight = 0;
		layout.marginWidth = 0;
		setLayout(layout);
		fList = new Table(this, style);
		fList.setLayoutData(new GridData(GridData.FILL_BOTH));
		fList.setFont(parent.getFont());
		fList.addDisposeListener(new DisposeListener() {
			public void widgetDisposed(DisposeEvent e) {
				fLabelProvider.dispose();
				if (fUpdateJob != null) {
					fUpdateJob.cancel();
				}
			}
		});
		fLabelProvider = labelProvider;
		fIgnoreCase = ignoreCase;
		fSorter = new TwoArrayQuickSorter(new LabelComparator(ignoreCase));
		fAllowDuplicates = allowDuplicates;
		fMatchEmptyString = matchEmptyString;
	}

	/**
	 * Sets the list of elements.
	 * 
	 * @param elements
	 *            the elements to be shown in the list.
	 */
	public void setElements(Object[] elements) {
		if (elements == null) {
			fElements = new Object[0];
		} else {
			// copy list for sorting
			fElements = new Object[elements.length];
			System.arraycopy(elements, 0, fElements, 0, elements.length);
		}
		int length = fElements.length;
		// fill labels
		fLabels = new Label[length];
		Set imageSet = new HashSet();
		for (int i = 0; i != length; i++) {
			String text = fLabelProvider.getText(fElements[i]);
			Image image = fLabelProvider.getImage(fElements[i]);
			fLabels[i] = new Label(text, image);
			imageSet.add(image);
		}
		fImages.clear();
		fImages.addAll(imageSet);
		fSorter.sort(fLabels, fElements);
		fFilteredIndices = new int[length];
		fFoldedIndices = new int[length];
		updateList();
	}

	/**
	 * Tests if the list (before folding and filtering) is empty.
	 * 
	 * @return returns <code>true</code> if the list is empty,
	 *         <code>false</code> otherwise.
	 */
	public boolean isEmpty() {
		return (fElements == null) || (fElements.length == 0);
	}

	/**
	 * Sets the filter matcher.
	 * 
	 * @param filterMatcher
	 */
	public void setFilterMatcher(FilterMatcher filterMatcher) {
		Assert.isNotNull(filterMatcher);
		fFilterMatcher = filterMatcher;
	}

	/**
	 * Sets a custom comparator for sorting the list.
	 * 
	 * @param comparator
	 */
	public void setComparator(Comparator comparator) {
		Assert.isNotNull(comparator);
		fComparator = comparator;
	}

	/**
	 * Adds a selection listener to the list.
	 * 
	 * @param listener
	 *            the selection listener to be added.
	 */
	public void addSelectionListener(SelectionListener listener) {
		fList.addSelectionListener(listener);
	}

	/**
	 * Removes a selection listener from the list.
	 * 
	 * @param listener
	 *            the selection listener to be removed.
	 */
	public void removeSelectionListener(SelectionListener listener) {
		fList.removeSelectionListener(listener);
	}

	/**
	 * Sets the selection of the list. Empty or null array removes selection.
	 * 
	 * @param selection
	 *            an array of indices specifying the selection.
	 */
	public void setSelection(int[] selection) {
		if (selection == null || selection.length == 0) {
			fList.deselectAll();
		} else {
			// If there is no working update job, or the update job is ready to
			// accept selections, set the selection immediately.
			if (fUpdateJob == null) {
				fList.setSelection(selection);
				fList.notifyListeners(SWT.Selection, new Event());
			} else {
				// There is an update job doing the population of the list, so
				// it should update the selection.
				fUpdateJob.updateSelection(selection);
			}
		}
	}

	/**
	 * Returns the selection of the list.
	 * 
	 * @return returns an array of indices specifying the current selection.
	 */
	public int[] getSelectionIndices() {
		return fList.getSelectionIndices();
	}

	/**
	 * Returns the selection of the list. This is a convenience function for
	 * <code>getSelectionIndices()</code>.
	 * 
	 * @return returns the index of the selection, -1 for no selection.
	 */
	public int getSelectionIndex() {
		return fList.getSelectionIndex();
	}

	/**
	 * Sets the selection of the list. Empty or null array removes selection.
	 * 
	 * @param elements
	 *            the array of elements to be selected.
	 */
	public void setSelection(Object[] elements) {
		if (elements == null || elements.length == 0) {
			fList.deselectAll();
			return;
		}
		if (fElements == null) {
			return;
		}
		// fill indices
		int[] indices = new int[elements.length];
		for (int i = 0; i != elements.length; i++) {
			int j;
			for (j = 0; j != fFoldedCount; j++) {
				int max = (j == fFoldedCount - 1) ? fFilteredCount
						: fFoldedIndices[j + 1];
				int l;
				for (l = fFoldedIndices[j]; l != max; l++) {
					// found matching element?
					if (fElements[fFilteredIndices[l]].equals(elements[i])) {
						indices[i] = j;
						break;
					}
				}
				if (l != max) {
					break;
				}
			}
			// not found
			if (j == fFoldedCount) {
				indices[i] = 0;
			}
		}
		setSelection(indices);
	}

	/**
	 * Returns an array of the selected elements. The type of the elements
	 * returned in the list are the same as the ones passed with
	 * <code>setElements</code>. The array does not contain the rendered
	 * strings.
	 * 
	 * @return returns the array of selected elements.
	 */
	public Object[] getSelection() {
		if (fList.isDisposed() || (fList.getSelectionCount() == 0)) {
			return new Object[0];
		}
		int[] indices = fList.getSelectionIndices();
		Object[] elements = new Object[indices.length];
		for (int i = 0; i != indices.length; i++) {
			elements[i] = fElements[fFilteredIndices[fFoldedIndices[indices[i]]]];
		}
		return elements;
	}

	/**
	 * Sets the filter pattern. Current only prefix filter patterns are
	 * supported.
	 * 
	 * @param filter
	 *            the filter pattern.
	 */
	public void setFilter(String filter) {
		fFilter = (filter == null) ? "" : filter; //$NON-NLS-1$
		updateList();
	}

	private void updateList() {
		fFilteredCount = filter();
		fFoldedCount = fold();
		if (fUpdateJob != null) {
			fUpdateJob.cancel();
		}
		fUpdateJob = new TableUpdateJob(fList, fFoldedCount);
		fUpdateJob.schedule();
	}

	/**
	 * Returns the filter pattern.
	 * 
	 * @return returns the filter pattern.
	 */
	public String getFilter() {
		return fFilter;
	}

	/**
	 * Returns all elements which are folded together to one entry in the list.
	 * 
	 * @param index
	 *            the index selecting the entry in the list.
	 * @return returns an array of elements folded together, <code>null</code>
	 *         if index is out of range.
	 */
	public Object[] getFoldedElements(int index) {
		if ((index < 0) || (index >= fFoldedCount)) {
			return null;
		}
		int start = fFoldedIndices[index];
		int count = (index == fFoldedCount - 1) ? fFilteredCount - start
				: fFoldedIndices[index + 1] - start;
		Object[] elements = new Object[count];
		for (int i = 0; i != count; i++) {
			elements[i] = fElements[fFilteredIndices[start + i]];
		}
		return elements;
	}

	/*
	 * Folds duplicate entries. Two elements are considered as a pair of
	 * duplicates if they coiincide in the rendered string and image. @return
	 * returns the number of elements after folding.
	 */
	private int fold() {
		if (fAllowDuplicates) {
			for (int i = 0; i != fFilteredCount; i++) {
				fFoldedIndices[i] = i; // identity mapping
			}
			return fFilteredCount;
		}
		int k = 0;
		Label last = null;
		for (int i = 0; i != fFilteredCount; i++) {
			int j = fFilteredIndices[i];
			Label current = fLabels[j];
			if (!current.equals(last)) {
				fFoldedIndices[k] = i;
				k++;
				last = current;
			}
		}
		return k;
	}

	/*
	 * Filters the list with the filter pattern. @return returns the number of
	 * elements after filtering.
	 */
	private int filter() {
		if (((fFilter == null) || (fFilter.length() == 0))
				&& !fMatchEmptyString) {
			return 0;
		}
		fFilterMatcher.setFilter(fFilter.trim(), fIgnoreCase, false);
		int k = 0;
		for (int i = 0; i != fElements.length; i++) {
			if (fFilterMatcher.match(fElements[i])) {
				fFilteredIndices[k++] = i;
			}
		}
		return k;
	}

	private class TableUpdateJob extends WorkbenchJob {
		final Table fTable;

		final int fCount;

		private int currentIndex = 0;

		/*
		 * Programmatic selections requested while this job was running.
		 */
		int[] indicesToSelect;
		
		private boolean readyForSelection = false;

		/**
		 * Create a new instance of a job used to update the table.
		 * 
		 * @param table
		 * @param count
		 *            The number of items to update per running.
		 */
		public TableUpdateJob(Table table, int count) {
			super(WorkbenchMessages.FilteredList_UpdateJobName);
			setSystem(true);
			fTable = table;
			fCount = count;
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see org.eclipse.ui.progress.UIJob#runInUIThread(org.eclipse.core.runtime.IProgressMonitor)
		 */
public IStatus runInUIThread(IProgressMonitor monitor) {
            if (fTable.isDisposed()) {
				return Status.CANCEL_STATUS;
			}
            int itemCount = fTable.getItemCount();
                        
            // Remove excess items
            if (fCount < itemCount) {
                fTable.setRedraw(false);
                fTable.remove(fCount, itemCount - 1);
                fTable.setRedraw(true);
                itemCount = fTable.getItemCount();
            }
            // table empty -> no selection
            if (fCount == 0) {
                fTable.notifyListeners(SWT.Selection, new Event());
                return Status.OK_STATUS;
            }
            // How many we are going to do this time.
            int iterations = Math.min(10, fCount - currentIndex);
            for (int i = 0; i < iterations; i++) {
                if (monitor.isCanceled()) {
					return Status.CANCEL_STATUS;
				}
                final TableItem item = (currentIndex < itemCount) ? fTable
                        .getItem(currentIndex)
                        : new TableItem(fTable, SWT.NONE);
                final Label label = fLabels[fFilteredIndices[fFoldedIndices[currentIndex]]];
                item.setText(label.string);
                item.setImage(label.image);
                currentIndex++;
            }
            if (monitor.isCanceled()) {
				return Status.CANCEL_STATUS;
			}
            if (currentIndex < fCount) {
				schedule(100);
			} else {
                if (indicesToSelect == null) {
                 	// Make a default selection in the table if there is none.
                	// If a selection has already been made, honor it.
                	// See https://bugs.eclipse.org/bugs/show_bug.cgi?id=112146
                    if (fCount > 0) {
                    	if (fTable.getSelectionIndices().length == 0) {
                    		defaultSelect();
                    	} else {
                    		// There is a selection, but it likely hasn't changed since the
                    		// job started.  Force a selection notification, since the
                    		// items represented by the selection have changed.
							// See https://bugs.eclipse.org/bugs/show_bug.cgi?id=119456
                    		fTable.notifyListeners(SWT.Selection, new Event());
                    	}
                    }
                } else {
                	// Set the selection as indicated.
                    selectAndNotify(indicesToSelect);
                }
                // This flag signifies that the selection can now be directly
                // updated in the widget.
                readyForSelection = true;
            }
            return Status.OK_STATUS;
        }
		/**
		 * Update the selection for the supplied indices.
		 * 
		 * @param indices
		 */
		void updateSelection(final int[] indices) {
			indicesToSelect = indices;
			if (readyForSelection) {
				selectAndNotify(indices);
			}
		}

		/**
		 * Select the first element if there is no selection
		 */
		private void defaultSelect() {
			/**
			 * Reset to the first selection if no index has been queued.
			 */
			selectAndNotify(new int[] { 0 });
		}

		/**
		 * Select the supplied indices and notify any listeners
		 * 
		 * @param indices
		 */
		private void selectAndNotify(final int[] indices) {
			// It is possible that the table was disposed
			// before the update finished. If so then leave
			if (fTable.isDisposed()) {
				return;
			}
			fTable.setSelection(indices);
			fTable.notifyListeners(SWT.Selection, new Event());
		}
	}

	/**
	 * Returns whether or not duplicates are allowed.
	 * 
	 * @return <code>true</code> indicates duplicates are allowed
	 */
	public boolean getAllowDuplicates() {
		return fAllowDuplicates;
	}

	/**
	 * Sets whether or not duplicates are allowed. If this value is set the
	 * items should be set again for this value to take effect.
	 * 
	 * @param allowDuplicates
	 *            <code>true</code> indicates duplicates are allowed
	 */
	public void setAllowDuplicates(boolean allowDuplicates) {
		this.fAllowDuplicates = allowDuplicates;
	}

	/**
	 * Returns whether or not case should be ignored.
	 * 
	 * @return <code>true</code> if case should be ignored
	 */
	public boolean getIgnoreCase() {
		return fIgnoreCase;
	}

	/**
	 * Sets whether or not case should be ignored If this value is set the items
	 * should be set again for this value to take effect.
	 * 
	 * @param ignoreCase
	 *            <code>true</code> if case should be ignored
	 */
	public void setIgnoreCase(boolean ignoreCase) {
		this.fIgnoreCase = ignoreCase;
	}

	/**
	 * Returns whether empty filter strings should filter everything or nothing.
	 * 
	 * @return <code>true</code> for the empty string to match all items,
	 *         <code>false</code> to match none
	 */
	public boolean getMatchEmptyString() {
		return fMatchEmptyString;
	}

	/**
	 * Sets whether empty filter strings should filter everything or nothing. If
	 * this value is set the items should be set again for this value to take
	 * effect.
	 * 
	 * @param matchEmptyString
	 *            <code>true</code> for the empty string to match all items,
	 *            <code>false</code> to match none
	 */
	public void setMatchEmptyString(boolean matchEmptyString) {
		this.fMatchEmptyString = matchEmptyString;
	}

	/**
	 * Returns the label provider for the items.
	 * 
	 * @return the label provider
	 */
	public ILabelProvider getLabelProvider() {
		return fLabelProvider;
	}

	/**
	 * Sets the label provider. If this value is set the items should be set
	 * again for this value to take effect.
	 * 
	 * @param labelProvider
	 *            the label provider
	 */
	public void setLabelProvider(ILabelProvider labelProvider) {
		this.fLabelProvider = labelProvider;
	}
	
	/**
	 * Returns the accessible object for the receiver.
	 * If this is the first time this object is requested,
	 * then the object is created and returned.
	 *
	 * @return the accessible object
	 *
	 * @exception SWTException <ul>
	 *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
	 *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
	 * </ul>
	 * 
	 * @see Accessible#addAccessibleListener
	 * @see Accessible#addAccessibleControlListener
	 * 
	 * @since 3.3
	 */
	public Accessible getAccessible() {
		return fList.getAccessible();
	}
}
