/*******************************************************************************
 * Copyright (c) 2006, 2016 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *     Lars Vogel <Lars.Vogel@gmail.com> - Bug 430873
 *     Andrey Loskutov <loskutov@gmx.de> - Bug 364735
 ******************************************************************************/

package org.eclipse.jface.viewers;

import java.util.Arrays;
import java.util.Comparator;

import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.util.Policy;

/**
 * A viewer comparator is used by a {@link StructuredViewer} to
 * reorder the elements provided by its content provider.
 * <p>
 * The default <code>compare</code> method compares elements using two steps.
 * The first step uses the values returned from <code>category</code>.
 * By default, all elements are in the same category.
 * The second level uses strings obtained from the content viewer's label
 * provider via <code>ILabelProvider.getText()</code>.
 * The strings are compared using a comparator from {@link Policy#getComparator()}
 * which by default does a case sensitive string comparison.
 * </p>
 * <p>
 * Subclasses may implement the <code>isSorterProperty</code> method;
 * they may reimplement the <code>category</code> method to provide
 * categorization; and they may override the <code>compare</code> methods
 * to provide a totally different way of sorting elements.
 * </p>
 * @see IStructuredContentProvider
 * @see StructuredViewer
 *
 * @since 3.2
 */
public class ViewerComparator {

	/**
	 * The comparator to use to sort a viewer's contents.
	 */
	private Comparator<? super String> comparator;

	/**
	 * Creates a new {@link ViewerComparator}, which uses the default comparator
	 * to sort strings.
	 *
	 */
	public ViewerComparator(){
		this(null);
	}

	/**
	 * Creates a new {@link ViewerComparator}, which uses the given comparator
	 * to sort strings. The default implementation of
	 * {@link ViewerComparator#compare(Viewer, Object, Object)} expects this
	 * comparator to be able to compare the {@link String}s provided by the
	 * viewer's label provider.
	 *
	 * @param comparator the comparator to use
	 */
	public ViewerComparator(Comparator<? super String> comparator) {
		this.comparator = comparator;
	}

	/**
	 * Returns the comparator used to sort strings.
	 *
	 * @return the comparator used to sort strings
	 */
	protected Comparator<? super String> getComparator() {
		if (comparator == null){
			comparator = Policy.getComparator();
		}
		return comparator;
	}

	/**
	 * Returns the category of the given element. The category is a
	 * number used to allocate elements to bins; the bins are arranged
	 * in ascending numeric order. The elements within a bin are arranged
	 * via a second level sort criterion.
	 * <p>
	 * The default implementation of this framework method returns
	 * <code>0</code>. Subclasses may reimplement this method to provide
	 * non-trivial categorization.
	 * </p>
	 *
	 * @param element the element
	 * @return the category
	 */
	public int category(Object element) {
		return 0;
	}

	/**
	 * Returns a negative, zero, or positive number depending on whether
	 * the first element is less than, equal to, or greater than
	 * the second element.
	 * <p>
	 * The default implementation of this method is based on
	 * comparing the elements' categories as computed by the <code>category</code>
	 * framework method. Elements within the same category are further
	 * subjected to a case insensitive compare of their label strings, either
	 * as computed by the content viewer's label provider, or their
	 * <code>toString</code> values in other cases. Subclasses may override.
	 * </p>
	 *
	 * @param viewer the viewer
	 * @param e1 the first element
	 * @param e2 the second element
	 * @return a negative number if the first element is less  than the
	 *  second element; the value <code>0</code> if the first element is
	 *  equal to the second element; and a positive number if the first
	 *  element is greater than the second element
	 */
	public int compare(Viewer viewer, Object e1, Object e2) {
		int cat1 = category(e1);
		int cat2 = category(e2);

		if (cat1 != cat2) {
			return cat1 - cat2;
		}

		String name1 = getLabel(viewer, e1);
		String name2 = getLabel(viewer, e2);

		// use the comparator to compare the strings
		return getComparator().compare(name1, name2);
	}

	private String getLabel(Viewer viewer, Object e1) {
		String name1;
		if (viewer == null || !(viewer instanceof ContentViewer)) {
			name1 = e1.toString();
		} else {
			IBaseLabelProvider prov = ((ContentViewer) viewer)
					.getLabelProvider();
			if (prov instanceof ILabelProvider) {
				ILabelProvider lprov = (ILabelProvider) prov;
				if (lprov instanceof DecoratingLabelProvider) {
					// Bug 364735: use the real label provider to avoid unstable
					// sort behavior if the decoration is running while sorting.
					// decorations are usually visual aids to the user and
					// shouldn't be used in ordering.
					DecoratingLabelProvider dprov = (DecoratingLabelProvider) lprov;
					lprov = dprov.getLabelProvider();
				}
				name1 = lprov.getText(e1);
			} else {
				name1 = e1.toString();
			}
		}
		if (name1 == null) {
			name1 = "";//$NON-NLS-1$
		}
		return name1;
	}

	/**
	 * Returns whether this viewer sorter would be affected
	 * by a change to the given property of the given element.
	 * <p>
	 * The default implementation of this method returns <code>false</code>.
	 * Subclasses may reimplement.
	 * </p>
	 *
	 * @param element the element
	 * @param property the property
	 * @return <code>true</code> if the sorting would be affected,
	 *    and <code>false</code> if it would be unaffected
	 */
	public boolean isSorterProperty(Object element, String property) {
		return false;
	}

	/**
	 * Sorts the given elements in-place, modifying the given array.
	 * <p>
	 * The default implementation of this method uses the
	 * {@link java.util.Arrays#sort(Object[], Comparator)} algorithm on the
	 * given array, calling {@link #compare(Viewer, Object, Object)} to compare
	 * elements.
	 * </p>
	 * <p>
	 * Subclasses may reimplement this method to provide a more optimized implementation.
	 * </p>
	 *
	 * @param viewer the viewer
	 * @param elements the elements to sort
	 */
	public void sort(final Viewer viewer, Object[] elements) {
		try {
			Arrays.sort(elements, (a, b) -> ViewerComparator.this.compare(viewer, a, b));
		} catch (IllegalArgumentException e) {
			String msg = e
					+ "\nWorkaround for comparator violation:\n\tSet system property -Djava.util.Arrays.useLegacyMergeSort=true" //$NON-NLS-1$
					+ "\nthis: " + getClass().getName() //$NON-NLS-1$
					+ "\ncomparator: " + (comparator != null ? comparator.getClass().getName() : null) //$NON-NLS-1$
					+ "\narray:"; //$NON-NLS-1$
			StringBuilder labels = new StringBuilder();
			long timeout = System.currentTimeMillis() + 5000;
			for (Object element : elements) {
				labels.append("\n\t"); //$NON-NLS-1$
				if (labels.length() > 50000) {
					labels.append("... (more elements)"); //$NON-NLS-1$
					break;
				} else if (System.currentTimeMillis() > timeout) {
					labels.append("... (timeout)"); //$NON-NLS-1$
					break;
				}
				labels.append(getLabel(viewer, element));
			}
			msg += labels;
			Policy.getLog().log(new Status(IStatus.ERROR, "org.eclipse.jface", msg)); //$NON-NLS-1$
			throw e;
		}
	}
}
