/*******************************************************************************
 * Copyright (C) 2010, Dariusz Luksza <dariusz@luksza.org>
 * Copyright (C) 2012, Robin Stocker <robin@nibor.org>
 *
 * 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.core;

import java.io.IOException;
import java.util.Collection;
import java.util.List;

import org.eclipse.core.runtime.Assert;
import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.RepositoryState;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.revwalk.filter.RevFilter;
import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
import org.eclipse.jgit.treewalk.filter.PathFilter;
import org.eclipse.jgit.treewalk.filter.TreeFilter;

/**
 * Utility class for obtaining Rev object instances.
 */
public class RevUtils {

	private RevUtils() {
		// non instanciable utility class
	}

	/**
	 * Finds and returns instance of common ancestor commit for given to
	 * commit's
	 *
	 * @param repo repository in which common ancestor should be searched, cannot be null
	 * @param commit1 left commit id, cannot be null
	 * @param commit2 right commit id, cannot be null
	 * @return common ancestor for commit1 and commit2 parameters
	 * @throws IOException
	 */
	public static RevCommit getCommonAncestor(Repository repo,
			AnyObjectId commit1, AnyObjectId commit2) throws IOException {
		Assert.isNotNull(repo);
		Assert.isNotNull(commit1);
		Assert.isNotNull(commit2);

		RevWalk rw = new RevWalk(repo);
		try {
			rw.setRetainBody(false);
			rw.setRevFilter(RevFilter.MERGE_BASE);

			RevCommit srcRev = rw.lookupCommit(commit1);
			RevCommit dstRev = rw.lookupCommit(commit2);

			rw.markStart(dstRev);
			rw.markStart(srcRev);

			RevCommit result = rw.next();
			if (result != null) {
				rw.parseBody(result);
				return result;
			} else {
				return null;
			}
		} finally {
			rw.release();
		}
	}

	/**
	 * @param repository
	 * @param path
	 *            repository-relative path of file with conflicts
	 * @return an object with the interesting commits for this path
	 * @throws IOException
	 */
	public static ConflictCommits getConflictCommits(Repository repository,
			String path) throws IOException {
		RevWalk walk = new RevWalk(repository);
		try {
			RevCommit ourCommit;
			RevCommit theirCommit = null;
			walk.setTreeFilter(AndTreeFilter.create(PathFilter.create(path),
					TreeFilter.ANY_DIFF));

			RevCommit head = walk.parseCommit(repository
					.resolve(Constants.HEAD));
			walk.markStart(head);

			ourCommit = walk.next();

			RepositoryState state = repository.getRepositoryState();
			if (state == RepositoryState.REBASING
					|| state == RepositoryState.CHERRY_PICKING) {
				ObjectId cherryPickHead = repository.readCherryPickHead();
				if (cherryPickHead != null) {
					RevCommit cherryPickCommit = walk.parseCommit(cherryPickHead);
					theirCommit = cherryPickCommit;
				}
			} else if (state == RepositoryState.MERGING) {
				List<ObjectId> mergeHeads = repository.readMergeHeads();
				if (mergeHeads.size() == 1) {
					ObjectId mergeHead = mergeHeads.get(0);
					RevCommit mergeCommit = walk.parseCommit(mergeHead);
					walk.reset();
					walk.markStart(mergeCommit);
					theirCommit = walk.next();
				}
			}

			return new ConflictCommits(ourCommit, theirCommit);
		} finally {
			walk.release();
		}
	}

	/**
	 * Check if commit is contained in any of the passed refs.
	 *
	 * @param repo
	 *            the repo the commit is in
	 * @param commitId
	 *            the commit ID to search for
	 * @param refs
	 *            the refs to check
	 * @return true if the commit is contained, false otherwise
	 * @throws IOException
	 */
	public static boolean isContainedInAnyRef(Repository repo,
			ObjectId commitId, Collection<Ref> refs) throws IOException {
		// It's likely that we don't have to walk commits at all, so
		// check refs directly first.
		for (Ref ref : refs)
			if (commitId.equals(ref.getObjectId()))
				return true;

		final int skew = 24 * 60 * 60; // one day clock skew

		RevWalk walk = new RevWalk(repo);
		try {
			RevCommit commit = walk.parseCommit(commitId);
			for (Ref ref : refs) {
				RevCommit refCommit = walk.parseCommit(ref.getObjectId());

				// if commit is in the ref branch, then the tip of ref should be
				// newer than the commit we are looking for. Allow for a large
				// clock skew.
				if (refCommit.getCommitTime() + skew < commit.getCommitTime())
					continue;

				boolean contained = walk.isMergedInto(commit, refCommit);
				if (contained)
					return true;
			}
		} finally {
			walk.dispose();
		}
		return false;
	}

	/**
	 * The interesting commits from ours/theirs for a file in case of a
	 * conflict.
	 */
	public static class ConflictCommits {
		private final RevCommit ourCommit;
		private final RevCommit theirCommit;

		private ConflictCommits(RevCommit ourCommit, RevCommit theirCommit) {
			this.ourCommit = ourCommit;
			this.theirCommit = theirCommit;
		}

		/**
		 * @return the commit from "ours" that last modified a file, or
		 *         {@code null} if none found
		 */
		public RevCommit getOurCommit() {
			return ourCommit;
		}

		/**
		 * @return the commit from "theirs" that last modified a file, or
		 *         {@code null} if none found
		 */
		public RevCommit getTheirCommit() {
			return theirCommit;
		}
	}
}
