/*******************************************************************************
 * Copyright (C) 2007, Dave Watson <dwatson@mimvista.com>
 * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
 * Copyright (C) 2006, Shawn O. Pearce <spearce@spearce.org>
 * Copyright (C) 2010, Mathias Kinzler <mathias.kinzler@sap.com>
 * Copyright (C) 2011, Dariusz Luksza <dariusz@luksza.org>
 * Copyright (C) 2012, 2013 Robin Stocker <robin@nibor.org>
 * Copyright (C) 2012, 2013 François Rey <eclipse.org_@_francois_._rey_._name>
 * Copyright (C) 2013 Laurent Goubet <laurent.goubet@obeo.fr>
 *
 * 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
 *******************************************************************************/
package org.eclipse.egit.ui.internal.actions;

import java.io.IOException;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import org.eclipse.core.commands.AbstractHandler;
import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.expressions.IEvaluationContext;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.resources.mapping.ResourceMapping;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.PlatformObject;
import org.eclipse.egit.core.AdapterUtils;
import org.eclipse.egit.core.internal.CompareCoreUtils;
import org.eclipse.egit.core.project.RepositoryMapping;
import org.eclipse.egit.ui.internal.CommonUtils;
import org.eclipse.egit.ui.internal.selection.SelectionUtils;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jgit.diff.DiffConfig;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.FollowFilter;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevSort;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchWindowActionDelegate;
import org.eclipse.ui.handlers.HandlerUtil;

/**
 * A helper class for Team Actions on Git controlled projects
 */
abstract class RepositoryActionHandler extends AbstractHandler {

	private IEvaluationContext evaluationContext;

	private IStructuredSelection mySelection;

	/**
	 * Set the selection when used by {@link RepositoryAction} as
	 * {@link IWorkbenchWindowActionDelegate}
	 *
	 * @param selection
	 *            the new selection
	 */
	public void setSelection(ISelection selection) {
		mySelection = SelectionUtils.getStructuredSelection(selection);
	}

	/**
	 * Retrieve the list of projects that contains the given resources. All
	 * resources must actually map to a project shared with egit, otherwise an
	 * empty array is returned. In case of a linked resource, the project
	 * returned is the one that contains the link target and is shared with
	 * egit, if any, otherwise an empty array is also returned.
	 *
	 * @param selection
	 * @return the projects hosting the selected resources
	 */
	private IProject[] getProjectsForSelectedResources(
			IStructuredSelection selection) {
		Set<IProject> ret = new LinkedHashSet<IProject>();
		for (IResource resource : (IResource[]) getSelectedAdaptables(
				selection, IResource.class)) {
			RepositoryMapping mapping = RepositoryMapping.getMapping(resource);
			if (mapping != null && (mapping.getContainer() instanceof IProject))
				ret.add((IProject) mapping.getContainer());
			else
				return new IProject[0];
		}
		ret.addAll(extractProjectsFromMappings(selection));

		return ret.toArray(new IProject[ret.size()]);
	}

	private Set<IProject> extractProjectsFromMappings(
			IStructuredSelection selection) {
		Set<IProject> ret = new LinkedHashSet<IProject>();
		for (ResourceMapping mapping : (ResourceMapping[]) getSelectedAdaptables(
				selection, ResourceMapping.class)) {
			IProject[] mappedProjects = mapping.getProjects();
			if (mappedProjects != null && mappedProjects.length != 0) {
				// Some mappings (WorkingSetResourceMapping) return the projects
				// in unpredictable order. Sort them like the navigator to
				// correspond to the order the user usually sees.
				List<IProject> projects = new ArrayList<IProject>(
						Arrays.asList(mappedProjects));
				Collections
						.sort(projects, CommonUtils.RESOURCE_NAME_COMPARATOR);
				ret.addAll(projects);
			}
		}
		return ret;
	}

