/*
 * $Id$
 *
 * SARL is an general-purpose agent programming language.
 * More details on http://www.sarl.io
 *
 * Copyright (C) 2014-2026 SARL.io, the original authors and main authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.sarl.apputils.uiextensions;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.FileLocator;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.internal.core.ClasspathEntry;
import org.eclipse.jdt.internal.core.JavaProject;
import org.eclipse.jdt.internal.core.util.Util;
import org.eclipse.jdt.internal.launching.RuntimeClasspathEntry;
import org.eclipse.jdt.launching.IRuntimeClasspathEntry;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleException;
import org.osgi.framework.Version;
import org.osgi.framework.wiring.BundleWiring;

import io.sarl.apputils.uiextensions.Utilities.BundleURLMappings;
import io.sarl.apputils.uiextensions.classpath.JavaClasspathParser;
import io.sarl.lang.SARLConfig;

/** Utilities around bundles. It should be replaced
 * by the OSGi, Eclipse and Xtext API.
 *
 * @author $Author: sgalland$
 * @author $Author: ngaud$
 * @version $FullVersion$
 * @mavengroupid $GroupId$
 * @mavenartifactid $ArtifactId$
 * @since 0.15
 */
public final class Bundles {

	/** OS-independent paths of the bin folders.
	 */
	public static final String[] BIN_FOLDERS = {
		SARLConfig.FOLDER_BIN,
		"bin", //$NON-NLS-1$
	};

	/** OS-independent paths of the source folders.
	 */
	public static final String[] SRC_FOLDERS = {
		SARLConfig.FOLDER_SOURCE_JAVA,
		SARLConfig.FOLDER_SOURCE_SARL,
		"src", //$NON-NLS-1$
	};

	private static final String SOURCE_SUFIX = ".source"; //$NON-NLS-1$

	private static final String JAVADOC_SUFIX = ".javadoc"; //$NON-NLS-1$

	private static final String JAR_EXTENSION = "jar"; //$NON-NLS-1$

	private static final String DOT_JAR_EXTENSION = "." + JAR_EXTENSION; //$NON-NLS-1$

	private static final String ROOT_NAME = "/"; //$NON-NLS-1$

	private static final String DEFAULT_PATH_TO_CLASSES_IN_MAVEN_PROJECT = "target/classes"; //$NON-NLS-1$

	private Bundles() {
		//
	}

	/** Create an instance of a set of bundle dependencies.
	 *
	 * @return the set.
	 * @since 0.13
	 */
	protected static Set<BundleDependency> createBundleDependencySet() {
		return new TreeSet<>(new Comparator<BundleDependency>() {
			@Override
			public int compare(BundleDependency o1, BundleDependency o2) {
				return o1.getBundle().getSymbolicName().compareTo(o2.getBundle().getSymbolicName());
			}
		});
	}
	
	/** Replies the source location for the given bundle.
	 *
	 * <p>The source location is usually the root folder where the source code of the bundle is located.
	 *
	 * <p>We can't use P2Utils and we can't use SimpleConfiguratorManipulator because of
	 * API breakage between 3.5 and 4.2.
	 * So we do a bit EDV (Computer data processing) ;-)
	 *
	 * @param bundle the bundle for which the source location must be computed.
	 * @param bundleLocation the location of the bundle, as replied by {@link #getBundlePath(Bundle)}.
	 * @return the path to the source folder of the bundle, or {@code null} if undefined.
	 * @see #getBundlePath(Bundle)
	 * @throws BundleException if there is some error for retrieving the bundle path.
	 */
	public static IPath getSourceBundlePath(Bundle bundle, IPath bundleLocation) throws BundleException {
		if (bundle == null) {
			throw new ExpectingBundleException();
		}
		IPath sourcesPath = null;
		// Not an essential functionality, make it robust
		try {
			final var srcFolderPath = getSourceRootProjectFolderPath(bundle);
			if (srcFolderPath == null) {
				//common case, jar file.
				final var bundlesParentFolder = bundleLocation.removeLastSegments(1);
				final var binaryJarName = bundleLocation.lastSegment();
				final var symbolicName = bundle.getSymbolicName();
				final var sourceJarName = binaryJarName.replace(symbolicName,
						symbolicName.concat(SOURCE_SUFIX));
				final var potentialSourceJar = bundlesParentFolder.append(sourceJarName);
				if (potentialSourceJar.toFile().exists()) {
					sourcesPath = potentialSourceJar;
				}
			} else {
				sourcesPath = srcFolderPath;
			}
		} catch (Throwable t) {
			throw new RuntimeException(t);
		}

		return sourcesPath;
	}

