/*******************************************************************************
 * Copyright (c) 2000, 2022 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
 *     Brock Janiczak (brockj@tpg.com.au)
 *         - https://bugs.eclipse.org/bugs/show_bug.cgi?id=102236: [JUnit] display execution time next to each test
 *     Xavier Coulon <xcoulon@redhat.com> - https://bugs.eclipse.org/bugs/show_bug.cgi?id=102512 - [JUnit] test method name cut off before (
 *     Andrej Zachar <andrej@chocolatejar.eu> - [JUnit] Add a filter for ignored tests - https://bugs.eclipse.org/bugs/show_bug.cgi?id=298603
 *     Gautier de Saint Martin Lacaze <gautier.desaintmartinlacaze@gmail.com> - [JUnit] need 'collapse all' feature in JUnit view - https://bugs.eclipse.org/bugs/show_bug.cgi?id=277806
 *     Sandra Lions <sandra.lions-piron@oracle.com> - [JUnit] allow to sort by name and by execution time - https://bugs.eclipse.org/bugs/show_bug.cgi?id=219466
 *******************************************************************************/

package org.eclipse.jdt.internal.junit.ui;

import java.util.AbstractList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.stream.Collectors;

import org.eclipse.jdt.junit.model.ITestElement;
import org.eclipse.jdt.junit.model.ITestElement.Result;

import org.eclipse.swt.SWT;
import org.eclipse.swt.dnd.Clipboard;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.TableItem;

import org.eclipse.core.runtime.CoreException;

import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.viewers.AbstractTreeViewer;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.StructuredViewer;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerComparator;
import org.eclipse.jface.viewers.ViewerFilter;

import org.eclipse.ui.IWorkbenchActionConstants;
import org.eclipse.ui.part.PageBook;
import org.eclipse.ui.views.WorkbenchViewerSetup;

import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.debug.core.ILaunchManager;

import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IType;

import org.eclipse.jdt.internal.junit.launcher.ITestFinder;
import org.eclipse.jdt.internal.junit.model.TestCaseElement;
import org.eclipse.jdt.internal.junit.model.TestElement;
import org.eclipse.jdt.internal.junit.model.TestElement.Status;
import org.eclipse.jdt.internal.junit.model.TestRoot;
import org.eclipse.jdt.internal.junit.model.TestRunSession;
import org.eclipse.jdt.internal.junit.model.TestSuiteElement;
import org.eclipse.jdt.internal.junit.ui.TestRunnerViewPart.SortingCriterion;

import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants;

import org.eclipse.jdt.internal.ui.viewsupport.ColoringLabelProvider;
import org.eclipse.jdt.internal.ui.viewsupport.SelectionProviderMediator;


public class TestViewer {
	private final class TestSelectionListener implements ISelectionChangedListener {
		@Override
		public void selectionChanged(SelectionChangedEvent event) {
			handleSelected();
		}
	}

	private final class TestOpenListener extends SelectionAdapter {
		@Override
		public void widgetDefaultSelected(SelectionEvent e) {
			handleDefaultSelected();
		}
	}

	private final class FailuresOnlyFilter extends ViewerFilter {
		@Override
		public boolean select(Viewer viewer, Object parentElement, Object element) {
			return select(((TestElement) element));
		}

		public boolean select(TestElement testElement) {
			Status status= testElement.getStatus();
			if (status.isErrorOrFailure())
				return true;
			else
				return ! fTestRunSession.isRunning() && status == Status.RUNNING;  // rerunning
		}
	}

	private final class IgnoredOnlyFilter extends ViewerFilter {
		@Override
		public boolean select(Viewer viewer, Object parentElement, Object element) {
			return select(((TestElement) element));
		}

		public boolean select(TestElement testElement) {
			if (hasIgnoredInTestResult(testElement))
				return true;
			else
				return !fTestRunSession.isRunning() && testElement.getStatus() == Status.RUNNING; // rerunning
		}

		/**
		 * Checks whether a test was skipped i.e. it was ignored (<code>@Ignored</code>) or had any
		 * assumption failure.
		 *
		 * @param testElement the test element (a test suite or a single test case)
		 *
		 * @return <code>true</code> if the test element or any of its children has
		 *         {@link Result#IGNORED} test result
		 */
		private boolean hasIgnoredInTestResult(TestElement testElement) {
			if (testElement instanceof TestSuiteElement) {
				ITestElement[] children= ((TestSuiteElement) testElement).getChildren();
				for (ITestElement child : children) {
					boolean hasIgnoredTestResult= hasIgnoredInTestResult((TestElement) child);
					if (hasIgnoredTestResult) {
						return true;
					}
				}
				return false;
			}

			return testElement.getTestResult(false) == Result.IGNORED;
		}
	}