	/**
	 * Retrieve the list of projects that contains the selected resources. All
	 * resources must actually map to a project shared with egit, otherwise an
	 * empty array is returned. In case of a linked resource, the project
	 * returned is the one that contains the link target and is shared with
	 * egit, if any, otherwise an empty array is also returned.
	 *
	 * @param event
	 * @return the projects hosting the selected resources
	 * @throws ExecutionException
	 */
	protected IProject[] getProjectsForSelectedResources(ExecutionEvent event)
			throws ExecutionException {
		IStructuredSelection selection = getSelection(event);
		return getProjectsForSelectedResources(selection);
	}

	/**
	 * Retrieve the list of projects that contains the selected resources. All
	 * resources must actually map to a project shared with egit, otherwise an
	 * empty array is returned. In case of a linked resource, the project
	 * returned is the one that contains the link target and is shared with
	 * egit, if any, otherwise an empty array is also returned.
	 *
	 * @return the projects hosting the selected resources
	 */
	protected IProject[] getProjectsForSelectedResources() {
		IStructuredSelection selection = getSelection();
		return getProjectsForSelectedResources(selection);
	}

	/**
	 * @param projects
	 *            a list of projects
	 * @return the repositories that projects map to if all projects are mapped
	 */
	protected Repository[] getRepositoriesFor(final IProject[] projects) {
		Set<Repository> ret = new LinkedHashSet<Repository>();
		for (IProject project : projects) {
			RepositoryMapping repositoryMapping = RepositoryMapping
					.getMapping(project);
			if (repositoryMapping == null)
				return new Repository[0];
			ret.add(repositoryMapping.getRepository());
		}
		return ret.toArray(new Repository[ret.size()]);
	}

	/**
	 * List the projects with selected resources, if all projects are connected
	 * to a Git repository.
	 *
	 * @return the tracked projects affected by the current resource selection
	 */
	protected IProject[] getProjectsInRepositoryOfSelectedResources() {
		IStructuredSelection selection = getSelection();
		return getProjectsInRepositoryOfSelectedResources(selection);
	}

	/**
	 * List the projects with selected resources, if all projects are connected
	 * to a Git repository.
	 *
	 * @param selection
	 *
	 * @return the tracked projects affected by the current resource selection
	 */
	private IProject[] getProjectsInRepositoryOfSelectedResources(
			IStructuredSelection selection) {
		Set<IProject> ret = new LinkedHashSet<IProject>();
		Repository[] repositories = getRepositoriesFor(getProjectsForSelectedResources(selection));
		final IProject[] projects = ResourcesPlugin.getWorkspace().getRoot()
				.getProjects();
		for (IProject project : projects) {
			RepositoryMapping mapping = RepositoryMapping.getMapping(project);
			for (Repository repository : repositories)
				if (mapping != null && mapping.getRepository() == repository) {
					ret.add(project);
					break;
				}
		}
		return ret.toArray(new IProject[ret.size()]);
	}

	/**
	 * Figure out which repository to use. All selected resources must map to
	 * the same Git repository.
	 *
	 * @param warn
	 *            Put up a message dialog to warn why a resource was not
	 *            selected
	 * @param event
	 * @return repository for current project, or null
	 * @throws ExecutionException
	 */
	protected Repository getRepository(boolean warn, ExecutionEvent event)
			throws ExecutionException {
		IStructuredSelection selection = getSelection(event);
		if (warn) {
			Shell shell = getShell(event);
			return SelectionUtils.getRepositoryOrWarn(selection, shell);
		} else {
			return SelectionUtils.getRepository(selection);
		}
	}

	/**
	 * Figure out which repository to use. All selected resources must map to
	 * the same Git repository.
	 *
	 * @return repository for current project, or null
	 */
	protected Repository getRepository() {
		IStructuredSelection selection = getSelection();
		return SelectionUtils.getRepository(selection);
	}

