/*******************************************************************************
 * Copyright (c) 2010, 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
 *    Dariusz Luksza <dariusz@luksza.org> - add synchronization feature
 *    Daniel Megert <daniel_megert@ch.ibm.com> - Only check out on double-click
 *    Daniel Megert <daniel_megert@ch.ibm.com> - Don't reveal selection on refresh
 *    Robin Stocker <robin@nibor.org> - Show In support
 *******************************************************************************/
package org.eclipse.egit.ui.internal.repository;

import java.io.File;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.core.commands.Command;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener;
import org.eclipse.core.runtime.preferences.IEclipsePreferences.PreferenceChangeEvent;
import org.eclipse.egit.core.AdapterUtils;
import org.eclipse.egit.core.RepositoryCache;
import org.eclipse.egit.core.RepositoryUtil;
import org.eclipse.egit.core.internal.util.ResourceUtil;
import org.eclipse.egit.ui.Activator;
import org.eclipse.egit.ui.JobFamilies;
import org.eclipse.egit.ui.UIPreferences;
import org.eclipse.egit.ui.UIUtils;
import org.eclipse.egit.ui.internal.CommonUtils;
import org.eclipse.egit.ui.internal.UIIcons;
import org.eclipse.egit.ui.internal.UIText;
import org.eclipse.egit.ui.internal.branch.BranchOperationUI;
import org.eclipse.egit.ui.internal.history.HistoryPageInput;
import org.eclipse.egit.ui.internal.reflog.ReflogView;
import org.eclipse.egit.ui.internal.repository.tree.FetchNode;
import org.eclipse.egit.ui.internal.repository.tree.FileNode;
import org.eclipse.egit.ui.internal.repository.tree.FolderNode;
import org.eclipse.egit.ui.internal.repository.tree.PushNode;
import org.eclipse.egit.ui.internal.repository.tree.RefNode;
import org.eclipse.egit.ui.internal.repository.tree.RemoteNode;
import org.eclipse.egit.ui.internal.repository.tree.RepositoryNode;
import org.eclipse.egit.ui.internal.repository.tree.RepositoryTreeNode;
import org.eclipse.egit.ui.internal.repository.tree.RepositoryTreeNodeType;
import org.eclipse.egit.ui.internal.repository.tree.StashedCommitNode;
import org.eclipse.egit.ui.internal.repository.tree.TagNode;
import org.eclipse.egit.ui.internal.repository.tree.WorkingDirNode;
import org.eclipse.egit.ui.internal.selection.SelectionUtils;
import org.eclipse.egit.ui.internal.trace.GitTraceLocation;
import org.eclipse.jface.action.IMenuListener;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.dialogs.MessageDialogWithToggle;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.resource.JFaceColors;
import org.eclipse.jface.viewers.DoubleClickEvent;
import org.eclipse.jface.viewers.IDoubleClickListener;
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.OpenEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TreeSelection;
import org.eclipse.jface.window.Window;
import org.eclipse.jgit.events.ConfigChangedEvent;
import org.eclipse.jgit.events.ConfigChangedListener;
import org.eclipse.jgit.events.IndexChangedEvent;
import org.eclipse.jgit.events.IndexChangedListener;
import org.eclipse.jgit.events.ListenerHandle;
import org.eclipse.jgit.events.RefsChangedEvent;
import org.eclipse.jgit.events.RefsChangedListener;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StackLayout;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.team.ui.history.IHistoryView;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IFileEditorInput;
import org.eclipse.ui.IPageLayout;
import org.eclipse.ui.ISelectionListener;
import org.eclipse.ui.ISelectionService;
import org.eclipse.ui.IViewPart;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.commands.ICommandService;
import org.eclipse.ui.contexts.IContextService;
import org.eclipse.ui.forms.events.HyperlinkAdapter;
import org.eclipse.ui.forms.events.HyperlinkEvent;
import org.eclipse.ui.forms.widgets.FormToolkit;
import org.eclipse.ui.forms.widgets.Hyperlink;
import org.eclipse.ui.handlers.IHandlerService;
import org.eclipse.ui.handlers.RegistryToggleState;
import org.eclipse.ui.navigator.CommonNavigator;
import org.eclipse.ui.navigator.CommonViewer;
import org.eclipse.ui.part.IPage;
import org.eclipse.ui.part.IShowInSource;
import org.eclipse.ui.part.IShowInTargetList;
import org.eclipse.ui.part.ShowInContext;
import org.eclipse.ui.progress.IWorkbenchSiteProgressService;
import org.eclipse.ui.views.properties.IPropertySheetPage;
import org.eclipse.ui.views.properties.PropertySheet;
import org.eclipse.ui.views.properties.PropertySheetPage;