	private static IPath getBinFolderPath(Bundle bundle) {
		for (final var binFolder : BIN_FOLDERS) {
			final var binFolderURL = FileLocator.find(bundle, Path.fromPortableString(binFolder), null);
			if (binFolderURL != null) {
				try {
					final var binFolderFileURL = FileLocator.toFileURL(binFolderURL);
					return new Path(binFolderFileURL.getPath()).makeAbsolute();
				} catch (IOException e) {
					throw new RuntimeException(e);
				}
			}
		}
		return null;
	}

	private static IPath getSourceRootProjectFolderPath(Bundle bundle) {
		for (final var srcFolder : SRC_FOLDERS) {
			final var relPath = Path.fromPortableString(srcFolder);
			final var srcFolderURL = FileLocator.find(bundle, relPath, null);
			if (srcFolderURL != null) {
				try {
					final var srcFolderFileURL = FileLocator.toFileURL(srcFolderURL);
					var absPath = new Path(srcFolderFileURL.getPath()).makeAbsolute();
					absPath = absPath.removeLastSegments(relPath.segmentCount());
					return absPath;
				} catch (IOException e) {
					//
				}
			}
		}
		return null;
	}

	/** Replies the path of the binary files of the given bundle.
	 *
	 * @param bundle the bundle for which the path must be retreived.
	 * @return the path to the binaries of the bundle.
	 * @see #getSourceBundlePath(Bundle, IPath)
	 * @throws BundleException if there is some error for retrieving the bundle path.
	 */
	public static IPath getBundlePath(Bundle bundle) throws BundleException {
		if (bundle == null) {
			throw new ExpectingBundleException();
		}
		var path = getBinFolderPath(bundle);
		if (path == null) {
			// common jar file case, no bin folder
			final var opt = FileLocator.getBundleFileLocation(bundle);
			if (opt.isPresent()) {
				path = new Path(opt.get().getAbsolutePath());
			}
		}
		return path;
	}

	/** Replies the javadoc location for the given bundle.
	 *
	 * <p>We can't use P2Utils and we can't use SimpleConfiguratorManipulator because of
	 * API breakage between 3.5 and 4.2.
	 * So we do a bit EDV (Computer data processing) ;-)
	 *
	 * @param bundle the bundle for which the javadoc location must be computed.
	 * @param bundleLocation the location of the bundle, as replied by {@link #getBundlePath(Bundle)}.
	 * @return the path to the javadoc folder of the bundle, or {@code null} if undefined.
	 * @see #getBundlePath(Bundle)
	 * @throws BundleException if there is some error for retrieving the bundle path.
	 */
	public static IPath getJavadocBundlePath(Bundle bundle, IPath bundleLocation) throws BundleException {
		if (bundle == null) {
			throw new ExpectingBundleException();
		}
		IPath sourcesPath = null;
		// Not an essential functionality, make it robust
		try {
			final var srcFolderPath = getSourceRootProjectFolderPath(bundle);
			if (srcFolderPath == null) {
				//common case, jar file.
				final var bundlesParentFolder = bundleLocation.removeLastSegments(1);
				final var binaryJarName = bundleLocation.lastSegment();
				final var symbolicName = bundle.getSymbolicName();
				final var sourceJarName = binaryJarName.replace(symbolicName,
						symbolicName.concat(JAVADOC_SUFIX));
				final var potentialSourceJar = bundlesParentFolder.append(sourceJarName);
				if (potentialSourceJar.toFile().exists()) {
					sourcesPath = potentialSourceJar;
				}
			} else {
				sourcesPath = srcFolderPath;
			}
		} catch (Throwable t) {
			throw new RuntimeException(t);
		}
		return sourcesPath;
	}

