/*******************************************************************************
 * Copyright (c) 2011, 2013 SAP AG 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:
 *    Mathias Kinzler (SAP AG) - initial implementation
 *    Robin Stocker <robin@nibor.org> - ignore linked resources
 *******************************************************************************/
package org.eclipse.egit.ui.internal.dialogs;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.compare.ITypedElement;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.PlatformObject;
import org.eclipse.egit.core.AdaptableFileTreeIterator;
import org.eclipse.egit.core.internal.CompareCoreUtils;
import org.eclipse.egit.core.internal.storage.GitFileRevision;
import org.eclipse.egit.core.internal.util.ResourceUtil;
import org.eclipse.egit.core.project.RepositoryMapping;
import org.eclipse.egit.ui.Activator;
import org.eclipse.egit.ui.UIPreferences;
import org.eclipse.egit.ui.UIUtils;
import org.eclipse.egit.ui.internal.CompareUtils;
import org.eclipse.egit.ui.internal.EgitUiEditorUtils;
import org.eclipse.egit.ui.internal.FileEditableRevision;
import org.eclipse.egit.ui.internal.FileRevisionTypedElement;
import org.eclipse.egit.ui.internal.GitCompareFileRevisionEditorInput;
import org.eclipse.egit.ui.internal.LocalFileRevision;
import org.eclipse.egit.ui.internal.UIIcons;
import org.eclipse.egit.ui.internal.UIText;
import org.eclipse.egit.ui.internal.actions.BooleanPrefAction;
import org.eclipse.egit.ui.internal.dialogs.CompareTreeView.PathNode.Type;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.action.IMenuListener;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.dialogs.ProgressMonitorDialog;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.preference.IPersistentPreferenceStore;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.viewers.ArrayContentProvider;
import org.eclipse.jface.viewers.BaseLabelProvider;
import org.eclipse.jface.viewers.ILabelProvider;
import org.eclipse.jface.viewers.IOpenListener;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.ITreeSelection;
import org.eclipse.jface.viewers.OpenEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.ViewerComparator;
import org.eclipse.jgit.dircache.DirCacheIterator;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.AbstractTreeIterator;
import org.eclipse.jgit.treewalk.CanonicalTreeParser;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
import org.eclipse.jgit.treewalk.filter.NotIgnoredFilter;
import org.eclipse.jgit.treewalk.filter.OrTreeFilter;
import org.eclipse.jgit.treewalk.filter.PathFilter;
import org.eclipse.jgit.treewalk.filter.TreeFilter;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.ui.ISharedImages;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.actions.ActionFactory.IWorkbenchAction;
import org.eclipse.ui.model.IWorkbenchAdapter;
import org.eclipse.ui.model.WorkbenchContentProvider;
import org.eclipse.ui.model.WorkbenchLabelProvider;
import org.eclipse.ui.navigator.ICommonMenuConstants;
import org.eclipse.ui.part.IShowInSource;
import org.eclipse.ui.part.ShowInContext;
import org.eclipse.ui.part.ViewPart;

/**
 * Shows a tree when opening compare on an {@link IContainer}, or a
 * {@link Repository}
 * <p>
 * If the input is an {@link IContainer}, the tree is similar to the tree shown
 * by the PackageExplorer (based on {@link WorkbenchLabelProvider} and
 * {@link WorkbenchContentProvider}, otherwise a simple tree representing files
 * and folders is used based on {@link PathNode} instances. Linked resources
 * however are ignored and not listed as content.
 * <p>
 * The tree nodes are shown with icons for "Added", "Deleted", and
 * "Same Contents" for files. Files with same content can be hidden using a
 * filter button.
 * <p>
 * This view can also show files and folders outside the Eclipse workspace when
 * a {@link Repository} is used as input.
 * <p>
 */
public class CompareTreeView extends ViewPart implements IMenuListener, IShowInSource {
	/** The "magic" compare version to compare with the index */
	public static final String INDEX_VERSION = "%%%INDEX%%%"; //$NON-NLS-1$

	/** The View ID */
	public static final String ID = "org.eclipse.egit.ui.CompareTreeView"; //$NON-NLS-1$

	private static final Image FILE_IMAGE = PlatformUI.getWorkbench()
			.getSharedImages().getImage(ISharedImages.IMG_OBJ_FILE);

	private static final Image FOLDER_IMAGE = PlatformUI.getWorkbench()
			.getSharedImages().getImage(ISharedImages.IMG_OBJ_FOLDER);

	private final Image SAME_CONTENT = new Image(FILE_IMAGE.getDevice(),
			FILE_IMAGE, SWT.IMAGE_DISABLE);

	private Image ADDED = UIIcons.ELCL16_ADD.createImage();

	private Image DELETED = UIIcons.ELCL16_DELETE.createImage();

	private RepositoryMapping repositoryMapping;

	private TreeViewer tree;

	private IWorkbenchAction showEqualsAction;

	private Map<IPath, GitFileRevision> compareVersionMap = new HashMap<IPath, GitFileRevision>();

	private Map<IPath, GitFileRevision> baseVersionMap = new HashMap<IPath, GitFileRevision>();

	private Set<IPath> addedPaths = new HashSet<IPath>();

	private Set<IPath> equalContentPaths = new HashSet<IPath>();