/**
 * The "Git Repositories View"
 */
public class RepositoriesView extends CommonNavigator implements IShowInSource, IShowInTargetList {

	/** "remote" */
	public static final String REMOTE = "remote"; //$NON-NLS-1$

	/** "url" */
	public static final String URL = "url"; //$NON-NLS-1$

	/** "pushurl" */
	public static final String PUSHURL = "pushurl"; //$NON-NLS-1$

	/** "push" */
	public static final String PUSH = "push"; //$NON-NLS-1$

	/** "fetch" */
	public static final String FETCH = "fetch"; //$NON-NLS-1$

	/** view id */
	public static final String VIEW_ID = "org.eclipse.egit.ui.RepositoriesView"; //$NON-NLS-1$

	private static final long DEFAULT_REFRESH_DELAY = 1000;

	private final Set<Repository> repositories = new HashSet<Repository>();

	private final RefsChangedListener myRefsChangedListener;

	private final IndexChangedListener myIndexChangedListener;

	private final ConfigChangedListener myConfigChangeListener;

	private final List<ListenerHandle> myListeners = new LinkedList<ListenerHandle>();

	private Job scheduledJob;

	private final RepositoryUtil repositoryUtil;

	private final RepositoryCache repositoryCache;

	private Composite emptyArea;

	private StackLayout layout;

	private volatile long lastInputChange = 0L;

	private volatile long lastRepositoryChange = 0L;

	private volatile long lastInputUpdate = -1L;

	private boolean reactOnSelection = false;

	private final IPreferenceChangeListener configurationListener;

	private ISelectionListener selectionChangedListener;

	/**
	 * The default constructor
	 */
	public RepositoriesView() {
		repositoryUtil = Activator.getDefault().getRepositoryUtil();
		repositoryCache = org.eclipse.egit.core.Activator.getDefault()
				.getRepositoryCache();

		configurationListener = new IPreferenceChangeListener() {
			public void preferenceChange(PreferenceChangeEvent event) {
				lastInputChange = System.currentTimeMillis();
				scheduleRefresh(DEFAULT_REFRESH_DELAY);
			}
		};

		myRefsChangedListener = new RefsChangedListener() {
			public void onRefsChanged(RefsChangedEvent e) {
				lastRepositoryChange = System.currentTimeMillis();
				scheduleRefresh(DEFAULT_REFRESH_DELAY);
			}
		};

		myIndexChangedListener = new IndexChangedListener() {
			public void onIndexChanged(IndexChangedEvent event) {
				lastRepositoryChange = System.currentTimeMillis();
				scheduleRefresh(DEFAULT_REFRESH_DELAY);

			}
		};

		myConfigChangeListener = new ConfigChangedListener() {
			public void onConfigChanged(ConfigChangedEvent event) {
				lastRepositoryChange = System.currentTimeMillis();
				scheduleRefresh(DEFAULT_REFRESH_DELAY);
			}
		};

		selectionChangedListener = new ISelectionListener() {
			public void selectionChanged(IWorkbenchPart part,
					ISelection selection) {
				if (!reactOnSelection)
					return;

				// this may happen if we switch between editors
				if (part instanceof IEditorPart) {
					IEditorInput input = ((IEditorPart) part).getEditorInput();
					if (input instanceof IFileEditorInput)
						reactOnSelection(new StructuredSelection(
								((IFileEditorInput) input).getFile()));

				} else
					reactOnSelection(selection);
			}
		};
	}