	/** Replies the dependencies for the given bundle.
	 *
	 * @param bundle the bundle.
	 * @return the bundle dependencies.
	 * @throws BundleException if one bundle cannot be resolved.
	 */
	public static IBundleDependencies resolveBundleDependencies(Bundle bundle) throws BundleException {
		return resolveBundleDependencies(bundle, (BundleURLMappings) null);
	}

	/** Replies the dependencies for the given bundle.
	 *
	 * @param bundle the bundle.
	 * @param javadocURLs the mapping from bundles to the corresponding Javadoc URLs.
	 * @return the bundle dependencies.
	 * @throws BundleException if there is some error for retrieving the bundle path.
	 */
	public static IBundleDependencies resolveBundleDependencies(Bundle bundle, BundleURLMappings javadocURLs) throws BundleException {
		if (bundle == null) {
			throw new ExpectingBundleException();
		}
		final var docMapping = javadocURLs == null ? new Utilities.SARLBundleJavadocURLMappings() : javadocURLs;
		return new BundleDependencies(bundle, null, null, docMapping);
	}

	/** Replies the dependencies for the given bundle.
	 *
	 * @param bundle the bundle.
	 * @param directDependencies the list of the bundle symbolic names that are the direct dependencies of the bundle to
	 *      be considered. If the given bundle has other dependencies in its Manifest, they will be ignored if they
	 *      are not in this parameter.
	 * @return the bundle dependencies.
	 * @since 0.13
	 * @throws BundleException if one bundle cannot be resolved.
	 */
	public static IBundleDependencies resolveBundleDependenciesWithFilter(Bundle bundle, String... directDependencies) throws BundleException {
		return resolveBundleDependenciesWithFilter(bundle, (BundleURLMappings) null, directDependencies);
	}

	/** Replies the dependencies for the given bundle.
	 *
	 * @param bundle the bundle.
	 * @param javadocURLs the mapping from bundles to the corresponding Javadoc URLs.
	 * @param directDependencies the list of the bundle symbolic names that are the direct dependencies of the bundle to
	 *      be considered. If the given bundle has other dependencies in its Manifest, they will be ignored if they
	 *      are not in this parameter.
	 * @return the bundle dependencies.
	 * @since 0.13
	 * @throws BundleException if there is some error for retrieving the bundle path.
	 */
	public static IBundleDependencies resolveBundleDependenciesWithFilter(Bundle bundle, BundleURLMappings javadocURLs, String... directDependencies) throws BundleException {
		if (bundle == null) {
			throw new ExpectingBundleException();
		}
		final var docMapping = javadocURLs == null ? new Utilities.SARLBundleJavadocURLMappings() : javadocURLs;
		final var deps = directDependencies == null || directDependencies.length == 0 ? null : Arrays.asList(directDependencies);
		return new BundleDependencies(bundle, deps, null, docMapping);
	}

	/** Replies the dependencies for the given bundle.
	 *
	 * @param bundle the bundle.
	 * @param extraDependencies the list of the bundle symbolic names that are in the dependencies of the bundle to
	 *      be considered. There bundles, and their transitive dependencies, are automatically added in the dependencies of the bundle. 
	 * @return the bundle dependencies.
	 * @since 0.13
	 * @throws BundleException if one bundle cannot be resolved.
	 */
	public static IBundleDependencies resolveBundleDependenciesWithExtras(Bundle bundle, String... extraDependencies) throws BundleException {
		return resolveBundleDependenciesWithExtras(bundle, (BundleURLMappings) null, extraDependencies);
	}