	private static class ReverseList<E> extends AbstractList<E> {
		private final List<E> fList;
		public ReverseList(List<E> list) {
			fList= list;
		}
		@Override
		public E get(int index) {
			return fList.get(fList.size() - index - 1);
		}
		@Override
		public int size() {
			return fList.size();
		}
	}

	private class ExpandAllAction extends Action {
		public ExpandAllAction() {
			setText(JUnitMessages.ExpandAllAction_text);
			setToolTipText(JUnitMessages.ExpandAllAction_tooltip);
			setImageDescriptor(JUnitPlugin.getImageDescriptor("elcl16/expandall.png")); //$NON-NLS-1$
		}

		@Override
		public void run(){
			fTreeViewer.expandAll();
		}
	}

	private class CollapseAllAction extends Action {
		public CollapseAllAction() {
			setText(JUnitMessages.CollapseAllAction_text);
			setToolTipText(JUnitMessages.CollapseAllAction_tooltip);
			setImageDescriptor(JUnitPlugin.getImageDescriptor("elcl16/collapseall.png")); //$NON-NLS-1$
		}

		@Override
		public void run(){
			fTreeViewer.collapseAll();
		}
	}

	private final FailuresOnlyFilter fFailuresOnlyFilter= new FailuresOnlyFilter();
	private final IgnoredOnlyFilter fIgnoredOnlyFilter= new IgnoredOnlyFilter();

	private final TestRunnerViewPart fTestRunnerPart;
	private final Clipboard fClipboard;

	private PageBook fViewerbook;
	private TreeViewer fTreeViewer;
	private TestSessionTreeContentProvider fTreeContentProvider;
	private TestSessionLabelProvider fTreeLabelProvider;
	private TableViewer fTableViewer;
	private TestSessionTableContentProvider fTableContentProvider;
	private TestSessionLabelProvider fTableLabelProvider;
	private SelectionProviderMediator fSelectionProvider;

	private int fLayoutMode;
	private boolean fTreeHasFilter;
	private boolean fTableHasFilter;

	private TestRunSession fTestRunSession;

	private boolean fTreeNeedsRefresh;
	private boolean fTableNeedsRefresh;
	private HashSet<TestElement> fNeedUpdate;
	private TestCaseElement fAutoScrollTarget;

	private LinkedList<TestSuiteElement> fAutoClose;
	private HashSet<TestSuiteElement> fAutoExpand;


	public TestViewer(Composite parent, Clipboard clipboard, TestRunnerViewPart runner) {
		fTestRunnerPart= runner;
		fClipboard= clipboard;

		fLayoutMode= TestRunnerViewPart.LAYOUT_HIERARCHICAL;

		createTestViewers(parent);

		registerViewersRefresh();

		initContextMenu();
	}

	private void createTestViewers(Composite parent) {
		fViewerbook= new PageBook(parent, SWT.NULL);

		fTreeViewer= new TreeViewer(fViewerbook, SWT.V_SCROLL | SWT.SINGLE);
		fTreeViewer.setUseHashlookup(true);
		WorkbenchViewerSetup.setupViewer(fTreeViewer);
		fTreeContentProvider= new TestSessionTreeContentProvider();
		fTreeViewer.setContentProvider(fTreeContentProvider);
		fTreeLabelProvider= new TestSessionLabelProvider(fTestRunnerPart, TestRunnerViewPart.LAYOUT_HIERARCHICAL);
		fTreeViewer.setLabelProvider(new ColoringLabelProvider(fTreeLabelProvider));

		fTableViewer= new TableViewer(fViewerbook, SWT.V_SCROLL | SWT.H_SCROLL | SWT.SINGLE);
		fTableViewer.setUseHashlookup(true);
		fTableContentProvider= new TestSessionTableContentProvider();
		fTableViewer.setContentProvider(fTableContentProvider);
		fTableLabelProvider= new TestSessionLabelProvider(fTestRunnerPart, TestRunnerViewPart.LAYOUT_FLAT);
		fTableViewer.setLabelProvider(new ColoringLabelProvider(fTableLabelProvider));

		fSelectionProvider= new SelectionProviderMediator(new StructuredViewer[] { fTreeViewer, fTableViewer }, fTreeViewer);
		fSelectionProvider.addSelectionChangedListener(new TestSelectionListener());
		TestOpenListener testOpenListener= new TestOpenListener();
		fTreeViewer.getTree().addSelectionListener(testOpenListener);
		fTableViewer.getTable().addSelectionListener(testOpenListener);

		fTestRunnerPart.getSite().setSelectionProvider(fSelectionProvider);

		fViewerbook.showPage(fTreeViewer.getTree());
	}