	private Set<IPath> baseVersionPathsWithChildren = new HashSet<IPath>();

	private Map<IPath, List<PathNodeAdapter>> compareVersionPathsWithChildren = new HashMap<IPath, List<PathNodeAdapter>>();

	private Set<IPath> deletedPaths = new HashSet<IPath>();

	private List<IWorkbenchAction> actionsToDispose = new ArrayList<IWorkbenchAction>();

	private Object input;

	private String compareVersion;

	private String baseVersion;

	private boolean showEquals = false;

	@Override
	public void createPartControl(Composite parent) {
		Composite main = new Composite(parent, SWT.NONE);
		GridLayoutFactory.fillDefaults().spacing(0, 0).applyTo(main);
		GridDataFactory.fillDefaults().grab(true, true).applyTo(main);

		tree = new TreeViewer(main, SWT.MULTI);
		tree.setContentProvider(new PathNodeContentProvider());
		GridDataFactory.fillDefaults().grab(true, true).applyTo(tree.getTree());

		tree.addOpenListener(new IOpenListener() {
			public void open(OpenEvent event) {
				reactOnOpen(event);
			}
		});
		tree.getTree().setEnabled(false);
		createActions();

		getViewSite().setSelectionProvider(tree);
		createContextMenu();
	}

	private void createActions() {
		IWorkbenchAction reuseCompareEditorAction = new CompareUtils.ReuseCompareEditorAction();
		actionsToDispose.add(reuseCompareEditorAction);
		getViewSite().getActionBars().getMenuManager().add(
				reuseCompareEditorAction);

		showEqualsAction = new BooleanPrefAction(
				(IPersistentPreferenceStore) Activator.getDefault()
						.getPreferenceStore(),
				UIPreferences.TREE_COMPARE_SHOW_EQUALS,
				UIText.CompareTreeView_EqualFilesTooltip) {
			@Override
			public void apply(boolean value) {
				buildTrees(false);
			}
		};
		showEqualsAction.setImageDescriptor(UIIcons.ELCL16_FILTER);
		showEqualsAction.setEnabled(false);
		actionsToDispose.add(showEqualsAction);
		getViewSite().getActionBars().getToolBarManager().add(showEqualsAction);

		IAction expandAllAction = new Action(
				UIText.CompareTreeView_ExpandAllTooltip) {
			@Override
			public void run() {
				tree.expandAll();
			}
		};
		expandAllAction.setImageDescriptor(UIIcons.EXPAND_ALL);
		getViewSite().getActionBars().getToolBarManager().add(expandAllAction);

		IAction collapseAllAction = new Action(
				UIText.CompareTreeView_CollapseAllTooltip) {
			@Override
			public void run() {
				tree.collapseAll();
			}
		};
		collapseAllAction.setImageDescriptor(UIIcons.COLLAPSEALL);
		getViewSite().getActionBars().getToolBarManager().add(collapseAllAction);
	}

	private void reactOnOpen(OpenEvent event) {
		Object selected = ((IStructuredSelection) event.getSelection())
				.getFirstElement();
		ITypedElement left;
		ITypedElement right;
		if (selected instanceof IContainer) {
			// open/close folder
			TreeViewer tv = (TreeViewer) event.getViewer();
			tv.setExpandedState(selected, !tv.getExpandedState(selected));
			return;
		} else if (selected instanceof IFile) {
			final IFile res = (IFile) selected;
			LocalFileRevision revision = new LocalFileRevision(res);
			left = new FileEditableRevision(revision, res, PlatformUI.getWorkbench().getProgressService());
			GitFileRevision rightRevision = compareVersionMap.get(new Path(
					repositoryMapping.getRepoRelativePath(res)));
			if (rightRevision == null) {
				right = new GitCompareFileRevisionEditorInput.EmptyTypedElement(
						NLS
								.bind(
										UIText.CompareTreeView_ItemNotFoundInVersionMessage,
										res.getName(), getCompareVersion()));
			} else {
				String encoding = CompareCoreUtils.getResourceEncoding(res);
				right = new FileRevisionTypedElement(rightRevision, encoding);
			}
		} else if (selected instanceof GitFileRevision) {
			GitFileRevision rightRevision = (GitFileRevision) selected;
			left = new GitCompareFileRevisionEditorInput.EmptyTypedElement(NLS
					.bind(UIText.CompareTreeView_ItemNotFoundInVersionMessage,
							rightRevision.getName(), getBaseVersion()));
			right = new FileRevisionTypedElement(rightRevision, null);
		} else if (selected instanceof PathNode) {
			PathNode node = (PathNode) selected;
			String encoding = CompareCoreUtils.getResourceEncoding(
					getRepository(), node.getRepoRelativePath());
			switch (node.type) {
			case FILE_BOTH_SIDES_DIFFER:
				// fall through
			case FILE_BOTH_SIDES_SAME: {
				// open a compare editor with both sides filled
				GitFileRevision rightRevision = compareVersionMap
						.get(node.path);
				right = new FileRevisionTypedElement(rightRevision, encoding);
				GitFileRevision leftRevision = baseVersionMap.get(node.path);
				left = new FileRevisionTypedElement(leftRevision, encoding);
				break;
			}
			case FILE_DELETED: {
				// open compare editor with left side empty
				GitFileRevision rightRevision = compareVersionMap
						.get(node.path);
				right = new FileRevisionTypedElement(rightRevision, encoding);
				left = new GitCompareFileRevisionEditorInput.EmptyTypedElement(
						NLS
								.bind(
										UIText.CompareTreeView_ItemNotFoundInVersionMessage,
										rightRevision.getName(),
										getBaseVersion()));
				break;
			}
			case FILE_ADDED: {
				// open compare editor with right side empty
				GitFileRevision leftRevision = baseVersionMap.get(node.path);
				left = new FileRevisionTypedElement(leftRevision, encoding);
				right = new GitCompareFileRevisionEditorInput.EmptyTypedElement(
						NLS
								.bind(
										UIText.CompareTreeView_ItemNotFoundInVersionMessage,
										leftRevision.getName(),
										getCompareVersion()));
				break;
			}
			case FOLDER:
				// open/close folder
				TreeViewer tv = (TreeViewer) event.getViewer();
				tv.setExpandedState(selected, !tv.getExpandedState(selected));
				return;
			default:
				return;
			}

		} else if (selected instanceof PathNodeAdapter) {
			// deleted in workspace
			PathNodeAdapter node = (PathNodeAdapter) selected;
			GitFileRevision rightRevision = compareVersionMap
					.get(node.pathNode.path);
			String encoding = CompareCoreUtils.getResourceEncoding(
					getRepository(), node.pathNode.getRepoRelativePath());
			right = new FileRevisionTypedElement(rightRevision, encoding);
			left = new GitCompareFileRevisionEditorInput.EmptyTypedElement(NLS
					.bind(UIText.CompareTreeView_ItemNotFoundInVersionMessage,
							node.pathNode.path.lastSegment(), getBaseVersion()));
		} else
			return;

		GitCompareFileRevisionEditorInput compareInput = new GitCompareFileRevisionEditorInput(
				left, right, PlatformUI.getWorkbench()
						.getActiveWorkbenchWindow().getActivePage());
		CompareUtils.openInCompare(PlatformUI.getWorkbench()
				.getActiveWorkbenchWindow().getActivePage(), compareInput);
	}