	/** Replies the dependencies for the given bundle.
	 *
	 * @param bundle the bundle.
	 * @param javadocURLs the mapping from bundles to the corresponding Javadoc URLs.
	 * @param extraDependencies the list of the bundle symbolic names that are in the dependencies of the bundle to
	 *      be considered. There bundles, and their transitive dependencies, are automatically added in the dependencies of the bundle. 
	 * @return the bundle dependencies.
	 * @since 0.13
	 * @throws BundleException if there is some error for retrieving the bundle path.
	 */
	public static IBundleDependencies resolveBundleDependenciesWithExtras(Bundle bundle, BundleURLMappings javadocURLs, String... extraDependencies) throws BundleException {
		if (bundle == null) {
			throw new ExpectingBundleException();
		}
		final var docMapping = javadocURLs == null ? new Utilities.SARLBundleJavadocURLMappings() : javadocURLs;
		final var deps = extraDependencies == null || extraDependencies.length == 0 ? null : Arrays.asList(extraDependencies);
		return new BundleDependencies(bundle, null, deps, docMapping);
	}

	/** Container of bundle dependencies. This class is an iterable on the symbolic names of the dependency bundles.
	 *
	 * @author $Author: sgalland$
	 * @version $FullVersion$
	 * @mavengroupid $GroupId$
	 * @mavenartifactid $ArtifactId$
	 */
	public static class BundleDependency {

		private final Bundle bundle;

		private final IClasspathEntry classpathEntry;

		private final boolean isFragment;

		private IRuntimeClasspathEntry runtimeClasspathEntry;

		/** Constructor.
		 *
		 * @param bundle the bundle.
		 * @param classPathEntry the classpath entry for the bundle.
		 * @param isFragment indicates if the dependency is a fragment or not.
		 * @throws BundleException if a bundle cannot be resolved.
		 */
		BundleDependency(Bundle bundle, IClasspathEntry classPathEntry, boolean isFragment) throws BundleException {
			if (bundle == null) {
				throw new ExpectingBundleException();
			}
			assert classPathEntry != null;
			this.bundle = bundle;
			this.classpathEntry = classPathEntry;
			this.isFragment = isFragment;
		}

		/** Replies the bundle.
		 *
		 * @return the bundle.
		 */
		public Bundle getBundle() {
			return this.bundle;
		}

		/** The class path entry for the bundle.
		 *
		 * @return the classpath entry.
		 */
		public IClasspathEntry getClassPathEntry() {
			return this.classpathEntry;
		}

		/** The runtime class path entry for the bundle.
		 *
		 * @return the runtime classpath entry.
		 */
		public IRuntimeClasspathEntry getRuntimeClassPathEntry() {
			if (this.classpathEntry != null && this.runtimeClasspathEntry == null) {
				this.runtimeClasspathEntry = new RuntimeClasspathEntry(this.classpathEntry);
			}
			return this.runtimeClasspathEntry;
		}

		/** Replies if the dependency is a fragment bundle.
		 *
		 * @return {@code true} if the bundle is a fragment.
		 */
		public boolean isFragment() {
			return this.isFragment;
		}

		@Override
		public String toString() {
			return this.bundle.getSymbolicName();
		}

	}

	/** Container of bundle dependencies. This class is an iterable on the symbolic names of the dependency bundles.
	 *
	 * @author $Author: sgalland$
	 * @version $FullVersion$
	 * @mavengroupid $GroupId$
	 * @mavenartifactid $ArtifactId$
	 */
	public interface IBundleDependencies {

		/** Replies the name of the bundle that depends on the current dependencies.
		 *
		 * @return the bundle name.
		 * @throws BundleException if one bundle cannot be resolved.
		 */
		String getBundleSymbolicName() throws BundleException;

		/** Replies the detected binary path for the bundle.
		 *
		 * @return the output folder.
		 * @throws BundleException if one bundle cannot be resolved.
		 */
		IPath getBundleBinaryPath() throws BundleException;

		/** Replies the version of the bundle that is considered for computing the dependencies.
		 *
		 * @return the bundle version.
		 * @throws BundleException if one bundle cannot be resolved.
		 */
		Version getBundleVersion() throws BundleException;

