/**
 * Copyright (c) 2016 NumberFour AG.
 * 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
 *
 * Contributors:
 *   NumberFour AG - Initial API and implementation
 */
package org.eclipse.n4js.external;

import static com.google.common.collect.FluentIterable.from;
import static com.google.common.collect.Sets.newHashSet;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static org.eclipse.core.resources.ResourcesPlugin.getWorkspace;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IProjectDescription;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.runtime.Platform;
import org.eclipse.emf.common.util.URI;
import org.eclipse.n4js.projectModel.IN4JSCore;
import org.eclipse.n4js.projectModel.IN4JSProject;
import org.eclipse.n4js.projectModel.names.EclipseProjectName;
import org.eclipse.n4js.projectModel.names.N4JSProjectName;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.inject.Inject;
import com.google.inject.Singleton;

/**
 * Service for collecting available {@link N4JSExternalProject external project} instances based on the configured
 * external library locations.
 */
@Singleton
public class ExternalProjectsCollector {

	@Inject
	private ExternalLibraryWorkspace extWS;

	@Inject
	private IN4JSCore core;

	/** @return a new set that contains only those projects of the given set that <b>are</b> in the workspace */
	public <P extends IProject> Set<P> filterWSProjects(Iterable<P> addedProjects) {
		return filterWSProjects(addedProjects, true);
	}

	/** @return a new set that contains only those projects of the given set that <b>are not</b> in the workspace */
	public <P extends IProject> Set<P> filterNonWSProjects(Iterable<P> projects) {
		return filterWSProjects(projects, false);
	}

	private <P extends IProject> Set<P> filterWSProjects(Iterable<P> addedProjects, boolean positive) {
		Set<String> eclipseWorkspaceProjectNames = getAllEclipseWorkspaceProjectNames();
		Set<P> projectsToBuild = newHashSet();
		for (P addedProject : addedProjects) {
			if (positive == eclipseWorkspaceProjectNames.contains(addedProject.getName())) {
				projectsToBuild.add(addedProject);
			}
		}
		return projectsToBuild;
	}

	private Set<String> getAllEclipseWorkspaceProjectNames() {
		if (Platform.isRunning()) {
			return from(Arrays.asList(getWorkspace().getRoot().getProjects()))
					.filter(p -> p.isAccessible())
					.transform(p -> p.getName())
					.toSet();
		}
		return Collections.emptySet();
	}

	/**
	 * Returns with all {@link IProject project} instances that are available from the {@link IWorkspaceRoot workspace
	 * root} and has direct dependency to any external projects.
	 *
	 * <p>
	 * This method neither considers, nor modifies the {@link IProjectDescription#getDynamicReferences() dynamic
	 * references} of the workspace projects. Instead it gathers the dependency information from the {@link IN4JSProject
	 * N4JS project} using the {@link IN4JSCore N4JS core} service.
	 *
	 * <p>
	 * No transitive dependencies will be considered. For instance if an accessible Eclipse workspace project B depends
	 * on another accessible Eclipse project A and project A depends on an external project X, then this method will
	 * return with only A and *NOT* A and B.
	 *
	 * @return an iterable of Eclipse workspace projects that has direct dependency any external projects.
	 */
	public Collection<IProject> getWSProjectsDependendingOn() {
		return getWSProjectsDependendingOn(extWS.getProjects());
	}

	/**
	 * Sugar for collecting {@link IWorkspace Eclipse workspace} projects that have any direct dependency to any
	 * external projects. Same as {@link #getWSProjectsDependendingOn()} but does not considers all the available
	 * projects but only those that are given as the argument.
	 *
	 * @param externalProjects
	 *            the external projects that has to be considered as a possible dependency of an Eclipse workspace based
	 *            project.
	 * @return an iterable of Eclipse workspace projects that has direct dependency to an external project given as the
	 *         argument.
	 */
	public Collection<IProject> getWSProjectsDependendingOn(Iterable<N4JSExternalProject> externalProjects) {
		return getProjectsDependendingOn(asList(getWorkspace().getRoot().getProjects()), externalProjects);
	}

	/**
	 *
	 * @param externalProjects
	 *            the external projects that has to be considered as a possible dependency of an Eclipse workspace based
	 *            project.
	 * @return an iterable of external workspace projects that has direct dependency to an external project given as the
	 *         argument.
	 */
	public Collection<N4JSExternalProject> getExtProjectsDependendingOn(
			Iterable<N4JSExternalProject> externalProjects) {
		return getProjectsDependendingOn(extWS.getProjects(), externalProjects);
	}