	private String getBaseVersion() {
		// null in case of Workspace compare
		if (baseVersion == null)
			return UIText.CompareTreeView_WorkspaceVersionText;
		return baseVersion;
	}

	private String getCompareVersion() {
		return compareVersion;
	}

	@Override
	public void setFocus() {
		tree.getTree().setFocus();
	}

	/**
	 * Used to compare the working tree with another version
	 *
	 * @param input
	 *            the {@link IResource}s from which to build the tree
	 * @param compareVersion
	 *            a {@link Ref} name or {@link RevCommit} id or
	 *            {@link #INDEX_VERSION}
	 */
	public void setInput(final IResource[] input, String compareVersion) {
		setResourceInput(input);
		this.baseVersion = null;
		this.compareVersion = compareVersion;
		buildTrees(true);
		updateControls();
	}

	private void setResourceInput(final IResource[] input) {
		if (input.length > 0) {
			// we must make sure to only show the topmost resources as roots
			List<IResource> resources = new ArrayList<IResource>(input.length);
			List<IPath> allPaths = new ArrayList<IPath>(input.length);
			for (IResource originalInput : input) {
				allPaths.add(originalInput.getFullPath());
			}
			for (IResource originalInput : input) {
				boolean skip = false;
				for (IPath path : allPaths) {
					if (path.isPrefixOf(originalInput.getFullPath())
							&& path.segmentCount() < originalInput
									.getFullPath().segmentCount()) {
						skip = true;
						break;
					}
				}
				if (!skip)
					resources.add(originalInput);
			}
			this.input = resources.toArray(new IResource[resources.size()]);
		} else
			this.input = input;
	}

	/**
	 * Used to compare two versions with each other filtering by a workspace
	 * resource
	 *
	 * @param input
	 *            the {@link IResource}s from which to build the tree
	 * @param baseVersion
	 *            a {@link Ref} name or {@link RevCommit} id
	 * @param compareVersion
	 *            a {@link Ref} name or {@link RevCommit} id or
	 *            {@link #INDEX_VERSION}
	 */
	public void setInput(final IResource[] input, String baseVersion,
			String compareVersion) {
		setResourceInput(input);
		this.baseVersion = baseVersion;
		this.compareVersion = compareVersion;
		buildTrees(true);
		updateControls();
	}

	/**
	 * Used if no workspace filtering should be applied
	 *
	 * @param input
	 *            the {@link Repository} from which to build the tree
	 * @param baseVersion
	 *            a {@link Ref} name or {@link RevCommit} id
	 * @param compareVersion
	 *            a {@link Ref} name or {@link RevCommit} id
	 */
	public void setInput(final Repository input, String baseVersion,
			String compareVersion) {
		this.input = input;
		this.baseVersion = baseVersion;
		this.compareVersion = compareVersion;
		buildTrees(true);
		updateControls();
	}