		/** Replies the symbolic names of the direct dependencies of the bundle (no transitivity).
		 * The bundle itself is included in the replied list if it is not a directory.
		 *
		 * @return the symbolic names of the bundle dependencies.
		 * @throws BundleException if one bundle cannot be resolved.
		 */
		Iterable<String> getDirectSymbolicNames() throws BundleException;

		/** Replies the classpath entries of the bundle dependencies (no transitivity).
		 * The bundle itself is included in the replied list if it is not a directory.
		 *
		 * @return the classpath entries of the bundle dependencies.
		 * @throws BundleException if one bundle cannot be resolved.
		 */
		Iterable<IClasspathEntry> getDirectClasspathEntries() throws BundleException;

		/** Replies the runtime classpath entries of the bundle dependencies (no transitivity).
		 * The bundle itself is included in the replied list if it is not a directory.
		 *
		 * @return the runtime classpath entries of the bundle dependencies.
		 * @throws BundleException if one bundle cannot be resolved.
		 */
		Iterable<IRuntimeClasspathEntry> getDirectRuntimeClasspathEntries() throws BundleException;

		/** Replies the dependencies of the bundle.
		 * The bundle itself is included in the replied list if it is not a directory (no transitivity).
		 *
		 * @return the bundle dependencies, or {@code null} if the dependencies cannot be computed.
		 * @throws BundleException if one bundle cannot be resolved.
		 */
		Set<BundleDependency> getDirectDependencies() throws BundleException;

		/** Replies the symbolic names of the bundle dependencies (transitivity).
		 * The bundle itself is included in the replied list if it is not a directory.
		 *
		 * @param includeFragments indicates if bundle fragments should be replied also.
		 * @return the symbolic names of the bundle dependencies.
		 * @throws BundleException if one bundle cannot be resolved.
		 */
		Iterable<String> getTransitiveSymbolicNames(boolean includeFragments) throws BundleException;

		/** Replies the classpath entries of the bundle dependencies (transitivity).
		 * The bundle itself is included in the replied list if it is not a directory.
		 *
		 * @param includeFragments indicates if bundle fragments should be replied also.
		 * @return the classpath entries of the bundle dependencies.
		 * @throws BundleException if one bundle cannot be resolved.
		 */
		Iterable<IClasspathEntry> getTransitiveClasspathEntries(boolean includeFragments) throws BundleException;

		/** Replies the runtime classpath entries of the bundle dependencies (transitivity).
		 * The bundle itself is included in the replied list if it is not a directory.
		 *
		 * @param includeFragments indicates if bundle fragments should be replied also.
		 * @return the runtime classpath entries of the bundle dependencies.
		 * @throws BundleException if one bundle cannot be resolved.
		 */
		Iterable<IRuntimeClasspathEntry> getTransitiveRuntimeClasspathEntries(boolean includeFragments) throws BundleException;

		/** Replies the dependencies of the bundle (transitivity).
		 * The bundle itself is included in the replied list if it is not a directory.
		 *
		 * @param includeFragments indicates if bundle fragments should be replied also.
		 * @return the bundle dependencies, or {@code null} if the dependencies cannot be computed.
		 * @throws BundleException if one bundle cannot be resolved.
		 */
		Iterable<BundleDependency> getTransitiveDependencies(boolean includeFragments) throws BundleException;

	}

	/** Definition of a set of dependencies.
	 *
	 * <p>The set entries are sorted in the insertion order.
	 *
	 * @author $Author: sgalland$
	 * @version $FullVersion$
	 * @mavengroupid $GroupId$
	 * @mavenartifactid $ArtifactId$
	 */
	private static class DependencyDefinition {

		private Version version;

		private final Set<BundleDependency> dependencies = createBundleDependencySet();

		private final Set<String> dependencyIds = new TreeSet<>();

		DependencyDefinition(Version version, Collection<BundleDependency> dependencies) {
			this.version = version;
			addDependencies(dependencies);
		}

		/** Change the version of the dependency.
		 *
		 * @param version the new version. if {@code null}, the version does not change.
		 */
		public void setVersion(Version version) {
			if (version != null) {
				this.version = version;
			}
		}

