/*******************************************************************************
 * Copyright (c) 2000, 2017 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.team.core.synchronize;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.team.internal.core.Messages;
import org.eclipse.team.internal.core.TeamPlugin;
import org.eclipse.team.internal.core.subscribers.SyncInfoTreeChangeEvent;

/**
 * Provides addition API for accessing the <code>SyncInfo</code> in the set through
 * their resource's hierarchical relationships.
 * <p>
 * Events fired from a <code>SyncInfoTree</code> will be instances of <code>ISyncInfoTreeChangeEvent</code>.
 * </p>
 * @see SyncInfoSet
 * @since 3.0
 */
public class SyncInfoTree extends SyncInfoSet {

	protected Map<IPath, Set<IResource>> parents = Collections.synchronizedMap(new HashMap<>());

	/**
	 * Create an empty sync info tree.
	 */
	public SyncInfoTree() {
		super();
	}

	/**
	 * Create a sync info tree containing the given sync info elements.
	 *
	 * @param infos the sync info elements
	 */
	public SyncInfoTree(SyncInfo[] infos) {
		super(infos);
		for (int i = 0; i < infos.length; i++) {
			SyncInfo info = infos[i];
			IResource local = info.getLocal();
			addToParents(local, local);
		}
	}

	/**
	 * Return whether the given resource has any children in the sync set. The children
	 * could be either out-of-sync resources that are contained by the set or containers
	 * that are ancestors of out-of-sync resources contained by the set.
	 *
	 * @param resource the resource to check for children.
	 * @return <code>true</code> if the resource has children in the set.
	 */
	public synchronized boolean hasMembers(IResource resource) {
		if (resource.getType() == IResource.FILE) return false;
		IContainer parent = (IContainer)resource;
		if (parent.getType() == IResource.ROOT) return !isEmpty();
		IPath path = parent.getFullPath();
		Set allDescendants = parents.get(path);
		return (allDescendants != null && !allDescendants.isEmpty());
	}

	/**
	 * Return the <code>SyncInfo</code> for each out-of-sync resource in the subtree
	 * rooted at the given resource to the depth specified. The depth is one of:
	 * <ul>
	 * <li><code>IResource.DEPTH_ZERO</code>: the resource only,
	 * <li><code>IResource.DEPTH_ONE</code>: the resource or its direct children,
	 * <li><code>IResource.DEPTH_INFINITE</code>: the resource and all of it's
	 * descendants.
	 * </ul>
	 * If the given resource is out of sync, it will be included in the result.
	 * <p>
	 * The default implementation makes use of <code>getSyncInfo(IResource)</code>,
	 * <code>members(IResource)</code> and <code>getSyncInfos()</code> to provide
	 * the varying depths. Subclasses may override to optimize.
	 * </p>
	 *
	 * @param resource the root of the resource subtree
	 * @param depth    the depth of the subtree
	 * @return the <code>SyncInfo</code> for any out-of-sync resources
	 */
	public synchronized SyncInfo[] getSyncInfos(IResource resource, int depth) {
		if (depth == IResource.DEPTH_ZERO || resource.getType() == IResource.FILE) {
			SyncInfo info = getSyncInfo(resource);
			if (info == null) {
				return new SyncInfo[0];
			} else {
				return new SyncInfo[] { info };
			}
		}
		if (depth == IResource.DEPTH_ONE) {
			List<SyncInfo> result = new ArrayList<>();
			SyncInfo info = getSyncInfo(resource);
			if (info != null) {
				result.add(info);
			}
			IResource[] members = members(resource);
			for (int i = 0; i < members.length; i++) {
				IResource member = members[i];
				info = getSyncInfo(member);
				if (info != null) {
					result.add(info);
				}
			}
			return result.toArray(new SyncInfo[result.size()]);
		}
		// if it's the root then return all out of sync resources.
		if(resource.getType() == IResource.ROOT) {
			return getSyncInfos();
		}
		// for folders return all children deep.
		return internalGetDeepSyncInfo((IContainer)resource);
	}

	/*
	 * Return the <code>SyncInfo</code> for all out-of-sync resources in the
	 * set that are at or below the given resource in the resource hierarchy.
	 * @param resource the root resource
	 * @return the <code>SyncInfo</code> for all out-of-sync resources at or below the given resource
	 */
	private synchronized SyncInfo[] internalGetDeepSyncInfo(IContainer resource) {
		List<SyncInfo> infos = new ArrayList<>();
		IResource[] children = internalGetOutOfSyncDescendants(resource);
		for (int i = 0; i < children.length; i++) {
			IResource child = children[i];
			SyncInfo info = getSyncInfo(child);
			if(info != null) {
				infos.add(info);
			} else {
				TeamPlugin.log(IStatus.INFO, Messages.SyncInfoTree_0 + child.getFullPath(), null);
			}
		}
		return infos.toArray(new SyncInfo[infos.size()]);
	}

	@Override
	protected SyncInfoSetChangeEvent createEmptyChangeEvent() {
		return new SyncInfoTreeChangeEvent(this);
	}

	@Override
	public void add(SyncInfo info) {
		try {
			beginInput();
			boolean alreadyExists = getSyncInfo(info.getLocal()) != null;
			super.add(info);
			if(! alreadyExists) {
				IResource local = info.getLocal();
				addToParents(local, local);
			}
		} finally {
			endInput(null);
		}
	}

	@Override
	public void remove(IResource resource) {
		try {
			beginInput();
			super.remove(resource);
			removeFromParents(resource, resource);
		} finally {
			endInput(null);
		}

	}

	@Override
	public void clear() {
		try {
			beginInput();
			super.clear();
			synchronized(this) {
				parents.clear();
			}
		} finally {
			endInput(null);
		}
	}

