/*******************************************************************************
 * Copyright (C) 2011, 2013 Dariusz Luksza <dariusz@luksza.org> 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
 *******************************************************************************/
package org.eclipse.egit.core.synchronize;

import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.egit.core.Activator;
import org.eclipse.egit.core.synchronize.dto.GitSynchronizeData;
import org.eclipse.egit.core.synchronize.dto.GitSynchronizeDataSet;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.dircache.DirCacheIterator;
import org.eclipse.jgit.lib.AbbreviatedObjectId;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.treewalk.EmptyTreeIterator;
import org.eclipse.jgit.treewalk.FileTreeIterator;
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;

/**
 * Simple and thin tree cache for git meta data about resources in repository.
 */
class GitSyncCache {

	private final Map<File, GitSyncObjectCache> cache;

	public static GitSyncCache getAllData(GitSynchronizeDataSet gsds,
			IProgressMonitor monitor) {
		Map<GitSynchronizeData, Collection<String>> updateRequests = new HashMap<GitSynchronizeData, Collection<String>>();
		for (GitSynchronizeData data : gsds)
			updateRequests.put(data, Collections.<String> emptyList());
		return getAllData(updateRequests, monitor);
	}

	public static GitSyncCache getAllData(
			Map<GitSynchronizeData, Collection<String>> updateRequests,
			IProgressMonitor monitor) {
		GitSyncCache cache = new GitSyncCache();
		mergeAllDataIntoCache(updateRequests, monitor, cache);
		return cache;
	}

	public static void mergeAllDataIntoCache(
			Map<GitSynchronizeData, Collection<String>> updateRequests,
			IProgressMonitor monitor, GitSyncCache cache) {
		SubMonitor m = SubMonitor.convert(monitor, updateRequests.size());

		for (Entry<GitSynchronizeData, Collection<String>> entry : updateRequests
				.entrySet()) {
			Collection<String> paths = entry.getValue();
			GitSyncCache partialCache = getAllData(entry.getKey(), paths);
			cache.merge(partialCache, new HashSet<String>(paths));
			m.worked(1);
		}

		m.done();
	}

	private static GitSyncCache getAllData(GitSynchronizeData gsd,
			Collection<String> paths) {
		GitSyncCache cache = new GitSyncCache();
		TreeFilter filter = paths.isEmpty() ? null : createPathFilter(paths);

		Repository repo = gsd.getRepository();
		ObjectId baseTree = getTree(gsd.getSrcRevCommit());
		ObjectId remoteTree = getTree(gsd.getDstRevCommit());
		GitSyncObjectCache repoCache = cache.put(repo, baseTree, remoteTree);

		TreeFilter gsdFilter = gsd.getPathFilter();
		if (filter == null)
			loadDataFromGit(gsd, gsdFilter, repoCache);
		else if (gsdFilter == null)
			loadDataFromGit(gsd, filter, repoCache);
		else
			loadDataFromGit(gsd, AndTreeFilter.create(filter, gsdFilter),
					repoCache);
		return cache;
	}

	private static TreeFilter createPathFilter(Collection<String> paths) {
		// do not use PathFilterGroup to create the filter, see bug 362430
		List<TreeFilter> filters = new ArrayList<TreeFilter>(paths.size());
		for (String path : paths) {
			if (path.length() == 0)
				return null;
			filters.add(PathFilter.create(path));
		}
		if (filters.size() == 1)
			return filters.get(0);
		return OrTreeFilter.create(filters);
	}

	private static void loadDataFromGit(GitSynchronizeData gsd,
			TreeFilter filter, GitSyncObjectCache repoCache) {
		Repository repo = gsd.getRepository();
		TreeWalk tw = new TreeWalk(repo);
		if (filter != null)
			tw.setFilter(filter);

		try {
			// setup local tree
			FileTreeIterator fti = null;
			if (gsd.shouldIncludeLocal()) {
				fti = new FileTreeIterator(repo);
				tw.addTree(fti);
				if (filter != null)
					tw.setFilter(AndTreeFilter.create(filter,
							new NotIgnoredFilter(0)));
				else
					tw.setFilter(new NotIgnoredFilter(0));
			} else if (gsd.getSrcRevCommit() != null)
				tw.addTree(gsd.getSrcRevCommit().getTree());
			else
				tw.addTree(new EmptyTreeIterator());

			// setup base tree
			if (gsd.getCommonAncestorRev() != null)
				tw.addTree(gsd.getCommonAncestorRev().getTree());
			else
				tw.addTree(new EmptyTreeIterator());

			// setup remote tree
			if (gsd.getDstRevCommit() != null)
				tw.addTree(gsd.getDstRevCommit().getTree());
			else
				tw.addTree(new EmptyTreeIterator());

			DirCacheIterator dci = null;
			if (fti != null) {
				dci = new DirCacheIterator(DirCache.read(repo));
				tw.addTree(dci);
				fti.setDirCacheIterator(tw, 3);
			}
			List<ThreeWayDiffEntry> diffEntrys = ThreeWayDiffEntry.scan(tw);
			tw.release();

			for (ThreeWayDiffEntry diffEntry : diffEntrys)
				repoCache.addMember(diffEntry);
		} catch (Exception e) {
			Activator.logError(e.getMessage(), e);
		}
	}

	private static ObjectId getTree(RevCommit commit) {
		if (commit != null)
			return commit.getTree();
		else {
			return ObjectId.zeroId();
		}
	}

	private GitSyncCache() {
		cache = new HashMap<File, GitSyncObjectCache>();
	}

	/**
	 * @param repo
	 *            instance of {@link Repository} for with mapping should be
	 *            obtained
	 * @return instance of {@link GitSyncObjectCache} connected associated with
	 *         given repository or {@code null} when such mapping wasn't found
	 */
	public GitSyncObjectCache get(Repository repo) {
		return cache.get(repo.getDirectory());
	}

	public void merge(GitSyncCache other, Set<String> filterPaths) {
		for (Entry<File, GitSyncObjectCache> entry : other.cache.entrySet()) {
			File key = entry.getKey();
			if (cache.containsKey(key))
				cache.get(key).merge(entry.getValue(), filterPaths);
			else
				cache.put(key, entry.getValue());
		}
	}

	@Override
	public String toString() {
		StringBuilder builder = new StringBuilder();
		for (Entry<File, GitSyncObjectCache> entry : cache.entrySet())
			builder.append(entry.getKey().getPath())
					.append(": ").append(entry.getValue()); //$NON-NLS-1$

		return builder.toString();
	}

	/**
	 * Create mapping for given repository and returns object associated with
	 * this repository. Any other mapping will be overwritten.
	 *
	 * @param repo
	 * @param remoteTree
	 * @param baseTree
	 * @return new mapping object associated with given {@link Repository}
	 */
	private GitSyncObjectCache put(Repository repo, ObjectId baseTree,
			ObjectId remoteTree) {
		ThreeWayDiffEntry entry = new ThreeWayDiffEntry();
		entry.baseId = AbbreviatedObjectId.fromObjectId(baseTree);
		entry.remoteId = AbbreviatedObjectId.fromObjectId(remoteTree);
		GitSyncObjectCache objectCache = new GitSyncObjectCache("", entry); //$NON-NLS-1$
		cache.put(repo.getDirectory(), objectCache);

		return objectCache;
	}

}