		/** Add the given dependencies.
		 *
		 * @param dependencies the dependencies.
		 */
		public void addDependencies(Collection<BundleDependency> dependencies) {
			for (final var dependency : dependencies) {
				if (this.dependencyIds.add(dependency.getBundle().getSymbolicName())) {
					this.dependencies.add(dependency);
				}
			}
		}

		/** Replies the version of the bundle.
		 *
		 * @return the version.
		 */
		public Version getVersion() {
			return this.version;
		}

		/** Replies the dependencies.
		 *
		 * @return the dependencies.
		 */
		public Set<BundleDependency> getDependencies() {
			return Collections.unmodifiableSet(this.dependencies);
		}

		@Override
		public String toString() {
			final var buf = new StringBuilder();
			buf.append("version="); //$NON-NLS-1$
			buf.append(this.version);
			buf.append("; dependencies="); //$NON-NLS-1$
			buf.append(this.dependencies.toString());
			return buf.toString();
		}

	}

	/** Container of bundle dependencies. This class is an iterable on the symbolic names of the dependency bundles.
	 *
	 * @author $Author: sgalland$
	 * @version $FullVersion$
	 * @mavengroupid $GroupId$
	 * @mavenartifactid $ArtifactId$
	 */
	private static class BundleDependencies implements IBundleDependencies {

		private final Bundle bundle;

		private final Collection<String> directDependencies;

		private final Collection<String> extraDependencies;

		private final BundleURLMappings javadocURLs;

		private IPath binaryBundlePath;

		private Map<Bundle, DependencyDefinition> bundleDependencies;

		/** Constructor.
		 *
		 * @param bundle the bundle.
		 * @param directDependencies the list of the bundle symbolic names that are the direct dependencies of the bundle to
		 *      be considered. If the given bundle has other dependencies in its Manifest, they will be ignored if they
		 *      are not in this parameter.
		 * @param extraDependencies the list of the bundle symbolic names that are in the dependencies of the bundle to
		 *      be considered. There bundles, and their transitive dependencies, are automatically added in the dependencies of the bundle. 
		 * @param javadocURLs the mapping from bundles to the corresponding Javadoc URLs.
		 * @throws BundleException if a bundle cannot be resolved.
		 */
		BundleDependencies(Bundle bundle, Collection<String> directDependencies, Collection<String> extraDependencies, BundleURLMappings javadocURLs) throws BundleException {
			if (bundle == null) {
				throw new ExpectingBundleException();
			}
			this.bundle = bundle;
			this.directDependencies = directDependencies;
			this.extraDependencies = extraDependencies;
			this.javadocURLs = javadocURLs;
		}

		private Map<Bundle, DependencyDefinition> getBundleDependencies() {
			if (this.bundleDependencies == null) {
				this.bundleDependencies = new TreeMap<>(new Comparator<Bundle>() {
					@Override
					public int compare(Bundle o1, Bundle o2) {
						return o1.getSymbolicName().compareTo(o2.getSymbolicName());
					}
				});
			}
			return this.bundleDependencies;
		}

		private DependencyDefinition getBundleDependencies(Bundle bundle) {
			final var repository = getBundleDependencies();
			synchronized (repository) {
				return repository.get(bundle);
			}
		}

		private void setBundleDependencies(Bundle bundle, Set<BundleDependency> cpEntries, boolean overwrite) {
			final var repository = getBundleDependencies();
			synchronized (repository) {
				if (overwrite || !repository.containsKey(bundle)) {
					repository.put(bundle, new DependencyDefinition(bundle.getVersion(), cpEntries));
				} else {
					final var dependencySet = repository.get(bundle);
					assert dependencySet != null;
					dependencySet.setVersion(bundle.getVersion());
					dependencySet.addDependencies(cpEntries);
				}
			}
		}

