/*******************************************************************************
 * 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.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.eclipse.egit.core.internal.CoreText;
import org.eclipse.egit.core.synchronize.ThreeWayDiffEntry.ChangeType;
import org.eclipse.osgi.util.NLS;

/**
 * Thin cache object. It contains list of object members, object name and
 * {@link ThreeWayDiffEntry} data.
 */
class GitSyncObjectCache {

	private final String name;

	private ThreeWayDiffEntry diffEntry;

	private Map<String, GitSyncObjectCache> members;

	/**
	 * Creates node and leaf element
	 *
	 * @param name
	 *            entry name
	 * @param diffEntry
	 *            entry meta data
	 */
	GitSyncObjectCache(String name, ThreeWayDiffEntry diffEntry) {
		this.name = name;
		this.diffEntry = diffEntry;
	}

	/**
	 * @return name of this object
	 */
	public String getName() {
		return name;
	}

	/**
	 *
	 * @return entry meta data
	 */
	public ThreeWayDiffEntry getDiffEntry() {
		return diffEntry;
	}

	/**
	 * Store given {@code entry} in cache. It assumes that parent of
	 * {@code entry} is already in cache, if not {@link RuntimeException} will
	 * be thrown.
	 *
	 * @param entry
	 *            that should be stored in cache
	 * @throws RuntimeException
	 *             when cannot find parent of given {@code entry} in cache
	 */
	public void addMember(ThreeWayDiffEntry entry) {
		String memberPath = entry.getPath();

		if (members == null)
			members = new HashMap<String, GitSyncObjectCache>();

		int start = -1;
		Map<String, GitSyncObjectCache> parent = members;
		int separatorIdx = memberPath.indexOf("/"); //$NON-NLS-1$
		while (separatorIdx > 0) {
			String key = memberPath.substring(start + 1, separatorIdx);
			GitSyncObjectCache cacheObject = parent.get(key);
			if (cacheObject == null)
				throw new RuntimeException(NLS.bind(
						CoreText.GitSyncObjectCache_noData, key));

			start = separatorIdx;
			separatorIdx = memberPath.indexOf("/", separatorIdx + 1); //$NON-NLS-1$
			if (cacheObject.members == null)
				cacheObject.members = new HashMap<String, GitSyncObjectCache>();

			parent = cacheObject.members;
		}

		String newName;
		if (start > 0)
			newName = memberPath.substring(start + 1);
		else
			newName = memberPath;

		GitSyncObjectCache obj = new GitSyncObjectCache(newName, entry);
		parent.put(newName, obj);
	}

	/**
	 * @param childPath
	 *            repository relative path of entry that should be obtained
	 * @return cached object, or {@code null} when cache doen't contain object
	 *         for given path
	 */
	public GitSyncObjectCache get(String childPath) {
		if (childPath.length() == 0)
			return this;
		if (childPath
				.substring(childPath.lastIndexOf("/") + 1, childPath.length()).equals(name)) //$NON-NLS-1$
			return this;
		if (members == null)
			return null;

		int start = -1;
		Map<String, GitSyncObjectCache> parent = members;
		int separatorIdx = childPath.indexOf("/"); //$NON-NLS-1$
		while (separatorIdx > 0) {
			String key = childPath.substring(start + 1, separatorIdx);

			GitSyncObjectCache childObject = parent.get(key);
			if (childObject == null)
				return null;

			start = separatorIdx;
			separatorIdx = childPath.indexOf("/", separatorIdx + 1); //$NON-NLS-1$
			parent = childObject.members;
			if (parent == null)
				return null;
		}

		return parent.get(childPath.subSequence(
				childPath.lastIndexOf("/") + 1, childPath.length())); //$NON-NLS-1$
	}

	/**
	 * @return number of cached members
	 */
	public int membersCount() {
		return members != null ? members.size() : 0;
	}

	/**
	 * @return list of all cached members or {@code null} when there this object
	 *         doesn't contain members
	 */
	public Collection<GitSyncObjectCache> members() {
		return members != null ? members.values() : null;
	}

	@Override
	public String toString() {
		StringBuilder builder = new StringBuilder();
		builder.append("entry: ").append(diffEntry).append("\n"); //$NON-NLS-1$ //$NON-NLS-2$
		if (members != null) {
			builder.append("members: "); //$NON-NLS-1$
			for (GitSyncObjectCache obj : members.values())
				builder.append(obj.toString()).append("\n"); //$NON-NLS-1$
		}

		return builder.toString();
	}

	void merge(GitSyncObjectCache other, Set<String> filterPaths) {
		if (other.members != null) {
			if (members == null)
				members = new HashMap<String, GitSyncObjectCache>();

			for (Entry<String, GitSyncObjectCache> entry : members.entrySet()) {
				String key = entry.getKey();
				if (!other.members.containsKey(key)) {
					GitSyncObjectCache obj = entry.getValue();
					String entryPath = obj.getDiffEntry().getPath();
					if (containsPathOrParent(filterPaths, entryPath))
						obj.getDiffEntry().changeType = ChangeType.IN_SYNC;
				}
			}

			for (Entry<String, GitSyncObjectCache> entry : other.members
					.entrySet()) {
				String key = entry.getKey();
				GitSyncObjectCache obj = entry.getValue();
				if (members.containsKey(key)) {
					members.get(key).merge(obj, filterPaths);
				} else {
					members.put(key, obj);
				}
			}
		} else if (members != null) {
			for (GitSyncObjectCache obj : members.values()) {
				String entryPath = obj.getDiffEntry().getPath();
				if (containsPathOrParent(filterPaths, entryPath))
					obj.getDiffEntry().changeType = ChangeType.IN_SYNC;
			}
		} else {
			// we are on a leaf entry, use the newer diff entry (need to update
			// changeType, direction and other fields)
			diffEntry = other.diffEntry;
		}
	}

	private static boolean containsPathOrParent(Set<String> filterPaths,
			String pathToTest) {
		if (filterPaths.contains(pathToTest))
			return true;
		// "" is the root directory of the repository, so it matches everything
		if (filterPaths.contains("")) //$NON-NLS-1$
			return true;
		String path = pathToTest;
		int lastSlash = path.lastIndexOf('/');
		while (lastSlash != -1) {
			path = path.substring(0, lastSlash);
			if (filterPaths.contains(path))
				return true;
			lastSlash = path.lastIndexOf('/');
		}
		return false;
	}
}