	/**
	 * Create area shown when no repositories are present
	 *
	 * @param parent
	 */
	protected void createEmptyArea(Composite parent) {
		emptyArea = new Composite(parent, SWT.NONE);
		emptyArea.setBackgroundMode(SWT.INHERIT_FORCE);
		MenuManager manager = new MenuManager();
		manager.addMenuListener(new IMenuListener() {
			public void menuAboutToShow(IMenuManager m) {
				getNavigatorActionService().fillContextMenu(m);
			}
		});
		getSite().registerContextMenu(manager, getCommonViewer());
		Menu menu = manager.createContextMenu(emptyArea);
		emptyArea.setMenu(menu);
		GridLayoutFactory.fillDefaults().applyTo(emptyArea);
		Composite infoArea = new Composite(emptyArea, SWT.NONE);
		infoArea.setMenu(menu);
		GridDataFactory.swtDefaults().align(SWT.CENTER, SWT.CENTER)
				.grab(true, true).applyTo(infoArea);
		GridLayoutFactory.swtDefaults().applyTo(infoArea);
		Label messageLabel = new Label(infoArea, SWT.WRAP);
		messageLabel.setText(UIText.RepositoriesView_messsageEmpty);
		messageLabel.setMenu(menu);
		GridDataFactory.swtDefaults().align(SWT.FILL, SWT.FILL)
				.grab(true, false).applyTo(messageLabel);
		Composite optionsArea = new Composite(infoArea, SWT.NONE);
		optionsArea.setMenu(menu);
		GridLayoutFactory.swtDefaults().numColumns(2).applyTo(optionsArea);
		GridDataFactory.swtDefaults().align(SWT.CENTER, SWT.CENTER)
				.grab(true, true).applyTo(optionsArea);

		final FormToolkit toolkit = new FormToolkit(emptyArea.getDisplay());
		emptyArea.addDisposeListener(new DisposeListener() {

			public void widgetDisposed(DisposeEvent e) {
				toolkit.dispose();
			}
		});
		final Color linkColor = JFaceColors.getHyperlinkText(emptyArea
				.getDisplay());

		Label addLabel = new Label(optionsArea, SWT.NONE);
		Image addImage = UIIcons.NEW_REPOSITORY.createImage();
		UIUtils.hookDisposal(addLabel, addImage);
		addLabel.setImage(addImage);
		Hyperlink addLink = toolkit.createHyperlink(optionsArea,
				UIText.RepositoriesView_linkAdd, SWT.WRAP);
		addLink.setForeground(linkColor);
		addLink.addHyperlinkListener(new HyperlinkAdapter() {
			public void linkActivated(HyperlinkEvent e) {
				IHandlerService service = CommonUtils.getService(getViewSite(), IHandlerService.class);
				UIUtils.executeCommand(service,
						"org.eclipse.egit.ui.RepositoriesViewAddRepository"); //$NON-NLS-1$
			}
		});
		GridDataFactory.swtDefaults().align(SWT.FILL, SWT.FILL)
				.grab(true, false).applyTo(addLink);

		Label cloneLabel = new Label(optionsArea, SWT.NONE);
		Image cloneImage = UIIcons.CLONEGIT.createImage();
		UIUtils.hookDisposal(cloneLabel, cloneImage);
		cloneLabel.setImage(cloneImage);
		Hyperlink cloneLink = toolkit.createHyperlink(optionsArea,
				UIText.RepositoriesView_linkClone, SWT.WRAP);
		cloneLink.setForeground(linkColor);
		cloneLink.addHyperlinkListener(new HyperlinkAdapter() {
			public void linkActivated(HyperlinkEvent e) {
				IHandlerService service = CommonUtils.getService(getViewSite(), IHandlerService.class);
				UIUtils.executeCommand(service,
						"org.eclipse.egit.ui.RepositoriesViewClone"); //$NON-NLS-1$
			}
		});
		GridDataFactory.swtDefaults().align(SWT.FILL, SWT.FILL)
				.grab(true, false).applyTo(cloneLink);

		Label createLabel = new Label(optionsArea, SWT.NONE);
		Image createImage = UIIcons.CREATE_REPOSITORY.createImage();
		UIUtils.hookDisposal(createLabel, createImage);
		createLabel.setImage(createImage);
		Hyperlink createLink = toolkit.createHyperlink(optionsArea,
				UIText.RepositoriesView_linkCreate, SWT.WRAP);
		createLink.setForeground(linkColor);
		createLink.setText(UIText.RepositoriesView_linkCreate);
		createLink.addHyperlinkListener(new HyperlinkAdapter() {
			public void linkActivated(HyperlinkEvent e) {
				IHandlerService service = CommonUtils.getService(getViewSite(), IHandlerService.class);
				UIUtils.executeCommand(service,
						"org.eclipse.egit.ui.RepositoriesViewCreateRepository"); //$NON-NLS-1$
			}
		});
		GridDataFactory.swtDefaults().align(SWT.FILL, SWT.FILL)
				.grab(true, false).applyTo(createLink);
	}