	private void updateControls() {
		for (IWorkbenchAction action : actionsToDispose)
			action.setEnabled(input != null);
		tree.getTree().setEnabled(input != null);
		if (input == null)
			setContentDescription(UIText.CompareTreeView_NoInputText);
		else {
			String name;
			if (input instanceof IResource[]) {
				IResource[] resources = (IResource[]) input;
				if (resources.length == 1)
					name = (resources[0]).getFullPath().makeRelative()
							.toString();
				else
					name = UIText.CompareTreeView_MultipleResourcesHeaderText;
			} else if (input instanceof Repository)
				name = Activator.getDefault().getRepositoryUtil()
						.getRepositoryName(((Repository) input));
			else
				throw new IllegalStateException();
			if (baseVersion == null)
				setContentDescription(NLS
						.bind(
								UIText.CompareTreeView_ComparingWorkspaceVersionDescription,
								name,
								compareVersion.equals(INDEX_VERSION) ? UIText.CompareTreeView_IndexVersionText
										: compareVersion));
			else
				setContentDescription(NLS
						.bind(
								UIText.CompareTreeView_ComparingTwoVersionDescription,
								new String[] {
										baseVersion,
										name,
										compareVersion.equals(INDEX_VERSION) ? UIText.CompareTreeView_IndexVersionText
												: compareVersion }));
		}
	}

	private void buildTrees(final boolean buildMaps) {
		final Object[] wsExpaneded = tree.getExpandedElements();
		final ISelection wsSel = tree.getSelection();

		tree.setInput(null);

		if (baseVersion == null) {
			tree.setContentProvider(new WorkbenchTreeContentProvider());
			tree.setComparator(new WorkbenchTreeComparator());
			tree.setLabelProvider(new WorkbenchTreeLabelProvider());
		} else {
			tree.setContentProvider(new PathNodeContentProvider());
			tree.setComparator(new PathNodeTreeComparator());
			tree.setLabelProvider(new PathNodeLabelProvider());
		}
		for (IWorkbenchAction action : actionsToDispose)
			action.setEnabled(false);

		showEquals = Activator.getDefault().getPreferenceStore().getBoolean(
				UIPreferences.TREE_COMPARE_SHOW_EQUALS);
		final Repository repo;
		if (input instanceof IResource[]) {
			repositoryMapping = RepositoryMapping
					.getMapping(((IResource[]) input)[0]);
			if (repositoryMapping == null
					|| repositoryMapping.getRepository() == null)
				return;
			repo = repositoryMapping.getRepository();
		} else if (input instanceof Repository) {
			repo = (Repository) input;
		} else
			return;
		final RevCommit baseCommit;
		final RevCommit compareCommit;
		RevWalk rw = new RevWalk(repo);
		try {
			ObjectId commitId = repo.resolve(compareVersion);
			compareCommit = commitId != null ? rw.parseCommit(commitId) : null;
			if (baseVersion == null)
				baseCommit = null;
			else {
				commitId = repo.resolve(baseVersion);
				baseCommit = rw.parseCommit(commitId);
			}
		} catch (IOException e) {
			Activator.handleError(e.getMessage(), e, true);
			return;
		} finally {
			rw.release();
		}
		showBusy(true);
		try {
			// this does the hard work...
			new ProgressMonitorDialog(getViewSite().getShell()).run(true, true,
					new IRunnableWithProgress() {
						public void run(IProgressMonitor monitor)
								throws InvocationTargetException,
								InterruptedException {
							try {
								if (buildMaps)
									buildMaps(repo, baseCommit, compareCommit,
											monitor);
								PlatformUI.getWorkbench().getDisplay()
										.asyncExec(new Runnable() {
											public void run() {
												tree.setInput(input);
												tree
														.setExpandedElements(wsExpaneded);
												tree.setSelection(wsSel);
												updateControls();
											}
										});
							} catch (IOException e) {
								throw new InvocationTargetException(e);
							}
						}
					});
		} catch (InvocationTargetException e) {
			Activator.handleError(e.getTargetException().getMessage(), e
					.getTargetException(), true);
		} catch (InterruptedException e) {
			input = null;
		} finally {
			showBusy(false);
		}
	}