	/**
	 * Sugar for collecting {@link IWorkspace Eclipse workspace} projects that have any direct dependency to any
	 * external projects. Same as {@link #getWSProjectsDependendingOn()} but does not considers all the available
	 * projects but only those that are given as the argument.
	 *
	 * @param externalProjects
	 *            the external projects that has to be considered as a possible dependency of an Eclipse workspace based
	 *            project.
	 * @return an iterable of Eclipse workspace projects that has direct dependency to an external project given as the
	 *         argument.
	 */
	private <P extends IProject> Collection<P> getProjectsDependendingOn(Iterable<P> wsProjects,
			Iterable<N4JSExternalProject> externalProjects) {

		if (!Platform.isRunning()) {
			return emptyList();
		}

		Set<N4JSProjectName> externalIds = from(externalProjects)
				.transform(p -> new EclipseProjectName(p.getName()).toN4JSProjectName())
				.toSet();
		LinkedList<P> filteredProjects = new LinkedList<>();

		Map<N4JSProjectName, IProject> externalsMapping = new HashMap<>();
		for (IProject prj : externalProjects) {
			externalsMapping.put(new EclipseProjectName(prj.getName()).toN4JSProjectName(), prj);
		}

		for (P prj : wsProjects) {

			if (prj instanceof N4JSExternalProject) {
				N4JSExternalProject extPrj = (N4JSExternalProject) prj;
				for (N4JSProjectName eID : extPrj.getAllDirectDependencyIds()) {
					IProject externalDependency = externalsMapping.get(eID);
					if (externalDependency != null) {
						filteredProjects.add(prj);
					}
				}
			} else {

				Set<N4JSProjectName> deps = Sets.newHashSet(getDirectExternalDependencyIds(prj));
				Iterables.retainAll(deps, externalIds);
				if (!Iterables.isEmpty(deps)) {
					filteredProjects.add(prj);
				}
			}
		}

		return filteredProjects;
	}

	/**
	 * Returns mapping between all {@link IProject project} instances that are available from the {@link IWorkspaceRoot
	 * workspace root} and its direct dependencies to any external projects.
	 *
	 * <p>
	 * This method neither considers, nor modifies the {@link IProjectDescription#getDynamicReferences() dynamic
	 * references} of the workspace projects. Instead it gathers the dependency information from the {@link IN4JSProject
	 * N4JS project} using the {@link IN4JSCore N4JS core} service.
	 *
	 * <p>
	 * No transitive dependencies will be considered. For instance if an accessible Eclipse workspace project B depends
	 * on another accessible Eclipse project A and project A depends on an external project X and Y, then this method
	 * will return with only {A->[X]} and *NOT* {A->[X,Y], B->[X,Y]}.
	 *
	 * @return a map where each entry maps an external project to the workspace projects that depend on it.
	 */
	public Multimap<N4JSExternalProject, IProject> getWSProjectDependents() {
		return getWSProjectDependents(extWS.getProjects());
	}

	/**
	 * Sugar for collecting {@link IWorkspace Eclipse workspace} projects that have any direct dependency to any
	 * external projects. Same as {@link #getWSProjectDependents()} but does not consider all the available projects but
	 * only those that are given as the argument.
	 *
	 * @param externalProjects
	 *            the external projects that has to be considered as a possible dependency of an Eclipse workspace based
	 *            project.
	 * @return a map where each entry maps an external project to the workspace projects that depend on it.
	 */
	public Multimap<N4JSExternalProject, IProject> getWSProjectDependents(
			Iterable<N4JSExternalProject> externalProjects) {
		return getProjectDependents(asList(getWorkspace().getRoot().getProjects()), externalProjects);
	}

	/***/
	public Multimap<N4JSExternalProject, N4JSExternalProject> getExtProjectDependents(
			Iterable<N4JSExternalProject> externalProjects) {

		return getProjectDependents(extWS.getProjects(), externalProjects);
	}

	/***/
	private <P extends IProject> Multimap<N4JSExternalProject, P> getProjectDependents(Iterable<P> wsProjects,
			Iterable<N4JSExternalProject> externalProjects) {

		Multimap<N4JSExternalProject, P> mapping = HashMultimap.create();

		if (!Platform.isRunning()) {
			return mapping;
		}

		Map<N4JSProjectName, N4JSExternalProject> externalsMapping = new HashMap<>();
		for (N4JSExternalProject prj : externalProjects) {
			externalsMapping.put(new EclipseProjectName(prj.getName()).toN4JSProjectName(), prj);
		}

		for (P prj : wsProjects) {
			if (prj instanceof N4JSExternalProject) {
				N4JSExternalProject extPrj = (N4JSExternalProject) prj;
				for (N4JSProjectName eID : extPrj.getAllDirectDependencyIds()) {
					N4JSExternalProject externalDependency = externalsMapping.get(eID);
					if (externalDependency != null) {
						mapping.put(externalDependency, prj);
					}
				}
			} else {
				for (N4JSProjectName eID : getDirectExternalDependencyIds(prj)) {
					N4JSExternalProject externalDependency = externalsMapping.get(eID);
					if (externalDependency != null) {
						mapping.put(externalDependency, prj);
					}
				}
			}
		}

		return mapping;
	}

	/**
	 * Returns with all external project dependency project IDs for a particular non-external, accessible project.
	 */
	private Iterable<N4JSProjectName> getDirectExternalDependencyIds(IProject project) {

		if (null == project || !project.isAccessible()) {
			return emptyList();
		}

		IN4JSProject n4Project = core.findProject(URI.createPlatformResourceURI(project.getName(), true)).orNull();
		if (null == n4Project || !n4Project.exists() || n4Project.isExternal()) {
			return emptyList();
		}

		return from(n4Project.getAllDirectDependencies())
				.filter(IN4JSProject.class)
				.filter(p -> p.isExternal())
				.transform(p -> p.getProjectName());
	}
}