	private void initContextMenu() {
		MenuManager menuMgr= new MenuManager("#PopupMenu"); //$NON-NLS-1$
		menuMgr.setRemoveAllWhenShown(true);
		menuMgr.addMenuListener(this::handleMenuAboutToShow);
		fTestRunnerPart.getSite().registerContextMenu(menuMgr, fSelectionProvider);
		Menu menu= menuMgr.createContextMenu(fViewerbook);
		fTreeViewer.getTree().setMenu(menu);
		fTableViewer.getTable().setMenu(menu);
	}


	void handleMenuAboutToShow(IMenuManager manager) {
		IStructuredSelection selection= (IStructuredSelection) fSelectionProvider.getSelection();
		if (! selection.isEmpty()) {
			TestElement testElement= (TestElement) selection.getFirstElement();

			if (testElement instanceof TestSuiteElement) {
				TestSuiteElement testSuiteElement= (TestSuiteElement) testElement;
				manager.add(getOpenTestAction(testSuiteElement));
				manager.add(new Separator());
				if (!fTestRunnerPart.lastLaunchIsKeptAlive()) {
					addRerunActions(manager, testSuiteElement);
				}
			} else {
				TestCaseElement testCaseElement= (TestCaseElement) testElement;
				manager.add(getOpenTestAction(testCaseElement));
				manager.add(new Separator());
				addRerunActions(manager, testCaseElement);
			}
			if (fLayoutMode == TestRunnerViewPart.LAYOUT_HIERARCHICAL) {
				manager.add(new Separator());
				manager.add(new ExpandAllAction());
				manager.add(new CollapseAllAction());
			}

		}
		if (fTestRunSession != null && fTestRunSession.getFailureCount() + fTestRunSession.getErrorCount() > 0) {
			if (fLayoutMode != TestRunnerViewPart.LAYOUT_HIERARCHICAL)
				manager.add(new Separator());
			manager.add(new CopyFailureListAction(fTestRunnerPart, fClipboard));
		}
		manager.add(new Separator(IWorkbenchActionConstants.MB_ADDITIONS));
		manager.add(new Separator(IWorkbenchActionConstants.MB_ADDITIONS + "-end")); //$NON-NLS-1$
	}

	private void addRerunActions(IMenuManager manager, TestCaseElement testCaseElement) {
		String className= testCaseElement.getClassName();
		String testMethodName= testCaseElement.getTestMethodName();
		String[] parameterTypes= testCaseElement.getParameterTypes();
		if (parameterTypes != null) {
			String paramTypesStr= Arrays.stream(parameterTypes).collect(Collectors.joining(",")); //$NON-NLS-1$
			testMethodName= testMethodName + "(" + paramTypesStr + ")"; //$NON-NLS-1$ //$NON-NLS-2$
		}
		if (fTestRunnerPart.lastLaunchIsKeptAlive()) {
			manager.add(new RerunAction(JUnitMessages.RerunAction_label_rerun, fTestRunnerPart, testCaseElement.getId(), className, testMethodName, testCaseElement.getDisplayName(), testCaseElement.getUniqueId(), ILaunchManager.RUN_MODE));
		} else {
			manager.add(new RerunAction(JUnitMessages.RerunAction_label_run, fTestRunnerPart, testCaseElement.getId(), className, testMethodName, testCaseElement.getDisplayName(), testCaseElement.getUniqueId(), ILaunchManager.RUN_MODE));
			manager.add(new RerunAction(JUnitMessages.RerunAction_label_debug, fTestRunnerPart, testCaseElement.getId(), className, testMethodName, testCaseElement.getDisplayName(), testCaseElement.getUniqueId(), ILaunchManager.DEBUG_MODE));
		}
	}