	private void buildMaps(Repository repository, RevCommit baseCommit,
			RevCommit compareCommit, IProgressMonitor monitor)
			throws InterruptedException, IOException {
		monitor.beginTask(UIText.CompareTreeView_AnalyzingRepositoryTaskText,
				IProgressMonitor.UNKNOWN);
		boolean useIndex = compareVersion.equals(INDEX_VERSION);
		deletedPaths.clear();
		equalContentPaths.clear();
		baseVersionMap.clear();
		compareVersionMap.clear();
		compareVersionPathsWithChildren.clear();
		addedPaths.clear();
		baseVersionPathsWithChildren.clear();
		boolean checkIgnored = false;
		TreeWalk tw = new TreeWalk(repository);
		try {
			int baseTreeIndex;
			if (baseCommit == null) {
				checkIgnored = true;
				baseTreeIndex = tw.addTree(new AdaptableFileTreeIterator(
						repository, ResourcesPlugin.getWorkspace().getRoot()));
			} else
				baseTreeIndex = tw.addTree(new CanonicalTreeParser(null,
						repository.newObjectReader(), baseCommit.getTree()));
			int compareTreeIndex;
			if (!useIndex)
				compareTreeIndex = tw.addTree(new CanonicalTreeParser(null,
						repository.newObjectReader(), compareCommit.getTree()));
			else
				compareTreeIndex = tw.addTree(new DirCacheIterator(repository
						.readDirCache()));

			if (input instanceof IResource[]) {
				IResource[] resources = (IResource[]) input;
				List<TreeFilter> orFilters = new ArrayList<TreeFilter>(
						resources.length);

				for (IResource resource : resources) {
					String relPath = repositoryMapping
							.getRepoRelativePath(resource);
					if (relPath.length() > 0)
						orFilters.add(PathFilter.create(relPath));
				}
				if (checkIgnored) {
					if (orFilters.size() > 1) {
						TreeFilter andFilter = AndTreeFilter.create(new NotIgnoredFilter(baseTreeIndex),
								OrTreeFilter.create(orFilters));
						tw.setFilter(andFilter);
					} else if (orFilters.size() == 1) {
						TreeFilter andFilter = AndTreeFilter.create(new NotIgnoredFilter(baseTreeIndex),
								orFilters.get(0));
						tw.setFilter(andFilter);
					} else
						tw.setFilter(new NotIgnoredFilter(baseTreeIndex));

				} else if (orFilters.size() > 1)
					tw.setFilter(OrTreeFilter.create(orFilters));
				else if (orFilters.size() == 1)
					tw.setFilter(orFilters.get(0));
			}

			tw.setRecursive(true);

			if (monitor.isCanceled())
				throw new InterruptedException();
			while (tw.next()) {
				if (monitor.isCanceled())
					throw new InterruptedException();
				AbstractTreeIterator compareVersionIterator = tw.getTree(
						compareTreeIndex, AbstractTreeIterator.class);
				AbstractTreeIterator baseVersionIterator = tw.getTree(
						baseTreeIndex, AbstractTreeIterator.class);
				if (compareVersionIterator != null
						&& baseVersionIterator != null) {
					monitor.setTaskName(baseVersionIterator
							.getEntryPathString());
					IPath currentPath = new Path(baseVersionIterator
							.getEntryPathString());
					if (!useIndex)
						compareVersionMap
								.put(currentPath, GitFileRevision.inCommit(
										repository, compareCommit,
										baseVersionIterator
												.getEntryPathString(), tw
												.getObjectId(compareTreeIndex)));
					else
						compareVersionMap.put(currentPath, GitFileRevision
								.inIndex(repository, baseVersionIterator
										.getEntryPathString()));
					if (baseCommit != null)
						baseVersionMap.put(currentPath, GitFileRevision
								.inCommit(repository, baseCommit,
										baseVersionIterator
												.getEntryPathString(), tw
												.getObjectId(baseTreeIndex)));
					boolean equalContent = compareVersionIterator
							.getEntryObjectId().equals(
									baseVersionIterator.getEntryObjectId());
					if (equalContent)
						equalContentPaths.add(currentPath);

					if (equalContent && !showEquals)
						continue;

					while (currentPath.segmentCount() > 0) {
						currentPath = currentPath.removeLastSegments(1);
						if (!baseVersionPathsWithChildren.add(currentPath))
							break;
					}

				} else if (baseVersionIterator != null
						&& compareVersionIterator == null) {
					monitor.setTaskName(baseVersionIterator
							.getEntryPathString());
					// only on base side
					IPath currentPath = new Path(baseVersionIterator
							.getEntryPathString());
					addedPaths.add(currentPath);
					if (baseCommit != null)
						baseVersionMap.put(currentPath, GitFileRevision
								.inCommit(repository, baseCommit,
										baseVersionIterator
												.getEntryPathString(), tw
												.getObjectId(baseTreeIndex)));
					while (currentPath.segmentCount() > 0) {
						currentPath = currentPath.removeLastSegments(1);
						if (!baseVersionPathsWithChildren.add(currentPath))
							break;
					}

				} else if (compareVersionIterator != null
						&& baseVersionIterator == null) {
					monitor.setTaskName(compareVersionIterator
							.getEntryPathString());
					// only on compare side
					IPath currentPath = new Path(compareVersionIterator
							.getEntryPathString());
					deletedPaths.add(currentPath);
					List<PathNodeAdapter> children = compareVersionPathsWithChildren
							.get(currentPath.removeLastSegments(1));
					if (children == null) {
						children = new ArrayList<PathNodeAdapter>(1);
						compareVersionPathsWithChildren.put(currentPath
								.removeLastSegments(1), children);
					}
					children.add(new PathNodeAdapter(new PathNode(currentPath,
							Type.FILE_DELETED)));

					if (!useIndex)
						compareVersionMap
								.put(currentPath, GitFileRevision.inCommit(
										repository, compareCommit,
										compareVersionIterator
												.getEntryPathString(), tw
												.getObjectId(compareTreeIndex)));
					else
						compareVersionMap.put(currentPath, GitFileRevision
								.inIndex(repository, compareVersionIterator
										.getEntryPathString()));
				}
			}
		} finally {
			tw.release();
			monitor.done();
		}
	}

	@Override
	public void dispose() {
		super.dispose();
		for (IWorkbenchAction action : actionsToDispose)
			action.dispose();
		ADDED.dispose();
		DELETED.dispose();
		SAME_CONTENT.dispose();
	}