	private synchronized boolean addToParents(IResource resource, IResource parent) {
		if (parent.getType() == IResource.ROOT) {
			return false;
		}
		// this flag is used to indicate if the parent was previously in the set
		boolean addedParent = false;
		if (parent.getType() == IResource.FILE) {
			// the file is new
			addedParent = true;
		} else {
			Set<IResource> children = parents.get(parent.getFullPath());
			if (children == null) {
				children = new HashSet<>();
				parents.put(parent.getFullPath(), children);
				// this is a new folder in the sync set
				addedParent = true;
			}
			children.add(resource);
		}
		// if the parent already existed and the resource is new, record it
		if (!addToParents(resource, parent.getParent()) && addedParent) {
			internalAddedSubtreeRoot(parent);
		}
		return addedParent;
	}

	private synchronized boolean removeFromParents(IResource resource, IResource parent) {
		if (parent.getType() == IResource.ROOT) {
			return false;
		}
		// this flag is used to indicate if the parent was removed from the set
		boolean removedParent = false;
		if (parent.getType() == IResource.FILE) {
			// the file will be removed
			removedParent = true;
		} else {
			Set<IResource> children = parents.get(parent.getFullPath());
			if (children != null) {
				children.remove(resource);
				if (children.isEmpty()) {
					parents.remove(parent.getFullPath());
					removedParent = true;
				}
			}
		}
		//	if the parent wasn't removed and the resource was, record it
		if (!removeFromParents(resource, parent.getParent()) && removedParent) {
			internalRemovedSubtreeRoot(parent);
		}
		return removedParent;
	}

	private void internalAddedSubtreeRoot(IResource parent) {
		((SyncInfoTreeChangeEvent)getChangeEvent()).addedSubtreeRoot(parent);
	}

	private void internalRemovedSubtreeRoot(IResource parent) {
		((SyncInfoTreeChangeEvent)getChangeEvent()).removedSubtreeRoot(parent);
	}

	/**
	 * Remove from this set the <code>SyncInfo</code> for the given resource and any
	 * of its descendants within the specified depth. The depth is one of:
	 * <ul>
	 * <li><code>IResource.DEPTH_ZERO</code>: the resource only,
	 * <li><code>IResource.DEPTH_ONE</code>: the resource or its direct children,
	 * <li><code>IResource.DEPTH_INFINITE</code>: the resource and all of it's
	 * descendants.
	 * </ul>
	 * 
	 * @param resource the root of the resource subtree
	 * @param depth    the depth of the subtree
	 */
	public void remove(IResource resource, int depth) {
		try {
			beginInput();
			if (getSyncInfo(resource) != null) {
				remove(resource);
			}
			if (depth == IResource.DEPTH_ZERO || resource.getType() == IResource.FILE) return;
			if (depth == IResource.DEPTH_ONE) {
				IResource[] members = members(resource);
				for (int i = 0; i < members.length; i++) {
					IResource member = members[i];
					if (getSyncInfo(member) != null) {
						remove(member);
					}
				}
			} else if (depth == IResource.DEPTH_INFINITE) {
				IResource [] toRemove = internalGetOutOfSyncDescendants((IContainer)resource);
				for (int i = 0; i < toRemove.length; i++) {
					remove(toRemove[i]);
				}
			}
		} finally {
			endInput(null);
		}
	}

	/**
	 * This is an internal method and is not intended to be invoked or
	 * overridden by clients.
	 */
	protected synchronized IResource[] internalGetOutOfSyncDescendants(IContainer resource) {
		// The parent map contains a set of all out-of-sync children
		Set<IResource> allChildren = parents.get(resource.getFullPath());
		if (allChildren == null) return new IResource[0];
		return allChildren.toArray(new IResource[allChildren.size()]);
	}

	private synchronized IResource[] internalMembers(IWorkspaceRoot root) {
		Set possibleChildren = parents.keySet();
		Set<IResource> children = new HashSet<>();
		for (Iterator it = possibleChildren.iterator(); it.hasNext();) {
			Object next = it.next();
			IResource element = root.findMember((IPath)next);
			if (element != null) {
				children.add(element.getProject());
			}
		}
		return children.toArray(new IResource[children.size()]);
	}

	/**
	 * Return the immediate children of the given resource who are either out-of-sync
	 * or contain out-of-sync resources.
	 *
	 * @param resource the parent resource
	 * @return the children of the resource that are either out-of-sync or are ancestors of
	 * out-of-sync resources contained in the set
	 */
	public synchronized IResource[] members(IResource resource) {
		if (resource.getType() == IResource.FILE) return new IResource[0];
		IContainer parent = (IContainer)resource;
		if (parent.getType() == IResource.ROOT) return internalMembers((IWorkspaceRoot)parent);
		// OPTIMIZE: could be optimized so that we don't traverse all the deep
		// children to find the immediate ones.
		Set<IResource> children = new HashSet<>();
		IPath path = parent.getFullPath();
		Set possibleChildren = parents.get(path);
		if(possibleChildren != null) {
			for (Iterator it = possibleChildren.iterator(); it.hasNext();) {
				Object next = it.next();
				IResource element = (IResource)next;
				IPath childPath = element.getFullPath();
				IResource modelObject = null;
				if(childPath.segmentCount() == (path.segmentCount() +  1)) {
					modelObject = element;

				} else if (childPath.segmentCount() > path.segmentCount()) {
					IContainer childFolder = parent.getFolder(new Path(null, childPath.segment(path.segmentCount())));
					modelObject = childFolder;
				}
				if (modelObject != null) {
					children.add(modelObject);
				}
			}
		}
		return children.toArray(new IResource[children.size()]);
	}

}
