/*******************************************************************************
 * Copyright (c) 2014, 2016 Red Hat Inc., 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:
 *     Mickael Istria (Red Hat Inc.) - initial API and implementation
 *     Ivica Loncar - Projects open from inside parent inherit working sets
 ******************************************************************************/
package org.eclipse.ui.internal.navigator.resources.nested;

import java.util.HashSet;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;

import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IPath;

/**
 * @since 3.3
 *
 */
public class NestedProjectManager {

	private static NestedProjectManager INSTANCE = new NestedProjectManager();

	/**
	 * This structure sorts project by location, so we can assume that:
	 * <ul>
	 * <li>If a project is nested under another, then the parent project is the
	 * previous item in the map. So getting the parent is just about checking
	 * the previous project for parency.</li>
	 * <li>the children project of a project (with any depth not only direct
	 * ones) are the immediately following items in the map.</li>
	 * </ul>
	 */
	private SortedMap<IPath, IProject> locationsToProjects = new TreeMap<>(new PathComparator());

	private NestedProjectManager() {
		refreshProjectsList();
		ResourcesPlugin.getWorkspace().addResourceChangeListener(event -> {
			IResourceDelta delta = event.getDelta();
			IResource resource = null;
			if (delta != null) {
				resource = delta.getResource();
			}
			if (resource != null
					&& (resource.getType() == IResource.PROJECT || resource.getType() == IResource.ROOT)) {
				refreshProjectsList();
			}
		}, IResourceChangeEvent.POST_CHANGE);
	}

	private void refreshProjectsList() {
		IProject[] knownProjects = ResourcesPlugin.getWorkspace().getRoot().getProjects();
		synchronized (locationsToProjects) {
			locationsToProjects.clear();
			for (IProject project : knownProjects) {
				IPath location = project.getLocation();
				if (location != null) {
					locationsToProjects.put(location, project);
				}
			}
		}
	}

	public static NestedProjectManager getInstance() {
		return INSTANCE;
	}

	/**
	 * @param folder a folder to decide about
	 * @return an {@link IProject} that or {@code null}
	 */
	public IProject getProject(IFolder folder) {
		if (folder == null) {
			return null;
		}
		IPath location = folder.getLocation();
		if (location == null) {
			return null;
		}
		IProject res;
		synchronized (locationsToProjects) {
			res = locationsToProjects.get(location);
		}
		if (res != null && (!res.exists() || !location.equals(res.getLocation()))) {
			// project was deleted and state not refreshed
			refreshProjectsList();
			return getProject(folder);
		}
		return res;
	}

	/**
	 * A shorthand for {@code getProject(folder) != null}.
	 *
	 * @param folder
	 * @return {@code true} if project having the same location as {@code folder} exists and nested is enabled, {@code false} otherwise
	 */
	public boolean isShownAsProject(IFolder folder) {
		return getProject(folder) != null;
	}

	public boolean isShownAsNested(IProject project) {
		if (!project.exists()) {
			return false;
		}
		IPath location = project.getLocation();
		if (location == null) {
			return false;
		}
		IPath queriedLocation = location.removeLastSegments(1);
		synchronized (locationsToProjects) {
			while (queriedLocation.segmentCount() > 0) {
				if (locationsToProjects.containsKey(queriedLocation)) {
					return true;
				}
				queriedLocation = queriedLocation.removeLastSegments(1);
			}
		}
		return false;
	}

	public IContainer getMostDirectOpenContainer(IProject project) {
		IPath location = project.getLocation();
		if (location == null) {
			return null;
		}
		IProject mostDirectParentProject = null;
		IPath queriedLocation = location.removeLastSegments(1);
		synchronized (locationsToProjects) {
			while (mostDirectParentProject == null && queriedLocation.segmentCount() > 0) {
				mostDirectParentProject = locationsToProjects.get(queriedLocation);
				if (mostDirectParentProject != null && mostDirectParentProject.getLocation() == null) {
					mostDirectParentProject = null;
				}
				queriedLocation = queriedLocation.removeLastSegments(1);
			}
		}
		if (mostDirectParentProject != null) {
			IPath parentContainerAbsolutePath = location.removeLastSegments(1);
			IPath location2 = mostDirectParentProject.getLocation();
			if (location2 == null) {
				return null;
			}
			if (parentContainerAbsolutePath.equals(location2)) {
				return mostDirectParentProject;
			}
			IPath parentFolderPathRelativeToProject = parentContainerAbsolutePath
					.removeFirstSegments(location2.segmentCount());
			return mostDirectParentProject.getFolder(parentFolderPathRelativeToProject);
		}
		return null;
	}

	/**
	 * @param container
	 *            a container to ask for nested projects
	 * @return the direct children projects for given container
	 */
	public IProject[] getDirectChildrenProjects(IContainer container) {
		if (container instanceof IWorkspaceRoot) {
			IWorkspaceRoot root = (IWorkspaceRoot) container;
			return root.getProjects();
		}
		Set<IProject> res = new HashSet<>();
		IPath containerLocation = container.getLocation();
		IPath projectLocation = container.getProject().getLocation();
		if (containerLocation == null || projectLocation == null) {
			return res.toArray(new IProject[res.size()]);
		}
		synchronized (locationsToProjects) {
			for (Entry<IPath, IProject> entry : locationsToProjects.tailMap(containerLocation).entrySet()) {
				if (entry.getValue().equals(container.getProject())) {
					// ignore current project
				} else if (containerLocation.isPrefixOf(entry.getKey())) {
					if (entry.getKey().segmentCount() == containerLocation.segmentCount() + 1) {
						res.add(entry.getValue());
					}
				} else { // moved to another branch, not worth continuing
					break;
				}
			}
		}
		return res.toArray(new IProject[res.size()]);
	}

	/**
	 * @param container
	 * @return whether the container has some projects as direct children
	 */
	public boolean hasDirectChildrenProjects(IContainer container) {
		if (container instanceof IWorkspaceRoot) {
			IWorkspaceRoot root = (IWorkspaceRoot) container;
			return root.getProjects().length > 0;
		}
		IPath containerLocation = container.getLocation();
		IPath projectLocation = container.getProject().getLocation();
		if (containerLocation == null || projectLocation == null) {
			return false;
		}
		synchronized (locationsToProjects) {
			for (Entry<IPath, IProject> entry : locationsToProjects.tailMap(containerLocation).entrySet()) {
				if (entry.getValue().equals(container.getProject())) {
					// ignore current project
				} else if (containerLocation.isPrefixOf(entry.getKey())) {
					if (entry.getKey().segmentCount() == containerLocation.segmentCount() + 1) {
						return true;
					}
				} else { // moved to another branch, not worth continuing
					break;
				}
			}
		}
		return false;
	}
}