	final static class PathNode {
		/** Type; note that the ordinal is used to sort the tree */
		public enum Type {
			/** Folder */
			FOLDER,
			/** File added (only on base side) */
			FILE_ADDED,
			/** File deleted (only on compare side) */
			FILE_DELETED,
			/** File differs on both sides */
			FILE_BOTH_SIDES_DIFFER,
			/** File same on both sides */
			FILE_BOTH_SIDES_SAME
		}

		public final IPath path;

		public final Type type;

		/**
		 * @param path
		 * @param type
		 */
		public PathNode(IPath path, Type type) {
			this.path = path;
			this.type = type;
		}

		public String getRepoRelativePath() {
			return path.toString();
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + path.hashCode();
			result = prime * result + type.hashCode();
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			PathNode other = (PathNode) obj;
			if (!path.equals(other.path))
				return false;
			if (!type.equals(other.type))
				return false;
			return true;
		}

		@Override
		public String toString() {
			return type.name() + ": " + path.toString(); //$NON-NLS-1$
		}
	}

	private final class WorkbenchTreeLabelProvider extends
			WorkbenchLabelProvider {
		@Override
		protected ImageDescriptor decorateImage(ImageDescriptor baseImage,
				Object element) {
			if (!(element instanceof IFile)) {
				return super.decorateImage(baseImage, element);
			}
			IPath elementPath = new Path(repositoryMapping
					.getRepoRelativePath((IFile) element));
			// decorate with + for files not found in the repository and = for
			// "same" files
			if (addedPaths.contains(elementPath)) {
				return UIIcons.ELCL16_ADD;
			}
			if (deletedPaths.contains(elementPath)) {
				return UIIcons.ELCL16_DELETE;
			}
			if (equalContentPaths.contains(elementPath)) {
				ImageDescriptor imageDesc = super.decorateImage(baseImage,
						element);
				return ImageDescriptor.createWithFlags(imageDesc,
						SWT.IMAGE_DISABLE);
			}
			return super.decorateImage(baseImage, element);
		}
	}

	/**
	 * Sorts the workbench tree by type and state (Folder, Added, Deleted,
	 * Changed, Unchanged Files)
	 */
	private final class WorkbenchTreeComparator extends ViewerComparator {
		private static final int FOLDERCATEGORY = 5;

		private static final int ADDEDCATEGORY = 10;

		private static final int DELETEDCATEGORY = 15;

		private static final int CHANGEDCATEGORY = 20;

		private static final int UNCHANGEDCATEGORY = 30;

		private static final int UNKNOWNCATEGORY = 50;

		@Override
		public int category(Object element) {
			IResource adapter = (IResource) getAdapter(element, IResource.class);
			if (adapter != null) {
				if (adapter instanceof IContainer) {
					return FOLDERCATEGORY;
				}
				if (adapter instanceof IFile) {
					IFile file = (IFile) adapter;
					IPath path = new Path(repositoryMapping
							.getRepoRelativePath(file));
					if (addedPaths.contains(path))
						return ADDEDCATEGORY;
					if (equalContentPaths.contains(path))
						return UNCHANGEDCATEGORY;
					return CHANGEDCATEGORY;
				}
			}
			if (element instanceof PathNodeAdapter)
				return DELETEDCATEGORY;

			return UNKNOWNCATEGORY;
		}

		private Object getAdapter(Object sourceObject, Class adapterType) {
			Assert.isNotNull(adapterType);
			if (sourceObject == null)
				return null;

			if (adapterType.isInstance(sourceObject))
				return sourceObject;

			if (sourceObject instanceof IAdaptable) {
				IAdaptable adaptable = (IAdaptable) sourceObject;

				Object result = adaptable.getAdapter(adapterType);
				if (result != null) {
					// Sanity-check
					Assert.isTrue(adapterType.isInstance(result));
					return result;
				}
			}

			if (!(sourceObject instanceof PlatformObject)) {
				Object result = Platform.getAdapterManager().getAdapter(
						sourceObject, adapterType);
				if (result != null)
					return result;
			}
			return null;
		}
	}

	/**
	 * Sorts the workbench tree by type and state (Folder, Added, Deleted,
	 * Changed, Unchanged Files)
	 */
	private final static class PathNodeTreeComparator extends ViewerComparator {
		private static final int UNKNOWNCATEGORY = 50;

		@Override
		public int category(Object element) {
			if (element instanceof PathNode) {
				return ((PathNode) element).type.ordinal();
			}
			return UNKNOWNCATEGORY;
		}
	}