	private void addRerunActions(IMenuManager manager, TestSuiteElement testSuiteElement) {
		String qualifiedName= null;
		String testMethodName= null; // test method name is null when re-running a regular test class

		String testName= testSuiteElement.getTestName();

		IType testType= findTestClass(testSuiteElement, true);
		if (testType != null) {
			qualifiedName= testType.getFullyQualifiedName();

			if (!qualifiedName.equals(testName)) {
				int index= testName.indexOf('(');
				if (index > 0) { // test factory method
					testMethodName= testName.substring(0, index);
				}
			}
			String[] parameterTypes= testSuiteElement.getParameterTypes();
			if (testMethodName != null && parameterTypes != null) {
				String paramTypesStr= Arrays.stream(parameterTypes).collect(Collectors.joining(",")); //$NON-NLS-1$
				testMethodName= testMethodName + "(" + paramTypesStr + ")"; //$NON-NLS-1$ //$NON-NLS-2$
			}
		} else {
			// see bug 443498
			testType= findTestClass(testSuiteElement.getParent(), false);
			if (testType != null) {
				qualifiedName= testType.getFullyQualifiedName();

				String className= testSuiteElement.getSuiteTypeName();
				if (!qualifiedName.equals(className)) {
					testMethodName= testName;
				}
			}
		}
		if (qualifiedName != null) {
			manager.add(new RerunAction(JUnitMessages.RerunAction_label_run, fTestRunnerPart, testSuiteElement.getId(), qualifiedName, testMethodName, testSuiteElement.getDisplayName(), testSuiteElement.getUniqueId(), ILaunchManager.RUN_MODE));
			manager.add(new RerunAction(JUnitMessages.RerunAction_label_debug, fTestRunnerPart, testSuiteElement.getId(), qualifiedName, testMethodName, testSuiteElement.getDisplayName(), testSuiteElement.getUniqueId(), ILaunchManager.DEBUG_MODE));
		}
	}

	/*
	 * Returns the element's test class or the next container's test class, which exists, and for which ITestFinder.isTest() is true.
	 */
	private IType findTestClass(ITestElement element, boolean checkOnlyCurrentElement) {
		ITestFinder finder= ((TestRunSession)element.getTestRunSession()).getTestRunnerKind().getFinder();
		if (ITestFinder.NULL.equals(finder)) {
			return null;
		}
		IJavaProject project= fTestRunnerPart.getLaunchedProject();
		if (project == null)
			return null;
		ITestElement current= element;
		while (current != null) {
			try {
				String className= null;
				if (current instanceof TestRoot) {
					ILaunch launch= ((TestRunSession) element.getTestRunSession()).getLaunch();
					if (launch != null) {
						ILaunchConfiguration configuration= launch.getLaunchConfiguration();
						if (configuration != null) {
							className= configuration.getAttribute(IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME, (String) null);
						}
					}
				} else if (current instanceof TestElement) {
					className= ((TestElement)current).getClassName();
				}
				if (className != null) {
					IType type= project.findType(className);
					if (type != null && finder.isTest(type)) {
						return type;
					} else if (checkOnlyCurrentElement) {
						return null;
					}
				}
			} catch (CoreException e) {
				// fall through
			}
			current= current instanceof TestElement ? ((TestElement)current).getParent() : null;
		}
		return null;
	}

	public Control getTestViewerControl() {
		return fViewerbook;
	}

	public synchronized void registerActiveSession(TestRunSession testRunSession) {
		fTestRunSession= testRunSession;
		registerAutoScrollTarget(null);
		registerViewersRefresh();
	}

	void handleDefaultSelected() {
		IStructuredSelection selection= (IStructuredSelection) fSelectionProvider.getSelection();
		if (selection.size() != 1)
			return;

		TestElement testElement= (TestElement) selection.getFirstElement();

		OpenTestAction action;
		if (testElement instanceof TestSuiteElement) {
			action= getOpenTestAction((TestSuiteElement) testElement);
		} else if (testElement instanceof TestCaseElement) {
			action= getOpenTestAction((TestCaseElement) testElement);
		} else {
			throw new IllegalStateException(String.valueOf(testElement));
		}

		if (action.isEnabled())
			action.run();
	}

	private OpenTestAction getOpenTestAction(TestCaseElement testCase) {
		return new OpenTestAction(fTestRunnerPart, testCase, testCase.getParameterTypes());
	}