	@SuppressWarnings("boxing")
	@Override
	public void createPartControl(Composite aParent) {
		Composite displayArea = new Composite(aParent, SWT.NONE);
		layout = new StackLayout();
		displayArea.setLayout(layout);
		createEmptyArea(displayArea);

		super.createPartControl(displayArea);

		IWorkbenchWindow w = PlatformUI.getWorkbench()
				.getActiveWorkbenchWindow();
		ICommandService csrv = CommonUtils.getService(w, ICommandService.class);
		Command command = csrv
				.getCommand("org.eclipse.egit.ui.RepositoriesLinkWithSelection"); //$NON-NLS-1$
		reactOnSelection = (Boolean) command.getState(
				RegistryToggleState.STATE_ID).getValue();

		IWorkbenchSiteProgressService service = CommonUtils.getService(getSite(), IWorkbenchSiteProgressService.class);
		if (service != null) {
			service.showBusyForFamily(JobFamilies.REPO_VIEW_REFRESH);
			service.showBusyForFamily(JobFamilies.CLONE);
		}
	}

	@Override
	public Object getAdapter(Class adapter) {
		// integrate with Properties view
		if (adapter == IPropertySheetPage.class) {
			PropertySheetPage page = new PropertySheetPage();
			page
					.setPropertySourceProvider(new RepositoryPropertySourceProvider(
							page));
			return page;
		}
		return super.getAdapter(adapter);
	}

	/**
	 * Used by the "link with selection" action
	 *
	 * @param reactOnSelection
	 */
	public void setReactOnSelection(boolean reactOnSelection) {
		this.reactOnSelection = reactOnSelection;
	}

	@Override
	protected CommonViewer createCommonViewer(Composite aParent) {
		CommonViewer viewer = super.createCommonViewer(aParent);
		// handle the double-click event for tags and branches
		viewer.addDoubleClickListener(new IDoubleClickListener() {
			public void doubleClick(DoubleClickEvent event) {
				TreeSelection sel = (TreeSelection) event.getSelection();
				RepositoryTreeNode element = (RepositoryTreeNode) sel
						.getFirstElement();
				// Disable checkout for bare repositories
				if (element.getRepository().isBare())
					return;
				if (element instanceof RefNode)
					executeOpenCommandWithConfirmation(((RefNode) element)
							.getObject().getName());
				if (element instanceof TagNode)
					executeOpenCommandWithConfirmation(((TagNode) element)
							.getObject().getName());
			}
		});
		// handle open event for the working directory
		viewer.addOpenListener(new IOpenListener() {
			public void open(OpenEvent event) {
				TreeSelection sel = (TreeSelection) event.getSelection();
				RepositoryTreeNode element = (RepositoryTreeNode) sel
						.getFirstElement();
				if (element instanceof FileNode
						|| element instanceof StashedCommitNode)
					executeOpenCommand();
			}
		});
		// react on selection changes
		ISelectionService srv = CommonUtils.getService(getSite(), ISelectionService.class);
		srv.addPostSelectionListener(selectionChangedListener);
		// react on changes in the configured repositories
		repositoryUtil.getPreferences().addPreferenceChangeListener(
				configurationListener);
		initRepositoriesAndListeners();
		activateContextService();

		emptyArea.setBackground(viewer.getControl().getBackground());
		if (!repositories.isEmpty())
			layout.topControl = viewer.getControl();
		else
			layout.topControl = emptyArea;

		return viewer;
	}