	/**
	 * Used to render the "local" (workspace) side of a tree compare where one
	 * side is the workspace
	 */
	private final class WorkbenchTreeContentProvider extends
			WorkbenchContentProvider {
		@Override
		public Object[] getChildren(Object element) {
			boolean rebuildArray = false;
			Object[] children;
			if (element == input)
				children = (Object[]) input;
			else
				children = super.getChildren(element);
			List<Object> childList = new ArrayList<Object>(children.length);
			for (Object child : children) {
				IResource childResource = (IResource) child;
				if (childResource.isLinked())
					continue;
				IPath path = new Path(
						repositoryMapping.getRepoRelativePath(childResource));
				boolean isFile = childResource.getType() == IResource.FILE;

				// each path that is not ignored creates an entry in either
				// compareVersionMap or addedPaths, so we can check if a path
				// was ignored by looking into these tables
				if (isFile && !compareVersionMap.containsKey(path)
						&& !addedPaths.contains(path)) {
					rebuildArray = true;
					continue;
				}
				if (!showEquals && equalContentPaths.contains(path)) {
					rebuildArray = true;
					continue;
				}
				if (child instanceof IContainer
						&& !baseVersionPathsWithChildren.contains(path)) {
					rebuildArray = true;
					continue;
				}
				if (!showEquals && equalContentPaths.contains(path)) {
					rebuildArray = true;
					continue;
				}
				childList.add(child);
			}
			if (element instanceof IContainer) {
				List<PathNodeAdapter> deletedChildren = compareVersionPathsWithChildren
						.get(new Path(repositoryMapping
								.getRepoRelativePath((IResource) element)));
				if (deletedChildren != null) {
					rebuildArray = true;
					for (IWorkbenchAdapter path : deletedChildren)
						childList.add(path);
				}
			}
			if (rebuildArray)
				return childList.toArray();
			return children;
		}
	}

	/**
	 * Used to render the tree in case we have no workspace
	 */
	private final class PathNodeContentProvider extends ArrayContentProvider
			implements ITreeContentProvider {
		@Override
		public Object[] getElements(Object inputElement) {
			if (baseVersionPathsWithChildren.isEmpty() && addedPaths.isEmpty())
				return new String[] { UIText.CompareTreeView_NoDifferencesFoundMessage };
			if (input instanceof IResource[]) {
				IResource[] resources = (IResource[]) input;
				PathNode[] nodes = new PathNode[resources.length];
				for (int i = 0; i < resources.length; i++) {
					IResource resource = resources[i];
					if (resource instanceof IFile) {
						IPath path = new Path(repositoryMapping
								.getRepoRelativePath(resource));
						Type type;
						if (addedPaths.contains(path))
							type = Type.FILE_ADDED;
						else if (equalContentPaths.contains(path))
							type = Type.FILE_BOTH_SIDES_SAME;
						else
							type = Type.FILE_BOTH_SIDES_DIFFER;
						nodes[i] = new PathNode(path, type);
					} else
						nodes[i] = new PathNode(new Path(repositoryMapping
								.getRepoRelativePath(resource)), Type.FOLDER);
				}
				return nodes;
			}
			return new PathNode[] { new PathNode(new Path(""), Type.FOLDER) }; //$NON-NLS-1$
		}

		public Object[] getChildren(Object parentElement) {
			PathNode parentNode = (PathNode) parentElement;
			IPath parent = parentNode.path;
			List<PathNode> children = new ArrayList<PathNode>();
			for (IPath childPath : baseVersionPathsWithChildren) {
				if (childPath.segmentCount() > 0
						&& childPath.removeLastSegments(1).equals(parent)) {
					children.add(new PathNode(childPath, Type.FOLDER));
				}
			}
			for (IPath mapPath : baseVersionMap.keySet()) {
				if (mapPath.removeLastSegments(1).equals(parent)
						&& (showEquals || !equalContentPaths.contains(mapPath))) {
					if (addedPaths.contains(mapPath))
						children.add(new PathNode(mapPath, Type.FILE_ADDED));
					else if (equalContentPaths.contains(mapPath))
						children.add(new PathNode(mapPath,
								Type.FILE_BOTH_SIDES_SAME));
					else
						children.add(new PathNode(mapPath,
								Type.FILE_BOTH_SIDES_DIFFER));
				}
			}
			if (parentNode.type == Type.FOLDER) {
				List<PathNodeAdapter> deletedChildren = compareVersionPathsWithChildren
						.get(parent);
				if (deletedChildren != null)
					for (PathNodeAdapter path : deletedChildren)
						children.add(path.pathNode);

			}
			return children.toArray();
		}

		public boolean hasChildren(Object element) {
			if (!(element instanceof PathNode))
				return false;
			IPath parent = ((PathNode) element).path;
			for (IPath childPath : baseVersionPathsWithChildren) {
				if (childPath.removeLastSegments(1).equals(parent))
					return true;
			}
			for (IPath mapPath : baseVersionMap.keySet()) {
				if (mapPath.removeLastSegments(1).equals(parent)
						&& (showEquals || !equalContentPaths.contains(mapPath))) {
					return true;
				}
			}
			return false;
		}

		public Object getParent(Object element) {
			if (!(element instanceof PathNode))
				return null;
			IPath currentPath = ((PathNode) element).path;
			if (currentPath.segmentCount() > 0)
				return currentPath.removeLastSegments(1);
			return null;
		}
	}

	/**
	 * Used to render {@link PathNode} trees
	 */
	private final class PathNodeLabelProvider extends BaseLabelProvider
			implements ILabelProvider {
		public Image getImage(Object element) {
			if (element instanceof String)
				return null;
			Type type = ((PathNode) element).type;
			switch (type) {
			case FILE_BOTH_SIDES_SAME:
				return SAME_CONTENT;
			case FILE_BOTH_SIDES_DIFFER:
				return FILE_IMAGE;
			case FILE_ADDED:
				return ADDED;
			case FILE_DELETED:
				return DELETED;
			case FOLDER:
				return FOLDER_IMAGE;
			}
			return null;
		}

		public String getText(Object element) {
			if (element instanceof String)
				return (String) element;
			IPath path = ((PathNode) element).path;
			if (path.segmentCount() == 0)
				return UIText.CompareTreeView_RepositoryRootName;
			return path.lastSegment();

		}
	}