	private OpenTestAction getOpenTestAction(TestSuiteElement testSuite) {
		String testName= testSuite.getTestName();
		ITestElement[] children= testSuite.getChildren();

		if (testName.startsWith("[") && testName.endsWith("]") && children.length > 0 && children[0] instanceof TestCaseElement) { //$NON-NLS-1$ //$NON-NLS-2$
			// a group of parameterized tests
			return new OpenTestAction(fTestRunnerPart, (TestCaseElement) children[0], null);
		}

		int index= testName.indexOf('(');
		// test factory method
		if (index > 0) {
			return new OpenTestAction(fTestRunnerPart, testSuite.getSuiteTypeName(), testName.substring(0, index), testSuite.getParameterTypes(), true);
		}

		// regular test class
		return new OpenTestAction(fTestRunnerPart, testName);
	}

	private void handleSelected() {
		IStructuredSelection selection= (IStructuredSelection) fSelectionProvider.getSelection();
		TestElement testElement= null;
		if (selection.size() == 1) {
			testElement= (TestElement) selection.getFirstElement();
		}
		fTestRunnerPart.handleTestSelected(testElement);
	}

	public synchronized void setShowTime(boolean showTime) {
		try {
			fViewerbook.setRedraw(false);
			fTreeLabelProvider.setShowTime(showTime);
			fTableLabelProvider.setShowTime(showTime);
		} finally {
			fViewerbook.setRedraw(true);
		}
	}

	public synchronized void setSortingCriterion(SortingCriterion sortingCriterion) {
		ViewerComparator viewerComparator;
		switch (sortingCriterion) {
			case SORT_BY_EXECUTION_ORDER:
				viewerComparator= null;
				break;
			case SORT_BY_EXECUTION_TIME:
				viewerComparator= new TestExecutionTimeComparator();
				break;
			case SORT_BY_NAME:
				viewerComparator= new TestNameComparator();
				break;
			default:
				viewerComparator= null;
				break;
		}
		fTableViewer.setComparator(viewerComparator);
		fTreeViewer.setComparator(viewerComparator);
	}

	private final class TestNameComparator extends ViewerComparator {
		@Override
		public int compare(Viewer viewer, Object o1, Object o2) {
			return compareName(o1, o2);
		}
	}

	private final class TestExecutionTimeComparator extends ViewerComparator {
		@Override
		public int compare(Viewer viewer, Object o1, Object o2) {
			return compareElapsedTime(o1, o2);
		}
	}

	private Comparator<ITestElement> getComparator() {
		SortingCriterion sortingCriterion= fTestRunnerPart.getSortingCriterion();
		Comparator<ITestElement> comparator;
		switch (sortingCriterion) {
			case SORT_BY_EXECUTION_ORDER:
				comparator= null;
				break;
			case SORT_BY_EXECUTION_TIME:
				comparator= new Comparator<>() {
					@Override
					public int compare(ITestElement o1, ITestElement o2) {
						return compareElapsedTime(o1, o2);
					}
				};
				break;
			case SORT_BY_NAME:
				comparator= new Comparator<>() {
					@Override
					public int compare(ITestElement o1, ITestElement o2) {
						return compareName(o1, o2);
					}
				};
				break;
			default:
				comparator= null;
				break;
		}
		return comparator;
	}

	private int compareElapsedTime(Object o1, Object o2) {
		double elapsedTime1= ((TestElement)o1).getElapsedTimeInSeconds();
		double elapsedTime2= ((TestElement)o2).getElapsedTimeInSeconds();
		return Double.compare(elapsedTime2, elapsedTime1);
	}

	private int compareName(Object o1, Object o2) {
		String testName1= ((TestElement)o1).getTestName();
		String testName2= ((TestElement)o2).getTestName();
		return testName1.toLowerCase().compareTo(testName2.toLowerCase());
	}