	private void executeOpenCommandWithConfirmation(String refName) {
		if (!BranchOperationUI.checkoutWillShowQuestionDialog(refName)) {
			String shortName = Repository.shortenRefName(refName);

			IPreferenceStore store = Activator.getDefault()
					.getPreferenceStore();

			if (store.getBoolean(UIPreferences.SHOW_CHECKOUT_CONFIRMATION)) {
				String toggleMessage = UIText.RepositoriesView_CheckoutConfirmationToggleMessage;
				MessageDialogWithToggle dlg = MessageDialogWithToggle
						.openOkCancelConfirm(
								getViewSite().getShell(),
				UIText.RepositoriesView_CheckoutConfirmationTitle,
				MessageFormat.format(UIText.RepositoriesView_CheckoutConfirmationMessage,
										shortName),
										toggleMessage, false, store,
										UIPreferences.SHOW_CHECKOUT_CONFIRMATION);
				if (dlg.getReturnCode() != Window.OK)
					return;
			}
		}
		executeOpenCommand();
	}

	private void executeOpenCommand() {
		IHandlerService srv = CommonUtils.getService(getViewSite(), IHandlerService.class);

		try {
			srv.executeCommand("org.eclipse.egit.ui.RepositoriesViewOpen", null); //$NON-NLS-1$
		} catch (Exception e) {
			Activator.handleError(e.getMessage(), e, false);
		}
	}

	private void activateContextService() {
		IContextService contextService = CommonUtils.getService(getSite(), IContextService.class);
		if (contextService != null)
			contextService.activateContext(VIEW_ID);

	}

	private void initRepositoriesAndListeners() {
		synchronized (repositories) {
			repositories.clear();
			unregisterRepositoryListener();
			// listen for repository changes
			for (String dir : repositoryUtil.getConfiguredRepositories()) {
				File repoDir = new File(dir);
				try {
					Repository repo = repositoryCache.lookupRepository(repoDir);
					myListeners.add(repo.getListenerList()
							.addIndexChangedListener(myIndexChangedListener));
					myListeners.add(repo.getListenerList()
							.addRefsChangedListener(myRefsChangedListener));
					myListeners.add(repo.getListenerList()
							.addConfigChangedListener(myConfigChangeListener));
					repositories.add(repo);
				} catch (IOException e) {
					String message = NLS
							.bind(UIText.RepositoriesView_ExceptionLookingUpRepoMessage,
									repoDir.getPath());
					Activator.handleError(message, e, false);
					repositoryUtil.removeDir(repoDir);
				}
			}
		}
	}

	@Override
	public void dispose() {
		// make sure to cancel the refresh job
		if (this.scheduledJob != null) {
			this.scheduledJob.cancel();
			this.scheduledJob = null;
		}

		repositoryUtil.getPreferences().removePreferenceChangeListener(
				configurationListener);

		ISelectionService srv = CommonUtils.getService(getSite(), ISelectionService.class);
		srv.removePostSelectionListener(selectionChangedListener);

		// remove RepositoryChangedListener
		unregisterRepositoryListener();
		repositories.clear();

		super.dispose();
	}

	/**
	 * @see #showPaths(List)
	 * @param resource
	 */
	private void showResource(final IResource resource) {
		IPath location = resource.getLocation();
		if (location != null)
			showPaths(Arrays.asList(location));
	}

