/*******************************************************************************
 * Copyright (c) 2000, 2018 IBM Corporation and others.
 * 
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.linuxtools.internal.docker.ui;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.eclipse.core.commands.common.EventManager;
import org.eclipse.core.runtime.SafeRunner;
import org.eclipse.jface.util.SafeRunnable;
import org.eclipse.jface.viewers.CheckStateChangedEvent;
import org.eclipse.jface.viewers.CheckboxTableViewer;
import org.eclipse.jface.viewers.CheckboxTreeViewer;
import org.eclipse.jface.viewers.ICheckStateListener;
import org.eclipse.jface.viewers.ILabelProvider;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredContentProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.ITreeViewerListener;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TreeExpansionEvent;
import org.eclipse.jface.viewers.ViewerComparator;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.BusyIndicator;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.Tree;

/**
 * Workbench-level composite that combines a CheckboxTreeViewer and
 * CheckboxListViewer. All viewer selection-driven interactions are handled
 * within this object
 */
@SuppressWarnings("rawtypes")
public class CheckboxTreeAndListGroup extends EventManager implements
		ICheckStateListener, ISelectionChangedListener, ITreeViewerListener {
	private Object root;

	private Object currentTreeSelection;

	private List expandedTreeNodes = new ArrayList();

	private Map checkedStateStore = new HashMap(9);

	private List whiteCheckedTreeItems = new ArrayList();

	private ITreeContentProvider treeContentProvider;

	private ITreeContentProvider dynamicTreeContentProvider;

	private IStructuredContentProvider listContentProvider;

	private ILabelProvider treeLabelProvider;

	private ILabelProvider listLabelProvider;

	// widgets
	private CheckboxTreeViewer treeViewer;

	private CheckboxTableViewer listViewer;

	/**
	 * Create an instance of this class. Use this constructor if you wish to
	 * specify the width and/or height of the combined widget (to only hardcode
	 * one of the sizing dimensions, specify the other dimension's value as -1)
	 * 
	 * @param parent
	 * @param rootObject
	 * @param treeContentProvider
	 * @param treeLabelProvider
	 * @param listContentProvider
	 * @param listLabelProvider
	 * @param style
	 * @param width
	 * @param height
	 */
	public CheckboxTreeAndListGroup(Composite parent, Object rootObject,
			ITreeContentProvider treeContentProvider,
			ITreeContentProvider dynamicTreeContentProvider,
			ILabelProvider treeLabelProvider,
			IStructuredContentProvider listContentProvider,
			ILabelProvider listLabelProvider, int style, int width,
			int height) {

		root = rootObject;
		this.treeContentProvider = treeContentProvider;
		this.dynamicTreeContentProvider = dynamicTreeContentProvider;
		this.listContentProvider = listContentProvider;
		this.treeLabelProvider = treeLabelProvider;
		this.listLabelProvider = listLabelProvider;
		createContents(parent, width, height, style);
	}

	/**
	 * This method must be called just before this window becomes visible.
	 */
	public void aboutToOpen() {
		determineWhiteCheckedDescendents(root);
		checkNewTreeElements(treeContentProvider.getElements(root));
		currentTreeSelection = null;

		// select the first element in the list
		Object[] elements = treeContentProvider.getElements(root);
		Object primary = elements.length > 0 ? elements[0] : null;
		if (primary != null) {
			treeViewer.setSelection(new StructuredSelection(primary));
		}
		treeViewer.getControl().setFocus();
	}

	/**
	 * Add the passed listener to self's collection of clients that listen for
	 * changes to element checked states
	 *
	 * @param listener
	 *            ICheckStateListener
	 */
	public void addCheckStateListener(ICheckStateListener listener) {
		addListenerObject(listener);
	}

	/**
	 * Add the receiver and all of it's ancestors to the checkedStateStore if
	 * they are not already there.
	 */
	@SuppressWarnings("unchecked")
	private void addToHierarchyToCheckedStore(Object treeElement) {

		// if this tree element is already gray then its ancestors all are as
		// well
		if (!checkedStateStore.containsKey(treeElement)) {
			checkedStateStore.put(treeElement, new ArrayList());
		}

		Object parent = treeContentProvider.getParent(treeElement);
		if (parent != null) {
			addToHierarchyToCheckedStore(parent);
		}
	}

	/**
	 * Return a boolean indicating whether all children of the passed tree
	 * element are currently white-checked
	 *
	 * @return boolean
	 * @param treeElement
	 *            java.lang.Object
	 */
	protected boolean areAllChildrenWhiteChecked(Object treeElement) {
		Object[] children = treeContentProvider.getChildren(treeElement);
		for (int i = 0; i < children.length; ++i) {
			if (!whiteCheckedTreeItems.contains(children[i])) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Return a boolean indicating whether all list elements associated with the
	 * passed tree element are currently checked
	 *
	 * @return boolean
	 * @param treeElement
	 *            java.lang.Object
	 */
	protected boolean areAllElementsChecked(Object treeElement) {
		List checkedElements = (List) checkedStateStore.get(treeElement);
		if (checkedElements == null) {
			return false;
		}

		return getListItemsSize(treeElement) == checkedElements.size();
	}

	/**
	 * Iterate through the passed elements which are being realized for the
	 * first time and check each one in the tree viewer as appropriate
	 */
	protected void checkNewTreeElements(Object[] elements) {
		for (int i = 0; i < elements.length; ++i) {
			Object currentElement = elements[i];
			boolean checked = checkedStateStore.containsKey(currentElement);
			treeViewer.setChecked(currentElement, checked);
			treeViewer.setGrayed(currentElement,
					checked && !whiteCheckedTreeItems.contains(currentElement));
			// New - if in white list, make sure all descendent elements are
			// checked
			if (whiteCheckedTreeItems.contains(currentElement)) {
				setTreeChecked(currentElement, true);
			}
		}
	}

	/**
	 * An item was checked in one of self's two views. Determine which view this
	 * occurred in and delegate appropriately
	 *
	 * @param event
	 *            CheckStateChangedEvent
	 */
	@Override
	public void checkStateChanged(final CheckStateChangedEvent event) {

		// Potentially long operation - show a busy cursor
		BusyIndicator.showWhile(treeViewer.getControl().getDisplay(), () -> {
			if (event.getCheckable().equals(treeViewer)) {
				treeItemChecked(event.getElement(), event.getChecked());
			} else {
				listItemChecked(event.getElement(), event.getChecked(), true);
			}

			notifyCheckStateChangeListeners(event);
		});
	}

	/**
	 * Lay out and initialize self's visual components.
	 *
	 * @param parent
	 *            org.eclipse.swt.widgets.Composite
	 * @param width
	 *            int
	 * @param height
	 *            int
	 */
	protected void createContents(Composite parent, int width, int height,
			int style) {
		// group pane
		Composite composite = new Composite(parent, style);
		GridLayout layout = new GridLayout();
		layout.numColumns = 2;
		layout.makeColumnsEqualWidth = true;
		layout.marginHeight = 0;
		layout.marginWidth = 0;
		composite.setLayout(layout);
		composite.setLayoutData(new GridData(GridData.FILL_BOTH));
		composite.setFont(parent.getFont());

		createTreeViewer(composite, width / 2, height);
		createListViewer(composite, width / 2, height);

		initialize();
	}

	/**
	 * Create this group's list viewer.
	 */
	protected void createListViewer(Composite parent, int width, int height) {
		listViewer = CheckboxTableViewer.newCheckList(parent, SWT.BORDER);
		GridData data = new GridData(GridData.FILL_BOTH);
		data.widthHint = width;
		data.heightHint = height;
		listViewer.getTable().setLayoutData(data);
		listViewer.getTable().setFont(parent.getFont());
		listViewer.setContentProvider(listContentProvider);
		listViewer.setLabelProvider(listLabelProvider);
		listViewer.addCheckStateListener(this);
	}

	/**
	 * Create this group's tree viewer.
	 */
	protected void createTreeViewer(Composite parent, int width, int height) {
		Tree tree = new Tree(parent, SWT.CHECK | SWT.BORDER);
		GridData data = new GridData(GridData.FILL_BOTH);
		data.widthHint = width;
		data.heightHint = height;
		tree.setLayoutData(data);
		tree.setFont(parent.getFont());

		treeViewer = new DynamicCheckboxTreeViewer(tree,
				dynamicTreeContentProvider);
		treeViewer.setContentProvider(treeContentProvider);
		treeViewer.setLabelProvider(treeLabelProvider);
		treeViewer.addTreeListener(this);
		treeViewer.addCheckStateListener(this);
		treeViewer.addSelectionChangedListener(this);
	}

	/**
	 * Returns a boolean indicating whether the passed tree element should be at
	 * LEAST gray-checked. Note that this method does not consider whether it
	 * should be white-checked, so a specified tree item which should be
	 * white-checked will result in a <code>true</code> answer from this method.
	 * To determine whether a tree item should be white-checked use method
	 * #determineShouldBeWhiteChecked(Object).
	 *
	 * @param treeElement
	 *            java.lang.Object
	 * @return boolean
	 * @see #determineShouldBeWhiteChecked(java.lang.Object)
	 */
	protected boolean determineShouldBeAtLeastGrayChecked(Object treeElement) {
		// if any list items associated with treeElement are checked then it
		// retains its gray-checked status regardless of its children
		List checked = (List) checkedStateStore.get(treeElement);
		if (checked != null && (!checked.isEmpty())) {
			return true;
		}

		// if any children of treeElement are still gray-checked then
		// treeElement
		// must remain gray-checked as well
		Object[] children = treeContentProvider.getChildren(treeElement);
		for (int i = 0; i < children.length; ++i) {
			if (checkedStateStore.containsKey(children[i])) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Returns a boolean indicating whether the passed tree item should be
	 * white-checked.
	 *
	 * @return boolean
	 * @param treeElement
	 *            java.lang.Object
	 */
	protected boolean determineShouldBeWhiteChecked(Object treeElement) {
		return areAllChildrenWhiteChecked(treeElement)
				&& areAllElementsChecked(treeElement);
	}

	/**
	 * Recursively add appropriate tree elements to the collection of known
	 * white-checked tree elements.
	 *
	 * @param treeElement
	 *            java.lang.Object
	 */
	protected void determineWhiteCheckedDescendents(Object treeElement) {
		// always go through all children first since their white-checked
		// statuses will be needed to determine the white-checked status for
		// this tree element
		Object[] children = treeContentProvider.getElements(treeElement);
		for (int i = 0; i < children.length; ++i) {
			determineWhiteCheckedDescendents(children[i]);
		}

		// now determine the white-checked status for this tree element
		if (determineShouldBeWhiteChecked(treeElement)) {
			setWhiteChecked(treeElement, true);
		}
	}

	/**
	 * Cause the tree viewer to expand all its items
	 */
	public void expandAll() {
		treeViewer.expandAll();
	}

	/**
	 * Answer a flat collection of all of the checked elements in the list
	 * portion of self
	 *
	 * @return java.util.Vector
	 */
	@SuppressWarnings("unchecked")
	public Iterator getAllCheckedListItems() {
		List result = new ArrayList();
		Iterator listCollectionsEnum = checkedStateStore.values().iterator();

		while (listCollectionsEnum.hasNext()) {
			Iterator currentCollection = ((List) listCollectionsEnum.next())
					.iterator();
			while (currentCollection.hasNext()) {
				result.add(currentCollection.next());
			}
		}

		return result.iterator();
	}

	/**
	 * Returns a list of all of the items that are white checked. Any folders
	 * that are white checked are added and then any files from white checked
	 * folders are added.
	 *
	 * @return the list of all of the items that are white checked
	 */
	public Iterator getAllWhiteCheckedItems() {
		List result = new ArrayList();
		// Iterate through the children of the root as the root is not in the
		// store
		Object[] children = treeContentProvider.getChildren(root);
		for (int i = 0; i < children.length; ++i) {
			findAllWhiteCheckedItems(children[i], result);
		}
		return result.iterator();
	}

	/**
	 * Find all of the white checked children of the treeElement and add them to
	 * the collection. If the element itself is white select add it. If not then
	 * add any selected list elements and recurse down to the children.
	 * 
	 * @param treeElement
	 *            java.lang.Object
	 * @param result
	 *            java.util.Collection
	 */
	@SuppressWarnings("unchecked")
	private void findAllWhiteCheckedItems(Object treeElement,
			Collection result) {
		if (whiteCheckedTreeItems.contains(treeElement)) {
			result.add(treeElement);
		} else {
			Collection listChildren = (Collection) checkedStateStore
					.get(treeElement);
			// if it is not in the store then it and it's children are not
			// interesting
			if (listChildren == null) {
				return;
			}
			result.addAll(listChildren);
			Object[] children = treeContentProvider.getChildren(treeElement);
			for (int i = 0; i < children.length; ++i) {
				findAllWhiteCheckedItems(children[i], result);
			}
		}
	}

	/**
	 * Answer a collection of all of the checked elements in the tree portion of
	 * self
	 *
	 * @return java.util.Vector
	 */
	public Set getAllCheckedTreeItems() {
		return checkedStateStore.keySet();
	}

	/**
	 * Answer the number of elements that have been checked by the user.
	 *
	 * @return int
	 */
	public int getCheckedElementCount() {
		return checkedStateStore.size();
	}

	/**
	 * Return a count of the number of list items associated with a given tree
	 * item.
	 *
	 * @return int
	 * @param treeElement
	 *            java.lang.Object
	 */
	protected int getListItemsSize(Object treeElement) {
		Object[] elements = listContentProvider.getElements(treeElement);
		return elements.length;
	}

	/**
	 * Get the table the list viewer uses.
	 * 
	 * @return org.eclipse.swt.widgets.Table
	 */
	public Table getListTable() {
		return this.listViewer.getTable();
	}

	/**
	 * Logically gray-check all ancestors of treeItem by ensuring that they
	 * appear in the checked table
	 */
	@SuppressWarnings("unchecked")
	protected void grayCheckHierarchy(Object treeElement) {

		// if this tree element is already gray then its ancestors all are as
		// well
		if (checkedStateStore.containsKey(treeElement)) {
			return; // no need to proceed upwards from here
		}

		checkedStateStore.put(treeElement, new ArrayList());
		if (determineShouldBeWhiteChecked(treeElement)) {
			setWhiteChecked(treeElement, true);
		}
		Object parent = treeContentProvider.getParent(treeElement);
		if (parent != null) {
			grayCheckHierarchy(parent);
		}
	}

	/**
	 * Set the initial checked state of the passed list element to true.
	 *
	 * @param element
	 *            the element in the list to select
	 */
	public void initialCheckListItem(Object element) {
		Object parent = treeContentProvider.getParent(element);
		currentTreeSelection = parent;
		// As this is not done from the UI then set the box for updating from
		// the selection to false
		listItemChecked(element, true, false);
		updateHierarchy(parent);
	}

	/**
	 * Set the initial checked state of the passed element to true, as well as
	 * to all of its children and associated list elements
	 *
	 * @param element
	 *            the element in the tree to select
	 */
	public void initialCheckTreeItem(Object element) {
		treeItemChecked(element, true);
	}

	/**
	 * Initialize this group's viewers after they have been laid out.
	 */
	protected void initialize() {
		treeViewer.setInput(root);
	}

	/**
	 * Callback that's invoked when the checked status of an item in the list is
	 * changed by the user. Do not try and update the hierarchy if we are
	 * building the initial list.
	 */
	@SuppressWarnings("unchecked")
	protected void listItemChecked(Object listElement, boolean state,
			boolean updatingFromSelection) {
		List checkedListItems = (List) checkedStateStore
				.get(currentTreeSelection);

		if (state) {
			if (checkedListItems == null) {
				// since the associated tree item has gone from 0 -> 1 checked
				// list items, tree checking may need to be updated
				grayCheckHierarchy(currentTreeSelection);
				checkedListItems = (List) checkedStateStore
						.get(currentTreeSelection);
			}
			checkedListItems.add(listElement);
		} else {
			checkedListItems.remove(listElement);
			if (checkedListItems.isEmpty()) {
				// since the associated tree item has gone from 1 -> 0 checked
				// list items, tree checking may need to be updated
				ungrayCheckHierarchy(currentTreeSelection);
			}
		}

		if (updatingFromSelection) {
			updateHierarchy(currentTreeSelection);
		}
	}

	/**
	 * Notify all checked state listeners that the passed element has had its
	 * checked state changed to the passed state
	 */
	protected void notifyCheckStateChangeListeners(
			final CheckStateChangedEvent event) {
		Object[] array = getListeners();
		for (int i = 0; i < array.length; i++) {
			final ICheckStateListener l = (ICheckStateListener) array[i];
			SafeRunner.run(new SafeRunnable() {
				@Override
				public void run() {
					l.checkStateChanged(event);
				}
			});
		}
	}

	/**
	 * Set the contents of the list viewer based upon the specified selected
	 * tree element. This also includes checking the appropriate list items.
	 *
	 * @param treeElement
	 *            java.lang.Object
	 */
	protected void populateListViewer(final Object treeElement) {
		listViewer.setInput(treeElement);
		List listItemsToCheck = (List) checkedStateStore.get(treeElement);

		if (listItemsToCheck != null) {
			Iterator listItemsEnum = listItemsToCheck.iterator();
			while (listItemsEnum.hasNext()) {
				listViewer.setChecked(listItemsEnum.next(), true);
			}
		}
	}

	/**
	 * Remove the passed listener from self's collection of clients that listen
	 * for changes to element checked states
	 *
	 * @param listener
	 *            ICheckStateListener
	 */
	public void removeCheckStateListener(ICheckStateListener listener) {
		removeListenerObject(listener);
	}

	/**
	 * Handle the selection of an item in the tree viewer
	 *
	 * @param event
	 *            SelectionChangedEvent
	 */
	@Override
	public void selectionChanged(SelectionChangedEvent event) {
		IStructuredSelection selection = (IStructuredSelection) event
				.getSelection();
		Object selectedElement = selection.getFirstElement();
		if (selectedElement == null) {
			currentTreeSelection = null;
			listViewer.setInput(currentTreeSelection);
			return;
		}

		// ie.- if not an item deselection
		if (selectedElement != currentTreeSelection) {
			populateListViewer(selectedElement);
		}

		currentTreeSelection = selectedElement;
	}

	/**
	 * Select or deselect all of the elements in the tree depending on the value
	 * of the selection boolean. Be sure to update the displayed files as well.
	 *
	 * @param selection
	 *            boolean indicating whether or not to select all elements
	 */
	public void setAllSelections(final boolean selection) {

		// Potentially long operation - show a busy cursor
		BusyIndicator.showWhile(treeViewer.getControl().getDisplay(), () -> {
			setTreeChecked(root, selection);
			listViewer.setAllChecked(selection);
		});
	}

	/**
	 * Set the list viewer's providers to those passed
	 *
	 * @param contentProvider
	 *            ITreeContentProvider
	 * @param labelProvider
	 *            ILabelProvider
	 */
	public void setListProviders(IStructuredContentProvider contentProvider,
			ILabelProvider labelProvider) {
		listViewer.setContentProvider(contentProvider);
		listViewer.setLabelProvider(labelProvider);
	}

	/**
	 * Set the comparator that is to be applied to self's list viewer
	 *
	 * @param comparator
	 *            the comparator for the list viewer
	 */
	public void setListComparator(ViewerComparator comparator) {
		listViewer.setComparator(comparator);
	}

	/**
	 * Set the root of the widget to be new Root. Regenerate all of the tables
	 * and lists from this value.
	 * 
	 * @param newRoot
	 */
	public void setRoot(Object newRoot) {
		this.root = newRoot;
		initialize();
	}

	/**
	 * Set the checked state of the passed tree element appropriately, and do so
	 * recursively to all of its child tree elements as well
	 */
	@SuppressWarnings("unchecked")
	protected void setTreeChecked(Object treeElement, boolean state) {

		if (treeElement.equals(currentTreeSelection)) {
			listViewer.setAllChecked(state);
		}

		if (state) {
			Object[] listItems = listContentProvider.getElements(treeElement);
			List listItemsChecked = new ArrayList();
			for (int i = 0; i < listItems.length; ++i) {
				listItemsChecked.add(listItems[i]);
			}

			checkedStateStore.put(treeElement, listItemsChecked);
		} else {
			checkedStateStore.remove(treeElement);
		}

		setWhiteChecked(treeElement, state);
		treeViewer.setChecked(treeElement, state);
		treeViewer.setGrayed(treeElement, false);

		// now logically check/uncheck all children as well
		Object[] children = treeContentProvider.getChildren(treeElement);
		for (int i = 0; i < children.length; ++i) {
			setTreeChecked(children[i], state);
		}
	}

	/**
	 * Set the tree viewer's providers to those passed
	 *
	 * @param contentProvider
	 *            ITreeContentProvider
	 * @param labelProvider
	 *            ILabelProvider
	 */
	public void setTreeProviders(ITreeContentProvider contentProvider,
			ILabelProvider labelProvider) {
		treeViewer.setContentProvider(contentProvider);
		treeViewer.setLabelProvider(labelProvider);
	}

	/**
	 * Set the comparator that is to be applied to self's tree viewer
	 *
	 * @param comparator
	 *            the comparator for the tree
	 */
	public void setTreeComparator(ViewerComparator comparator) {
		treeViewer.setComparator(comparator);
	}

	/**
	 * Adjust the collection of references to white-checked tree elements
	 * appropriately.
	 *
	 * @param treeElement
	 *            java.lang.Object
	 * @param isWhiteChecked
	 *            boolean
	 */
	@SuppressWarnings("unchecked")
	protected void setWhiteChecked(Object treeElement, boolean isWhiteChecked) {
		if (isWhiteChecked) {
			if (!whiteCheckedTreeItems.contains(treeElement)) {
				whiteCheckedTreeItems.add(treeElement);
			}
		} else {
			whiteCheckedTreeItems.remove(treeElement);
		}
	}

	/**
	 * Handle the collapsing of an element in a tree viewer
	 */
	@Override
	public void treeCollapsed(TreeExpansionEvent event) {
		// We don't need to do anything with this
	}

	/**
	 * Handle the expansionsion of an element in a tree viewer
	 */
	@SuppressWarnings("unchecked")
	@Override
	public void treeExpanded(TreeExpansionEvent event) {

		Object item = event.getElement();

		// First see if the children need to be given their checked state at
		// all. If they've
		// already been realized then this won't be necessary
		if (!expandedTreeNodes.contains(item)) {
			expandedTreeNodes.add(item);
			Object[] children = treeContentProvider.getChildren(item);
			for (int i = 0; i < children.length; ++i) {
				dynamicTreeContentProvider.getElements(children[i]);
			}
			checkNewTreeElements(dynamicTreeContentProvider.getChildren(item));
		}
	}

	/**
	 * Callback that's invoked when the checked status of an item in the tree is
	 * changed by the user.
	 */
	protected void treeItemChecked(Object treeElement, boolean state) {

		// recursively adjust all child tree elements appropriately
		setTreeChecked(treeElement, state);

		Object parent = treeContentProvider.getParent(treeElement);
		if (parent == null) {
			return;
		}

		// now update upwards in the tree hierarchy
		if (state) {
			grayCheckHierarchy(parent);
		} else {
			ungrayCheckHierarchy(parent);
		}

		updateHierarchy(treeElement);
	}

	/**
	 * Logically un-gray-check all ancestors of treeItem iff appropriate.
	 */
	protected void ungrayCheckHierarchy(Object treeElement) {
		if (!determineShouldBeAtLeastGrayChecked(treeElement)) {
			checkedStateStore.remove(treeElement);
		}

		Object parent = treeContentProvider.getParent(treeElement);
		if (parent != null) {
			ungrayCheckHierarchy(parent);
		}
	}

	/**
	 * Set the checked state of self and all ancestors appropriately
	 */
	protected void updateHierarchy(Object treeElement) {

		boolean whiteChecked = determineShouldBeWhiteChecked(treeElement);
		boolean shouldBeAtLeastGray = determineShouldBeAtLeastGrayChecked(
				treeElement);

		treeViewer.setChecked(treeElement, shouldBeAtLeastGray);
		setWhiteChecked(treeElement, whiteChecked);
		if (!whiteChecked) {
			treeViewer.setGrayed(treeElement, shouldBeAtLeastGray);
		}

		// proceed up the tree element hierarchy
		Object parent = treeContentProvider.getParent(treeElement);
		if (parent != null) {
			updateHierarchy(parent);
		}
	}

	/**
	 * Update the selections of the tree elements in items to reflect the new
	 * selections provided.
	 * 
	 * @param items
	 *            Map with keys of Object (the tree element) and values of List
	 *            (the selected list elements).
	 */
	@SuppressWarnings("unchecked")
	public void updateSelections(final Map items) {

		// Potentially long operation - show a busy cursor
		BusyIndicator.showWhile(treeViewer.getControl().getDisplay(), () -> {
			// Update the store before the hierarchy to prevent updating parents
			// before all of the children are done
			for (Entry<?, List> entry : ((Map<Object, List>) items)
					.entrySet()) {
				Object key1 = entry.getKey();
				// Replace the items in the checked state store with those from
				// the supplied items
				List selections = entry.getValue();
				if (selections.size() == 0) {
					// If it is empty remove it from the list
					checkedStateStore.remove(key1);
				} else {
					checkedStateStore.put(key1, selections);
					// proceed up the tree element hierarchy
					Object parent = treeContentProvider.getParent(key1);
					if (parent != null) {
						addToHierarchyToCheckedStore(parent);
					}
				}
			}

			// Now update hierarchies
			for (Entry<Object, List> entry : ((Map<Object, List>) items)
					.entrySet()) {
				Object key2 = entry.getKey();
				updateHierarchy(key2);
				if (currentTreeSelection != null
						&& currentTreeSelection.equals(key2)) {
					listViewer.setAllChecked(false);
					listViewer.setCheckedElements(entry.getValue().toArray());
				}
			}
		});

	}
}