	/**
	 * It makes sense to display either failed or ignored tests, not both together.
	 *
	 * @param failuresOnly whether to show only failed tests
	 * @param ignoredOnly whether to show only skipped tests
	 * @param layoutMode the layout mode
	 */
	public synchronized void setShowFailuresOrIgnoredOnly(boolean failuresOnly, boolean ignoredOnly, int layoutMode) {
		/*
		 * Management of fTreeViewer and fTableViewer
		 * ******************************************
		 * - invisible viewer is updated on registerViewerUpdate unless its f*NeedsRefresh is true
		 * - invisible viewer is not refreshed upfront
		 * - on layout change, new viewer is refreshed if necessary
		 * - filter only applies to "current" layout mode / viewer
		 */
		try {
			fViewerbook.setRedraw(false);

			IStructuredSelection selection= null;
			boolean switchLayout= layoutMode != fLayoutMode;
			if (switchLayout) {
				selection= (IStructuredSelection) fSelectionProvider.getSelection();
				if (layoutMode == TestRunnerViewPart.LAYOUT_HIERARCHICAL) {
					if (fTreeNeedsRefresh) {
						clearUpdateAndExpansion();
					}
				} else {
					if (fTableNeedsRefresh) {
						clearUpdateAndExpansion();
					}
				}
				fLayoutMode= layoutMode;
				fViewerbook.showPage(getActiveViewer().getControl());
			}
			//avoid realizing all TableItems, especially in flat mode!
			StructuredViewer viewer= getActiveViewer();
			if (failuresOnly || ignoredOnly) {
				if (getActiveViewerHasFilter()) {
					//For simplicity clear both filters (only one of them is used)
					viewer.removeFilter(fFailuresOnlyFilter);
					viewer.removeFilter(fIgnoredOnlyFilter);
				}
				setActiveViewerHasFilter(true);
				viewer.setInput(null);
				//Set either the failures or the skipped tests filter
				ViewerFilter filter= fFailuresOnlyFilter;
				if (ignoredOnly == true) {
					filter= fIgnoredOnlyFilter;
				}
				viewer.addFilter(filter);
				setActiveViewerNeedsRefresh(true);

			} else {
				if (getActiveViewerHasFilter()) {
					setActiveViewerNeedsRefresh(true);
					setActiveViewerHasFilter(false);
					viewer.setInput(null);
					viewer.removeFilter(fIgnoredOnlyFilter);
					viewer.removeFilter(fFailuresOnlyFilter);
				}
			}
			processChangesInUI();

			if (selection != null) {
				// workaround for https://bugs.eclipse.org/bugs/show_bug.cgi?id=125708
				// (ITreeSelection not adapted if TreePaths changed):
				StructuredSelection flatSelection= new StructuredSelection(selection.toList());
				fSelectionProvider.setSelection(flatSelection, true);
			}

		} finally {
			fViewerbook.setRedraw(true);
		}
	}

	private boolean getActiveViewerHasFilter() {
		if (fLayoutMode == TestRunnerViewPart.LAYOUT_HIERARCHICAL)
			return fTreeHasFilter;
		else
			return fTableHasFilter;
	}

	private void setActiveViewerHasFilter(boolean filter) {
		if (fLayoutMode == TestRunnerViewPart.LAYOUT_HIERARCHICAL)
			fTreeHasFilter= filter;
		else
			fTableHasFilter= filter;
	}

	public StructuredViewer getActiveViewer() {
		if (fLayoutMode == TestRunnerViewPart.LAYOUT_HIERARCHICAL)
			return fTreeViewer;
		else
			return fTableViewer;
	}

	private boolean getActiveViewerNeedsRefresh() {
		if (fLayoutMode == TestRunnerViewPart.LAYOUT_HIERARCHICAL)
			return fTreeNeedsRefresh;
		else
			return fTableNeedsRefresh;
	}

	private void setActiveViewerNeedsRefresh(boolean needsRefresh) {
		if (fLayoutMode == TestRunnerViewPart.LAYOUT_HIERARCHICAL)
			fTreeNeedsRefresh= needsRefresh;
		else
			fTableNeedsRefresh= needsRefresh;
	}

	/**
	 * To be called periodically by the TestRunnerViewPart (in the UI thread).
	 */
	public void processChangesInUI() {
		TestRoot testRoot;
		if (fTestRunSession == null) {
			registerViewersRefresh();
			fTreeNeedsRefresh= false;
			fTableNeedsRefresh= false;
			fTreeViewer.setInput(null);
			fTableViewer.setInput(null);
			return;
		}

		testRoot= fTestRunSession.getTestRoot();

		StructuredViewer viewer= getActiveViewer();
		if (getActiveViewerNeedsRefresh()) {
			clearUpdateAndExpansion();
			setActiveViewerNeedsRefresh(false);
			viewer.setInput(testRoot);

		} else {
			Object[] toUpdate;
			synchronized (this) {
				toUpdate= fNeedUpdate.toArray();
				fNeedUpdate.clear();
			}
			if (! fTreeNeedsRefresh && toUpdate.length > 0) {
				if (fTreeHasFilter)
					for (Object element : toUpdate)
						updateElementInTree((TestElement) element);
				else {
					HashSet<Object> toUpdateWithParents= new HashSet<>(Arrays.asList(toUpdate));
					for (Object element : toUpdate) {
						TestElement parent= ((TestElement) element).getParent();
						while (parent != null) {
							toUpdateWithParents.add(parent);
							parent= parent.getParent();
						}
					}
					fTreeViewer.update(toUpdateWithParents.toArray(), null);
				}
			}
			if (! fTableNeedsRefresh && toUpdate.length > 0) {
				if (fTableHasFilter)
					for (Object element : toUpdate)
						updateElementInTable((TestElement) element);
				else
					fTableViewer.update(toUpdate, null);
			}
		}
		autoScrollInUI();
	}