	/**
	 * Opens the tree and marks the working directory files or folders that
	 * represent the passed paths if possible.
	 *
	 * @param paths
	 *            the paths to show
	 */
	private void showPaths(final List<IPath> paths) {
		final List<RepositoryTreeNode> nodesToShow = new ArrayList<RepositoryTreeNode>();

		Map<Repository, Collection<String>> pathsByRepo = ResourceUtil.splitPathsByRepository(paths);
		for (Map.Entry<Repository, Collection<String>> entry : pathsByRepo.entrySet()) {
			Repository repository = entry.getKey();
			try {
				boolean added = repositoryUtil.addConfiguredRepository(repository.getDirectory());
				if (added)
					scheduleRefresh(0);
			} catch (IllegalArgumentException iae) {
				Activator.handleError(iae.getMessage(), iae, false);
				continue;
			}

			if (this.scheduledJob != null)
				try {
					this.scheduledJob.join();
				} catch (InterruptedException e) {
					Activator.handleError(e.getMessage(), e, false);
				}

			for (String repoPath : entry.getValue()) {
				final RepositoryTreeNode node = getNodeForPath(repository, repoPath);
				if (node != null)
					nodesToShow.add(node);
			}
		}

		Display.getDefault().asyncExec(new Runnable() {
			public void run() {
				selectReveal(new StructuredSelection(nodesToShow));
			}
		});
	}

	/**
	 * Reveals and shows the given repository in the view.
	 *
	 * @param repositoryToShow
	 */
	public void showRepository(Repository repositoryToShow) {
		ITreeContentProvider cp = (ITreeContentProvider) getCommonViewer()
				.getContentProvider();
		for (Object repo : cp.getElements(getCommonViewer().getInput())) {
			RepositoryTreeNode node = (RepositoryTreeNode) repo;
			if (repositoryToShow.getDirectory().equals(node.getRepository().getDirectory()))
				selectReveal(new StructuredSelection(node));
		}
	}
	/**
	 * Refresh Repositories View
	 */
	public void refresh() {
		lastInputUpdate = -1L;
		scheduleRefresh(0);
	}

	private Job scheduleRefresh(long delay) {
		boolean trace = GitTraceLocation.REPOSITORIESVIEW.isActive();
		if (trace)
			GitTraceLocation.getTrace().trace(
					GitTraceLocation.REPOSITORIESVIEW.getLocation(),
					"Entering scheduleRefresh()"); //$NON-NLS-1$

		if (scheduledJob != null
				&& (scheduledJob.getState() == Job.RUNNING
						|| scheduledJob.getState() == Job.WAITING || scheduledJob
						.getState() == Job.SLEEPING)) {
			if (trace)
				GitTraceLocation.getTrace().trace(
						GitTraceLocation.REPOSITORIESVIEW.getLocation(),
						"Pending refresh job, returning"); //$NON-NLS-1$
			return scheduledJob;
		}

		final CommonViewer tv = getCommonViewer();
		final boolean needsNewInput = lastInputChange > lastInputUpdate;

		if (trace)
			GitTraceLocation.getTrace().trace(
					GitTraceLocation.REPOSITORIESVIEW.getLocation(),
					"New input required: " + needsNewInput); //$NON-NLS-1$

		Job job = new Job("Refreshing Git Repositories view") { //$NON-NLS-1$

			@Override
			protected IStatus run(IProgressMonitor monitor) {
				boolean actTrace = GitTraceLocation.REPOSITORIESVIEW.isActive();
				if (actTrace)
					GitTraceLocation.getTrace().trace(
							GitTraceLocation.REPOSITORIESVIEW.getLocation(),
							"Running the update"); //$NON-NLS-1$
				lastInputUpdate = System.currentTimeMillis();
				if (needsNewInput)
					initRepositoriesAndListeners();

				if (!UIUtils.isUsable(tv))
					return Status.CANCEL_STATUS;
				PlatformUI.getWorkbench().getDisplay()
						.asyncExec(new Runnable() {
					public void run() {
						if (!UIUtils.isUsable(tv))
							return;
						long start = 0;
						boolean traceActive = GitTraceLocation.REPOSITORIESVIEW
								.isActive();
						if (traceActive) {
							start = System.currentTimeMillis();
							GitTraceLocation.getTrace().trace(
									GitTraceLocation.REPOSITORIESVIEW
											.getLocation(),
									"Starting async update job"); //$NON-NLS-1$
						}


						if (needsNewInput) {
							// keep expansion state and selection so that we can
							// restore the tree
							// after update
							Object[] expanded = tv.getExpandedElements();
							tv.setInput(ResourcesPlugin.getWorkspace()
									.getRoot());
							tv.setExpandedElements(expanded);
						} else
							tv.refresh(true);

						IViewPart part = PlatformUI.getWorkbench()
								.getActiveWorkbenchWindow().getActivePage()
								.findView(IPageLayout.ID_PROP_SHEET);
						if (part instanceof PropertySheet) {
							PropertySheet sheet = (PropertySheet) part;
							IPage page = sheet.getCurrentPage();
							if (page instanceof PropertySheetPage)
								((PropertySheetPage) page).refresh();
						}
						if (traceActive)
							GitTraceLocation
									.getTrace()
									.trace(
											GitTraceLocation.REPOSITORIESVIEW
													.getLocation(),
											"Ending async update job after " + (System.currentTimeMillis() - start) + " ms"); //$NON-NLS-1$ //$NON-NLS-2$
						if (!repositories.isEmpty())
							layout.topControl = getCommonViewer().getControl();
						else
							layout.topControl = emptyArea;
						emptyArea.getParent().layout(true, true);
					}
				});

				if (lastInputChange > lastInputUpdate
						|| lastRepositoryChange > lastInputUpdate) {
					if (actTrace)
						GitTraceLocation.getTrace()
								.trace(
										GitTraceLocation.REPOSITORIESVIEW
												.getLocation(),
										"Rescheduling refresh job"); //$NON-NLS-1$
					schedule(DEFAULT_REFRESH_DELAY);
				}
				return Status.OK_STATUS;
			}

			@Override
			public boolean belongsTo(Object family) {
				if (JobFamilies.REPO_VIEW_REFRESH.equals(family))
					return true;
				return super.belongsTo(family);
			}

		};
		job.setSystem(true);

		IWorkbenchSiteProgressService service = CommonUtils.getService(getSite(), IWorkbenchSiteProgressService.class);

		if (trace)
			GitTraceLocation.getTrace().trace(
					GitTraceLocation.REPOSITORIESVIEW.getLocation(),
					"Scheduling refresh job"); //$NON-NLS-1$
		service.schedule(job, delay);

		scheduledJob = job;
		return scheduledJob;
	}