	/**
	 * Figure out which repositories to use. All selected resources must map to
	 * a Git repository.
	 *
	 * @param event
	 *
	 * @return repositories for selection, or an empty array
	 * @throws ExecutionException
	 */
	protected Repository[] getRepositories(ExecutionEvent event)
			throws ExecutionException {
		IProject[] selectedProjects = getProjectsForSelectedResources(event);
		if (selectedProjects.length > 0)
			return getRepositoriesFor(selectedProjects);
		IStructuredSelection selection = getSelection(event);
		if (!selection.isEmpty()) {
			Set<Repository> repos = new LinkedHashSet<Repository>();
			for (Object o : selection.toArray())
				if (o instanceof Repository)
					repos.add((Repository) o);
				else if (o instanceof PlatformObject) {
					Repository repo = (Repository) ((PlatformObject) o)
							.getAdapter(Repository.class);
					if (repo != null)
						repos.add(repo);
				}
			return repos.toArray(new Repository[repos.size()]);
		}
		return new Repository[0];
	}

	/**
	 * Get the currently selected repositories. All selected projects must map
	 * to a repository.
	 *
	 * @return repositories for selection, or an empty array
	 */
	protected Repository[] getRepositories() {
		IProject[] selectedProjects = getProjectsForSelectedResources();
		if (selectedProjects.length > 0)
			return getRepositoriesFor(selectedProjects);
		IStructuredSelection selection = getSelection();
		if (!selection.isEmpty()) {
			Set<Repository> repos = new LinkedHashSet<Repository>();
			for (Object o : selection.toArray())
				if (o instanceof Repository)
					repos.add((Repository) o);
				else if (o instanceof PlatformObject) {
					Repository repo = (Repository) ((PlatformObject) o)
							.getAdapter(Repository.class);
					if (repo != null)
						repos.add(repo);
				}
			return repos.toArray(new Repository[repos.size()]);
		}
		return new Repository[0];
	}

	/**
	 * @param event
	 *            the execution event, must not be null
	 * @return the current selection
	 * @throws ExecutionException
	 *             if the selection can't be determined
	 */
	protected static IStructuredSelection getSelection(ExecutionEvent event)
			throws ExecutionException {
		if (event == null)
			throw new IllegalArgumentException("event must not be NULL"); //$NON-NLS-1$
		Object context = event.getApplicationContext();
		if (context instanceof IEvaluationContext)
			return SelectionUtils.getSelection((IEvaluationContext) context);
		return StructuredSelection.EMPTY;
	}

	/**
	 * @return the current selection
	 */
	protected IStructuredSelection getSelection() {
		// if the selection was set explicitly, use it
		if (mySelection != null)
			return mySelection;
		return SelectionUtils.getSelection(evaluationContext);
	}

	public void setEnabled(Object evaluationContext) {
		this.evaluationContext = (IEvaluationContext) evaluationContext;
	}

	/**
	 * Creates an array of the given class type containing all the objects in
	 * the selection that adapt to the given class.
	 *
	 * @param selection
	 * @param c
	 * @return the selected adaptables
	 */
	@SuppressWarnings("unchecked")
	private Object[] getSelectedAdaptables(ISelection selection, Class c) {
		ArrayList result = null;
		if (selection != null && !selection.isEmpty()) {
			result = new ArrayList();
			Iterator elements = ((IStructuredSelection) selection).iterator();
			while (elements.hasNext()) {
				Object adapter = AdapterUtils.adapt(elements.next(), c);
				if (c.isInstance(adapter))
					result.add(adapter);
			}
		}
		if (result != null && !result.isEmpty())
			return result
					.toArray((Object[]) Array.newInstance(c, result.size()));
		return (Object[]) Array.newInstance(c, 0);
	}

	/**
	 * @param event
	 * @return the resources in the selection
	 * @throws ExecutionException
	 */
	protected IResource[] getSelectedResources(ExecutionEvent event)
			throws ExecutionException {
		IStructuredSelection selection = getSelection(event);
		return SelectionUtils.getSelectedResources(selection);
	}

	protected IPath[] getSelectedLocations(ExecutionEvent event)
			throws ExecutionException {
		IStructuredSelection selection = getSelection(event);
		return SelectionUtils.getSelectedLocations(selection);
	}

	/**
	 * @return the resources in the selection
	 */
	protected IResource[] getSelectedResources() {
		IStructuredSelection selection = getSelection();
		return SelectionUtils.getSelectedResources(selection);
	}

	/**
	 * @return the locations in the selection
	 */
	protected IPath[] getSelectedLocations() {
		IStructuredSelection selection = getSelection();
		return SelectionUtils.getSelectedLocations(selection);
	}