		private void addToBundleDependencies(Bundle bundle, BundleDependency dependency) {
			final var repository = getBundleDependencies();
			synchronized (repository) {
				final var dependencySet = repository.get(bundle);
				if (dependencySet == null) {
					repository.put(bundle, new DependencyDefinition(bundle.getVersion(), Collections.singletonList(dependency)));
				} else {
					dependencySet.addDependencies(Collections.singletonList(dependency));
				}
			}
		}

		@Override
		public String toString() {
			try {
				final var dependencies = getDependencyDefinition();
				final var buf = new StringBuilder();
				if (dependencies != null) {
					toDependencyTree(
							buf,
							new String(), new String(),
							this.bundle,
							false,
							dependencies.getDependencies());
				}
				return buf.toString();
			} catch (BundleException ex) {
				return ""; //$NON-NLS-1$
			}
		}

		private void toDependencyTree(StringBuilder builder, String indent1, String indent2, Bundle current,
				boolean isFragment, Collection<BundleDependency> dependencies) {
			builder.append(indent1);
			builder.append(current.getSymbolicName());
			if (isFragment) {
				builder.append(" (fragment)"); //$NON-NLS-1$
			}
			builder.append("\n"); //$NON-NLS-1$
			for (final var dependency : dependencies) {
				if (!Objects.equals(current.getSymbolicName(), dependency.getBundle().getSymbolicName())) {
					final var subdependencies = getBundleDependencies(dependency.getBundle());
					if (subdependencies != null) {
						toDependencyTree(
								builder,
								indent2 + "|- ", //$NON-NLS-1$
								indent2 + "   ", //$NON-NLS-1$
								dependency.getBundle(),
								dependency.isFragment(),
								subdependencies.getDependencies());
					}
				}
			}
		}

		@Override
		public IPath getBundleBinaryPath() throws BundleException {
			if (this.binaryBundlePath == null) {
				getDependencyDefinition();
			}
			return this.binaryBundlePath;
		}

		@Override
		public Iterable<String> getDirectSymbolicNames() throws BundleException {
			return () -> {
				try {
					return new SymbolicNameIterator(getDirectDependencies());
				} catch (BundleException ex) {
					throw new RuntimeException(ex);
				}
			};
		}

		@Override
		public Iterable<IClasspathEntry> getDirectClasspathEntries() throws BundleException {
			return () -> {
				try {
					return new ClasspathEntryIterator(getDirectDependencies());
				} catch (BundleException ex) {
					throw new RuntimeException(ex);
				}
			};
		}

		@Override
		public Iterable<IRuntimeClasspathEntry> getDirectRuntimeClasspathEntries() throws BundleException {
			return () -> {
				try {
					return new RuntimeClasspathEntryIterator(getDirectDependencies());
				} catch (BundleException ex) {
					throw new RuntimeException(ex);
				}
			};
		}

		@Override
		public Iterable<String> getTransitiveSymbolicNames(boolean includeFragments) throws BundleException {
			return () -> {
				try {
					return new SymbolicNameIterator(getTransitiveDependencies(includeFragments));
				} catch (BundleException ex) {
					throw new RuntimeException(ex);
				}
			};
		}

		@Override
		public Iterable<IClasspathEntry> getTransitiveClasspathEntries(boolean includeFragments) throws BundleException {
			return () -> {
				try {
					return new ClasspathEntryIterator(getTransitiveDependencies(includeFragments));
				} catch (BundleException ex) {
					throw new RuntimeException(ex);
				}
			};
		}

		@Override
		public Iterable<IRuntimeClasspathEntry> getTransitiveRuntimeClasspathEntries(boolean includeFragments) throws BundleException {
			return () -> {
				try {
					return new RuntimeClasspathEntryIterator(getTransitiveDependencies(includeFragments));
				} catch (BundleException ex) {
					throw new RuntimeException(ex);
				}
			};
		}