	private void unregisterRepositoryListener() {
		for (ListenerHandle lh : myListeners)
			lh.remove();
		myListeners.clear();
	}

	public boolean show(ShowInContext context) {
		ISelection selection = context.getSelection();
		if (selection instanceof IStructuredSelection) {
			IStructuredSelection ss = (IStructuredSelection) selection;
			List<IPath> paths = new ArrayList<IPath>();
			for (Iterator it = ss.iterator(); it.hasNext();) {
				Object element = it.next();
				IResource resource = AdapterUtils.adapt(element, IResource.class);
				if (resource != null) {
					IPath location = resource.getLocation();
					if (location != null)
						paths.add(location);
				} else if (element instanceof IPath)
					paths.add((IPath) element);
			}
			if (!paths.isEmpty()) {
				showPaths(paths);
				return true;
			}

			Repository repository = SelectionUtils.getRepository(ss);
			if (repository != null) {
				showRepository(repository);
				return true;
			}
		}
		if(context.getInput() instanceof IFileEditorInput) {
			IFileEditorInput input = (IFileEditorInput) context.getInput();
			showResource(input.getFile());
		}
		return false;
	}

	public ShowInContext getShowInContext() {
		IStructuredSelection selection = (IStructuredSelection) getCommonViewer()
				.getSelection();
		List<Object> elements = getShowInElements(selection);
		// GenericHistoryView only shows a selection of a single resource (see
		// bug 392949), so prepare our own history page input which can contain
		// multiple files to support showing more than one file in history.
		// It's also necessary for a single file that is outside of the
		// workspace (and as such is not an IResource).
		HistoryPageInput historyPageInput = getHistoryPageInput(selection);
		return new ShowInContext(historyPageInput, new StructuredSelection(elements));
	}