	private void updateElementInTree(final TestElement testElement) {
		if (isShown(testElement)) {
			updateShownElementInTree(testElement);
		} else {
			TestElement current= testElement;
			do {
				if (fTreeViewer.testFindItem(current) != null)
					fTreeViewer.remove(current);
				current= current.getParent();
			} while (! (current instanceof TestRoot) && ! isShown(current));

			while (current != null && ! (current instanceof TestRoot)) {
				fTreeViewer.update(current, null);
				current= current.getParent();
			}
		}
	}

	private void updateShownElementInTree(TestElement testElement) {
		if (testElement == null || testElement instanceof TestRoot) // paranoia null check
			return;

		TestSuiteElement parent= testElement.getParent();
		updateShownElementInTree(parent); // make sure parent is shown and up-to-date

		if (fTreeViewer.testFindItem(testElement) == null) {
			fTreeViewer.add(parent, testElement); // if not yet in tree: add
		} else {
			fTreeViewer.update(testElement, null); // if in tree: update
		}
	}

	private void updateElementInTable(TestElement element) {
		if (isShown(element)) {
			if (fTableViewer.testFindItem(element) == null) {
				TestElement previous= getNextFailure(element, false);
				int insertionIndex= -1;
				if (previous != null) {
					TableItem item= (TableItem) fTableViewer.testFindItem(previous);
					if (item != null)
						insertionIndex= fTableViewer.getTable().indexOf(item);
				}
				fTableViewer.insert(element, insertionIndex);
			} else  {
				fTableViewer.update(element, null);
			}
		} else {
			fTableViewer.remove(element);
		}
	}

	private boolean isShown(TestElement current) {
		return fFailuresOnlyFilter.select(current);
	}

	private void autoScrollInUI() {
		if (! fTestRunnerPart.isAutoScroll()) {
			clearAutoExpand();
			fAutoClose.clear();
			return;
		}

		if (fLayoutMode == TestRunnerViewPart.LAYOUT_FLAT) {
			if (fAutoScrollTarget != null)
				fTableViewer.reveal(fAutoScrollTarget);
			return;
		}

		synchronized (this) {
			for (TestSuiteElement suite : fAutoExpand) {
				fTreeViewer.setExpandedState(suite, true);
			}
			clearAutoExpand();
		}

		TestCaseElement current= fAutoScrollTarget;
		fAutoScrollTarget= null;

		TestSuiteElement parent= current == null ? null : (TestSuiteElement) fTreeContentProvider.getParent(current);
		if (fAutoClose.isEmpty() || ! fAutoClose.getLast().equals(parent)) {
			// we're in a new branch, so let's close old OK branches:
			for (ListIterator<TestSuiteElement> iter= fAutoClose.listIterator(fAutoClose.size()); iter.hasPrevious();) {
				TestSuiteElement previousAutoOpened= iter.previous();
				if (previousAutoOpened.equals(parent))
					break;

				if (previousAutoOpened.getStatus() == TestElement.Status.OK) {
					// auto-opened the element, and all children are OK -> auto close
					iter.remove();
					fTreeViewer.collapseToLevel(previousAutoOpened, AbstractTreeViewer.ALL_LEVELS);
				}
			}

			while (parent != null && ! fTestRunSession.getTestRoot().equals(parent) && fTreeViewer.getExpandedState(parent) == false) {
				fAutoClose.add(parent); // add to auto-opened elements -> close later if STATUS_OK
				parent= (TestSuiteElement) fTreeContentProvider.getParent(parent);
			}
		}
		if (current != null)
			fTreeViewer.reveal(current);
	}