		private IPath determineBundleBinaryPath(Bundle bundle) throws BundleException {
			final var bundlePath = Bundles.getBundlePath(bundle);
			if (bundlePath.toFile().isDirectory()) {
				// we have a directory, we assume we are in debug mode of the
				// product
				final var bundleSourcePath = Bundles.getSourceBundlePath(bundle, bundlePath);

				// Default value of the output folder for our project but will be
				// overload later in we find a .classpath precising the output
				// folder
				var outputFolder = bundleSourcePath;
				if (!bundlePath.equals(bundleSourcePath)) {
					outputFolder = Path.fromPortableString(bundleSourcePath.toPortableString().concat(DEFAULT_PATH_TO_CLASSES_IN_MAVEN_PROJECT));
				}
				URL bundleURL = null;
				try {
					bundleURL = new URI("file://" + bundleSourcePath.toPortableString()).toURL(); //$NON-NLS-1$
				} catch (Exception e) {
					return null;
				}
				final var classpathOutputFolder = readDotClasspathAndReferencestoClasspath(null, bundle, bundleURL);
				if (classpathOutputFolder != null) {
					outputFolder = classpathOutputFolder;
				}
				return outputFolder;
			}
			return bundlePath;
		}
		
		private DependencyDefinition getDependencyDefinition() throws BundleException {
			var dependencies = getBundleDependencies(this.bundle);
			if (dependencies == null) {
				final var cpEntries = createBundleDependencySet();

				this.binaryBundlePath = determineBundleBinaryPath(this.bundle);
				final var cpEntry = Utilities.newLibraryEntry(this.bundle, this.binaryBundlePath, null);
				updateBundleClassPath(this.bundle, cpEntry, cpEntries);
				
				if (this.extraDependencies != null && !this.extraDependencies.isEmpty()) {
					for (final var extraBundleName : this.extraDependencies) {
						final var extraBundle = Platform.getBundle(extraBundleName);
						if (extraBundle != null) {
							final var extraBinaryBundlePath = determineBundleBinaryPath(extraBundle);
							final var extraCpEntry = Utilities.newLibraryEntry(extraBundle, extraBinaryBundlePath, null);
							updateBundleClassPath(extraBundle, extraCpEntry, cpEntries);
						}
					}
				}
				
				setBundleDependencies(this.bundle, cpEntries, true);
			
				extractAllBundleDependencies(this.bundle, true);
				dependencies = getBundleDependencies(this.bundle);
			}
			return dependencies;
		}

		@Override
		public Version getBundleVersion() throws BundleException {
			final var dependencies = getDependencyDefinition();
			if (dependencies == null) {
				return null;
			}
			return dependencies.getVersion();
		}

		@Override
		public String getBundleSymbolicName() throws BundleException {
			return this.bundle.getSymbolicName();
		}

		@Override
		public Set<BundleDependency> getDirectDependencies() throws BundleException {
			final var dependencies = getDependencyDefinition();
			if (dependencies == null) {
				return null;
			}
			return Collections.unmodifiableSet(dependencies.getDependencies());
		}

		@Override
		public Iterable<BundleDependency> getTransitiveDependencies(boolean includeFragments) throws BundleException {
			final var dependencies = getDependencyDefinition();
			if (dependencies == null) {
				return Collections.emptyList();
			}
			return () -> new TransitiveDependencyIterator(dependencies.getDependencies(), includeFragments);
		}

		/** Add the given bundle to the entries.
		 *
		 * <p>This function add the classpath entry for the bundle, and the related fragments.
		 *
		 * @param bundle the bundle to point to. Never {@code null}.
		 * @param entry the classpath entry to add to. Never {@code null}.
		 * @param entries the list of entries to add to.
		 * @return the main added dependency.
		 * @throws BundleException if a bundle cannot be resolved
		 */
		private static BundleDependency updateBundleClassPath(Bundle bundle, IClasspathEntry entry, Set<BundleDependency> entries) throws BundleException {
			assert bundle != null;
			assert entry != null;
			final var rootDep = new BundleDependency(bundle, entry, false);
			entries.add(rootDep);
			final var fragments = Platform.getFragments(bundle);
			if (fragments != null && fragments.length > 0) {
				for (final var fragment : fragments) {
					final var fragmentEntry = Utilities.newLibraryEntry(fragment, null, null);