	public String[] getShowInTargetIds() {
		IStructuredSelection selection = (IStructuredSelection) getCommonViewer()
				.getSelection();
		for (Object element : selection.toList())
			if (element instanceof RepositoryNode)
				return new String[] { IHistoryView.VIEW_ID, ReflogView.VIEW_ID };

		// Make sure History view is always listed, regardless of perspective
		return new String[] { IHistoryView.VIEW_ID };
	}

	private static List<Object> getShowInElements(IStructuredSelection selection) {
		List<Object> elements = new ArrayList<Object>();
		for (Object element : selection.toList()) {
			if (element instanceof FileNode || element instanceof FolderNode
					|| element instanceof WorkingDirNode) {
				RepositoryTreeNode treeNode = (RepositoryTreeNode) element;
				IPath path = treeNode.getPath();
				IResource resource = ResourceUtil.getResourceForLocation(path);
				if (resource != null)
					elements.add(resource);
			} else if (element instanceof RepositoryNode) {
				// Can be shown in History, Reflog and Properties views
				elements.add(element);
			} else if (element instanceof RepositoryNode
					|| element instanceof RemoteNode
					|| element instanceof FetchNode
					|| element instanceof PushNode
					|| element instanceof TagNode
					|| element instanceof RefNode) {
				// These can be shown in Properties view directly
				elements.add(element);
			}
		}
		return elements;
	}

	/**
	 * @param selection
	 * @return the HistoryPageInput corresponding to the selection, or null
	 */
	private static HistoryPageInput getHistoryPageInput(IStructuredSelection selection) {
		List<File> files = new ArrayList<File>();
		Repository repo = null;
		for (Object element : selection.toList()) {
			Repository nodeRepository;
			if (element instanceof FileNode) {
				FileNode fileNode = (FileNode) element;
				files.add(fileNode.getObject());
				nodeRepository = fileNode.getRepository();
			} else if (element instanceof FolderNode) {
				FolderNode folderNode = (FolderNode) element;
				files.add(folderNode.getObject());
				nodeRepository = folderNode.getRepository();
			} else {
				// Don't return input if selection is not file/folder
				return null;
			}
			if (repo == null)
				repo = nodeRepository;
			// Don't return input if nodes from different repositories are selected
			if (repo != nodeRepository)
				return null;
		}
		if (repo != null)
			return new HistoryPageInput(repo, files.toArray(new File[files.size()]));
		else
			return null;
	}

	private void reactOnSelection(ISelection selection) {
		if (selection instanceof StructuredSelection) {
			StructuredSelection ssel = (StructuredSelection) selection;
			if (ssel.size() != 1)
				return;
			if (ssel.getFirstElement() instanceof IResource)
				showResource((IResource) ssel.getFirstElement());
			if (ssel.getFirstElement() instanceof IAdaptable) {
				IResource adapted = (IResource) ((IAdaptable) ssel
						.getFirstElement()).getAdapter(IResource.class);
				if (adapted != null)
					showResource(adapted);
			}
		}
	}

	private RepositoryTreeNode getNodeForPath(Repository repository, String repoRelativePath) {
		RepositoryTreeNode currentNode = null;
		ITreeContentProvider cp = (ITreeContentProvider) getCommonViewer()
				.getContentProvider();
		for (Object repo : cp.getElements(getCommonViewer().getInput())) {
			RepositoryTreeNode node = (RepositoryTreeNode) repo;
			// TODO equals implementation of Repository?
			if (repository.getDirectory().equals(
					((Repository) node.getObject()).getDirectory())) {
				for (Object child : cp.getChildren(node)) {
					RepositoryTreeNode childNode = (RepositoryTreeNode) child;
					if (childNode.getType() == RepositoryTreeNodeType.WORKINGDIR) {
						currentNode = childNode;
						break;
					}
				}
				break;
			}
		}

		IPath relPath = new Path(repoRelativePath);

		for (String segment : relPath.segments())
			for (Object child : cp.getChildren(currentNode)) {
				@SuppressWarnings("unchecked")
				RepositoryTreeNode<File> childNode = (RepositoryTreeNode<File>) child;
				if (childNode.getObject().getName().equals(segment)) {
					currentNode = childNode;
					break;
				}
			}

		return currentNode;
	}
}