	private final static class PathNodeAdapter implements IWorkbenchAdapter {
		private final static Object[] EMPTYARRAY = new Object[0];

		PathNode pathNode;

		public PathNodeAdapter(PathNode path) {
			pathNode = path;
		}

		public Object[] getChildren(Object o) {
			return EMPTYARRAY;
		}

		public ImageDescriptor getImageDescriptor(Object object) {
			return UIIcons.ELCL16_DELETE;
		}

		public String getLabel(Object o) {
			return pathNode.path.lastSegment();
		}

		public Object getParent(Object o) {
			// doesn't seem to hurt to simply return null
			return null;
		}
	}

	/*
	 * @see org.eclipse.jface.action.IMenuListener#menuAboutToShow(org.eclipse.jface.action.IMenuManager)
	 * @since 2.1
	 */
	public void menuAboutToShow(IMenuManager manager) {
		ITreeSelection selection = (ITreeSelection) tree.getSelection();
		if (selection.isEmpty())
			return;

		manager.add(new Separator(ICommonMenuConstants.GROUP_OPEN));
		manager.add(new Separator(ICommonMenuConstants.GROUP_ADDITIONS));

		IAction openAction = createOpenAction(selection);
		if (openAction != null)
			manager.appendToGroup(ICommonMenuConstants.GROUP_OPEN, openAction);

		MenuManager showInSubMenu = UIUtils.createShowInMenu(
				getSite().getWorkbenchWindow());
		manager.appendToGroup(ICommonMenuConstants.GROUP_OPEN, showInSubMenu);
	}

	/*
	 * @see org.eclipse.ui.part.IShowInSource#getShowInContext()
	 * @since 2.1
	 */
	public ShowInContext getShowInContext() {
		IPath repoPath = getRepositoryPath();
		ITreeSelection selection = (ITreeSelection) tree.getSelection();
		List<IResource> resources = new ArrayList<IResource>();
		for (Iterator it = selection.iterator(); it.hasNext();) {
			Object obj = it.next();
			if (obj instanceof IResource)
				resources.add((IResource) obj);
			else if (obj instanceof PathNode && repoPath != null) {
				PathNode pathNode = (PathNode) obj;
				IResource resource = ResourceUtil
						.getResourceForLocation(repoPath.append(pathNode
								.getRepoRelativePath()));
				if (resource != null)
					resources.add(resource);
			}
		}
		return new ShowInContext(null, new StructuredSelection(resources));
	}

	private void createContextMenu() {
		MenuManager manager = new MenuManager("#PopupMenu"); //$NON-NLS-1$
		manager.setRemoveAllWhenShown(true);
		manager.addMenuListener(this);

		Menu contextMenu = manager.createContextMenu(tree.getControl());
		tree.getControl().setMenu(contextMenu);
		getSite().registerContextMenu(manager, tree);
	}

	private void openFileInEditor(String filePath) {
		IWorkbenchWindow window = PlatformUI.getWorkbench()
				.getActiveWorkbenchWindow();
		File file = new File(filePath);
		if (!file.exists()) {
			String message = NLS.bind(UIText.CommitFileDiffViewer_FileDoesNotExist, filePath);
			Activator.showError(message, null);
		}
		IWorkbenchPage page = window.getActivePage();
		EgitUiEditorUtils.openEditor(file, page);
	}

	private IAction createOpenAction(ITreeSelection selection) {
		final List<String> pathsToOpen = getSelectedPaths(selection);
		if (pathsToOpen == null || pathsToOpen.isEmpty())
			return null;

		return new Action(
				UIText.CommitFileDiffViewer_OpenWorkingTreeVersionInEditorMenuLabel) {
			@Override
			public void run() {
				for (String filePath : pathsToOpen)
					openFileInEditor(filePath);
			}
		};
	}

	private List<String> getSelectedPaths(ITreeSelection selection) {
		IPath repoPath = getRepositoryPath();
		List<String> pathsToOpen = new ArrayList<String>();
		for (Iterator it = selection.iterator(); it.hasNext();) {
			Object obj = it.next();
			if (obj instanceof IFile) {
				pathsToOpen.add(((IFile) obj).getLocation().toOSString());
			} else if (obj instanceof PathNode && repoPath != null) {
				PathNode pathNode = (PathNode) obj;
				if (pathNode.type == PathNode.Type.FOLDER
						|| pathNode.type == PathNode.Type.FILE_DELETED)
					return null;
				pathsToOpen.add(repoPath.append(pathNode.path).toOSString());
			} else {
				return null; // fail if one of the selected elements does not fit
			}
		}
		return pathsToOpen;
	}

	private IPath getRepositoryPath() {
		Repository repo = getRepository();
		if (repo != null)
			return new Path(repo.getWorkTree().getAbsolutePath());

		return null;
	}

	private Repository getRepository() {
		if (repositoryMapping != null)
			return repositoryMapping.getRepository();
		else if (input instanceof Repository)
			return (Repository) input;

		return null;
	}

}
