/*******************************************************************************
 * 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.jdt.internal.launching;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IPath;
import org.eclipse.debug.core.sourcelookup.ISourceContainer;
import org.eclipse.debug.core.sourcelookup.containers.DirectorySourceContainer;
import org.eclipse.debug.core.sourcelookup.containers.ExternalArchiveSourceContainer;
import org.eclipse.debug.core.sourcelookup.containers.FolderSourceContainer;
import org.eclipse.debug.core.sourcelookup.containers.ProjectSourceContainer;
import org.eclipse.jdt.core.IJavaModel;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.launching.IRuntimeClasspathEntry;
import org.eclipse.jdt.launching.sourcelookup.containers.JavaProjectSourceContainer;
import org.eclipse.jdt.launching.sourcelookup.containers.PackageFragmentRootSourceContainer;

/**
 * Private source lookup utilities. Translates runtime classpath entries
 * to source containers.
 *
 * @since 3.0
 */
public class JavaSourceLookupUtil {

	/**
	 * Translates the given runtime classpath entries into associated source
	 * containers.
	 *
	 * @param entries entries to translate
	 * @return source containers corresponding to the given runtime classpath entries
	 */
	public static ISourceContainer[] translate(IRuntimeClasspathEntry[] entries) {
		List<ISourceContainer> containers = new ArrayList<>(entries.length);
		for (int i = 0; i < entries.length; i++) {
			IRuntimeClasspathEntry entry = entries[i];
			switch (entry.getType()) {
				case IRuntimeClasspathEntry.ARCHIVE:
					if (entry.getPath().lastSegment().equals("jrt-fs.jar")) { //$NON-NLS-1$
						getPackageFragmentRootContainers(entry, containers);
					} else {
						IPackageFragmentRoot root = getPackageFragmentRoot(entry);
						if (root == null) {
							addSourceAttachment(entry, containers);
						} else {
							ISourceContainer container = new PackageFragmentRootSourceContainer(root);
							if (!containers.contains(container)) {
								containers.add(container);
							}
						}
					}
					break;
				case IRuntimeClasspathEntry.PROJECT:
					IResource resource = entry.getResource();
					if (resource != null && resource.getType() == IResource.PROJECT) {
						IJavaProject javaProject = JavaCore.create((IProject)resource);
						ISourceContainer container = null;
						if (javaProject.exists()) {
							container = new JavaProjectSourceContainer(javaProject);
						} else if (resource.exists()) {
							container = new ProjectSourceContainer((IProject)resource, false);
						}
						if (container != null && !containers.contains(container)) {
							containers.add(container);
						}
					}
					break;
				default:
					// no other classpath types are valid in a resolved path
					break;
			}
		}
		return containers.toArray(new ISourceContainer[containers.size()]);
	}

	/**
	 * Tries to find and attach source containers for given runtime classpath entry
	 *
	 * @param entry
	 *            non null
	 * @param containers
	 */
	private static void addSourceAttachment(IRuntimeClasspathEntry entry, List<ISourceContainer> containers) {
		String path = entry.getSourceAttachmentLocation();
		ISourceContainer container = null;
		if (path == null) {
			// use the archive itself
			path = entry.getLocation();
		}
		if (path != null) {
			// check if file or folder
			File file = new File(path);
			if (file.isDirectory()) {
				IResource resource = entry.getResource();
				if (resource instanceof IContainer) {
					container = new FolderSourceContainer((IContainer) resource, false);
				} else {
					container = new DirectorySourceContainer(file, false);
				}
			} else {
				container = new ExternalArchiveSourceContainer(path, true);
			}
			if (!containers.contains(container)) {
				containers.add(container);
			}
		}
	}