	public void selectFirstFailure() {
		TestElement firstFailure= getNextChildFailure(fTestRunSession.getTestRoot(), true);
		if (firstFailure != null)
			getActiveViewer().setSelection(new StructuredSelection(firstFailure), true);
	}

	public void selectFailure(boolean showNext) {
		IStructuredSelection selection= (IStructuredSelection) getActiveViewer().getSelection();
		TestElement selected= (TestElement) selection.getFirstElement();
		TestElement next;

		if (selected == null) {
			next= getNextChildFailure(fTestRunSession.getTestRoot(), showNext);
		} else {
			next= getNextFailure(selected, showNext);
		}

		if (next != null)
			getActiveViewer().setSelection(new StructuredSelection(next), true);
	}

	private TestElement getNextFailure(TestElement selected, boolean showNext) {
		if (selected instanceof TestSuiteElement) {
			TestElement nextChild= getNextChildFailure((TestSuiteElement) selected, showNext);
			if (nextChild != null)
				return nextChild;
		}
		return getNextFailureSibling(selected, showNext);
	}

	private TestElement getNextFailureSibling(TestElement current, boolean showNext) {
		TestSuiteElement parent= current.getParent();
		if (parent == null)
			return null;

		ITestElement[] elements= parent.getChildren();
		Comparator<ITestElement> comparator= getComparator();
		if (comparator != null) {
			Arrays.sort(elements, comparator);
		}
		List<ITestElement> siblings= Arrays.asList(elements);

		if (! showNext)
			siblings= new ReverseList<>(siblings);

		int nextIndex= siblings.indexOf(current) + 1;
		for (int i= nextIndex; i < siblings.size(); i++) {
			TestElement sibling= (TestElement) siblings.get(i);
			if (sibling.getStatus().isErrorOrFailure()) {
				if (sibling instanceof TestCaseElement) {
					return sibling;
				} else {
					TestSuiteElement testSuiteElement= (TestSuiteElement) sibling;
					if (testSuiteElement.getChildren().length == 0) {
						return testSuiteElement;
					}
					return getNextChildFailure(testSuiteElement, showNext);
				}
			}
		}
		return getNextFailureSibling(parent, showNext);
	}

	private TestElement getNextChildFailure(TestSuiteElement root, boolean showNext) {
		ITestElement[] elements= root.getChildren();
		Comparator<ITestElement> comparator= getComparator();
		if (comparator != null) {
			Arrays.sort(elements, comparator);
		}
		List<ITestElement> children= Arrays.asList(elements);
		if (! showNext)
			children= new ReverseList<>(children);
		for (ITestElement element : children) {
			TestElement child= (TestElement) element;
			if (child.getStatus().isErrorOrFailure()) {
				if (child instanceof TestCaseElement) {
					return child;
				} else {
					TestSuiteElement testSuiteElement= (TestSuiteElement) child;
					if (testSuiteElement.getChildren().length == 0) {
						return testSuiteElement;
					}
					return getNextChildFailure(testSuiteElement, showNext);
				}
			}
		}
		return null;
	}

	public synchronized void registerViewersRefresh() {
		fTreeNeedsRefresh= true;
		fTableNeedsRefresh= true;
		clearUpdateAndExpansion();
	}

	private void clearUpdateAndExpansion() {
		fNeedUpdate= new LinkedHashSet<>();
		fAutoClose= new LinkedList<>();
		fAutoExpand= new HashSet<>();
	}

	/**
	 * @param testElement the added test
	 */
	public synchronized void registerTestAdded(TestElement testElement) {
		//TODO: performance: would only need to refresh parent of added element
		fTreeNeedsRefresh= true;
		fTableNeedsRefresh= true;
	}

	public synchronized void registerViewerUpdate(final TestElement testElement) {
		fNeedUpdate.add(testElement);
	}

	private synchronized void clearAutoExpand() {
		fAutoExpand.clear();
	}

	public void registerAutoScrollTarget(TestCaseElement testCaseElement) {
		fAutoScrollTarget= testCaseElement;
	}

	public synchronized void registerFailedForAutoScroll(TestElement testElement) {
		TestSuiteElement parent= (TestSuiteElement) fTreeContentProvider.getParent(testElement);
		if (parent != null)
			fAutoExpand.add(parent);
	}

	public void expandFirstLevel() {
		fTreeViewer.expandToLevel(2);
	}

}