	/**
	 * @return true if all selected items map to the same repository, false otherwise.
	 */
	protected boolean selectionMapsToSingleRepository() {
		return getRepository() != null;
	}

	/**
	 * @param event
	 * @return the shell
	 * @throws ExecutionException
	 */
	protected Shell getShell(ExecutionEvent event) throws ExecutionException {
		return HandlerUtil.getActiveShellChecked(event);
	}

	/**
	 * @param event
	 * @return the page
	 * @throws ExecutionException
	 */
	protected IWorkbenchPage getPartPage(ExecutionEvent event)
			throws ExecutionException {
		return getPart(event).getSite().getPage();
	}

	/**
	 * @param event
	 * @return the page
	 * @throws ExecutionException
	 */
	protected IWorkbenchPart getPart(ExecutionEvent event)
			throws ExecutionException {
		return HandlerUtil.getActivePartChecked(event);
	}


	/**
	 *
	 * @param repository
	 *            the repository to check
	 * @return {@code true} when {@link Constants#HEAD} can be resolved,
	 *         {@code false} otherwise
	 */
	protected boolean containsHead(Repository repository) {
		try {
			return repository != null ? repository.resolve(Constants.HEAD) != null
					: false;
		} catch (Exception e) {
			// do nothing
		}

		return false;
	}

	protected boolean isLocalBranchCheckedout(Repository repository) {
		try {
			return repository.getFullBranch().startsWith(Constants.R_HEADS);
		} catch (Exception e) {
			// do nothing
		}

		return false;
	}

	protected String getPreviousPath(Repository repository,
			ObjectReader reader, RevCommit headCommit,
			RevCommit previousCommit, String path) throws IOException {
		DiffEntry diffEntry = CompareCoreUtils.getChangeDiffEntry(repository, path,
				headCommit, previousCommit, reader);
		if (diffEntry != null)
			return diffEntry.getOldPath();
		else
			return path;
	}

	protected List<PreviousCommit> findPreviousCommits() throws IOException {
		List<PreviousCommit> result = new ArrayList<PreviousCommit>();
		Repository repository = getRepository();
		IResource resource = getSelectedResources()[0];
		String path = RepositoryMapping.getMapping(resource.getProject())
				.getRepoRelativePath(resource);
		RevWalk rw = new RevWalk(repository);
		rw.sort(RevSort.COMMIT_TIME_DESC, true);
		rw.sort(RevSort.BOUNDARY, true);
		try {
			if (path.length() > 0) {
				DiffConfig diffConfig = repository.getConfig().get(
						DiffConfig.KEY);
				FollowFilter filter = FollowFilter.create(path, diffConfig);
				rw.setTreeFilter(filter);
			}

			RevCommit headCommit = rw.parseCommit(repository.getRef(
					Constants.HEAD).getObjectId());
			rw.markStart(headCommit);
			headCommit = rw.next();

			if (headCommit == null)
				return result;
			List<RevCommit> directParents = Arrays.asList(headCommit
					.getParents());

			RevCommit previousCommit = rw.next();
			while (previousCommit != null && result.size() < directParents.size()) {
				if (directParents.contains(previousCommit)) {
					String previousPath = getPreviousPath(repository,
							rw.getObjectReader(), headCommit, previousCommit,
							path);
					result.add(new PreviousCommit(previousCommit, previousPath));
				}
				previousCommit = rw.next();
			}
		} finally {
			rw.dispose();
		}
		return result;
	}

	// keep track of the path of an ancestor (for following renames)
	protected static final class PreviousCommit {
		final RevCommit commit;
		final String path;
		PreviousCommit(final RevCommit commit, final String path) {
			this.commit = commit;
			this.path = path;
		}
	}

	/**
	 * By default egit operates only on resources that map to a project shared
	 * with egit. For linked resources the project that contains the link
	 * target, if any, must be shared with egit.
	 *
	 * @return the projects hosting the selected resources
	 */
	@Override
	public boolean isEnabled() {
		return getProjectsForSelectedResources().length > 0;
	}
}