	/**
	 * Returns whether the source attachments of the given package fragment
	 * root and runtime classpath entry are equal.
	 * <p>
	 * NOTE: If the runtime classpath entry's source attachment is not specified,
	 * then it is considered equal. This way, the corresponding package fragment
	 * root is used for source lookup if it has a source attachment or not.
	 * </p>
	 *
	 * @param root package fragment root
	 * @param entry runtime classpath entry
	 * @return whether the source attachments of the given package fragment
	 * root and runtime classpath entry are equal
	 * @throws JavaModelException if there is a problem accessing the given {@link IPackageFragmentRoot}
	 */
	private static boolean isSourceAttachmentEqual(IPackageFragmentRoot root, IRuntimeClasspathEntry entry) throws JavaModelException {
		IPath entryPath = entry.getSourceAttachmentPath();
		if (entryPath == null) {
			return true;
		}
		IPath rootPath = root.getSourceAttachmentPath();
		if (rootPath == null) {
			// entry has a source attachment that the package root does not
			return false;
		}
		return rootPath.equals(entryPath);

	}

	/**
	 * Determines if the given archive runtime classpath entry exists
	 * in the workspace as a package fragment root. Returns the associated
	 * package fragment root or <code>null</code> if none.
	 *
	 * @param entry archive runtime classpath entry
	 * @return package fragment root or <code>null</code>
	 */
	private static IPackageFragmentRoot getPackageFragmentRoot(IRuntimeClasspathEntry entry) {
		IResource resource = entry.getResource();
		if (resource != null) {
			// find package fragment associated with the resource
			IProject project = resource.getProject();
			IJavaProject jp = JavaCore.create(project);
			try {
				if (project.isOpen() && jp.exists()) {
					IPackageFragmentRoot root = jp.findPackageFragmentRoot(resource.getFullPath());
					if (root != null && isSourceAttachmentEqual(root, entry)) {
						// use package fragment root
						return root;
					}
				}
			} catch (JavaModelException e) {
				LaunchingPlugin.log(e);
			}
		}
		// Check all package fragment roots for case of external archive.
		// External jars are shared, so it does not matter which project it
		// originates from
		IJavaModel model = JavaCore.create(ResourcesPlugin.getWorkspace().getRoot());
		IPath entryPath = entry.getPath();
		try {
			IJavaProject[] jps = model.getJavaProjects();
			for (int i = 0; i < jps.length; i++) {
				IJavaProject jp = jps[i];
				IProject p =  jp.getProject();
				if (p.isOpen()) {
					IPackageFragmentRoot[] allRoots = jp.getPackageFragmentRoots();
					for (int j = 0; j < allRoots.length; j++) {
						IPackageFragmentRoot root = allRoots[j];
						if (root.getPath().equals(entryPath) && isSourceAttachmentEqual(root, entry)) {
							// use package fragment root
							return root;
						}
					}
				}
			}
		} catch (JavaModelException e) {
			LaunchingPlugin.log(e);
		}
		return null;
	}


	/**
	 * Determines the package fragment roots from the java model and adds corresponding PackageFragmentRootSourceContainer to the list of containers
	 * if their source attachment and path is same as the entry.
	 *
	 * @param entry
	 *            archive runtime classpath entry for jrt-fs.jar
	 * @param containers
	 *            container list to be updated
	 */

	private static void getPackageFragmentRootContainers(IRuntimeClasspathEntry entry, List<ISourceContainer> containers) {
		IJavaModel model = JavaCore.create(ResourcesPlugin.getWorkspace().getRoot());
		IPath entryPath = entry.getPath();
		boolean found = false;
		try {
			IJavaProject[] jps = model.getJavaProjects();
			for (int i = 0; i < jps.length; i++) {
				IJavaProject jp = jps[i];
				IProject p = jp.getProject();
				if (p.isOpen()) {
					IPackageFragmentRoot[] allRoots = jp.getPackageFragmentRoots();
					for (int j = 0; j < allRoots.length; j++) {
						IPackageFragmentRoot root = allRoots[j];
						if (root.getPath().equals(entryPath) && isSourceAttachmentEqual(root, entry)) {
							PackageFragmentRootSourceContainer container = new PackageFragmentRootSourceContainer(root);
							if (!containers.contains(container)) {
								containers.add(container);
								found = true;
							}
						}
					}
				}
			}
		}
		catch (JavaModelException e) {
			LaunchingPlugin.log(e);
		}

		if (!found) {
			addSourceAttachment(entry, containers);
		}
	}
}
