/*******************************************************************************
 * Copyright (c) 2007, 2019 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.pde.api.tools.internal.provisional.comparator;

import java.util.HashSet;
import java.util.Set;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.jdt.core.Flags;
import org.eclipse.pde.api.tools.internal.builder.AbstractProblemDetector;
import org.eclipse.pde.api.tools.internal.comparator.ClassFileComparator;
import org.eclipse.pde.api.tools.internal.comparator.Delta;
import org.eclipse.pde.api.tools.internal.provisional.ApiPlugin;
import org.eclipse.pde.api.tools.internal.provisional.IApiAnnotations;
import org.eclipse.pde.api.tools.internal.provisional.IApiDescription;
import org.eclipse.pde.api.tools.internal.provisional.IRequiredComponentDescription;
import org.eclipse.pde.api.tools.internal.provisional.RestrictionModifiers;
import org.eclipse.pde.api.tools.internal.provisional.VisibilityModifiers;
import org.eclipse.pde.api.tools.internal.provisional.model.ApiTypeContainerVisitor;
import org.eclipse.pde.api.tools.internal.provisional.model.IApiBaseline;
import org.eclipse.pde.api.tools.internal.provisional.model.IApiComponent;
import org.eclipse.pde.api.tools.internal.provisional.model.IApiScope;
import org.eclipse.pde.api.tools.internal.provisional.model.IApiType;
import org.eclipse.pde.api.tools.internal.provisional.model.IApiTypeContainer;
import org.eclipse.pde.api.tools.internal.provisional.model.IApiTypeRoot;
import org.eclipse.pde.api.tools.internal.util.Util;
import org.osgi.framework.Version;

/**
 * This class defines a comparator to get a IDelta out of the comparison of two
 * elements.
 *
 * @since 1.0
 */
public class ApiComparator {
	/**
	 * Default empty delta
	 */
	public static final IDelta NO_DELTA = new Delta();

	/**
	 * Returns a delta for a API component version change
	 */
	static IDelta checkBundleVersionChanges(IApiComponent apiComponent2, String id, String apiComponentVersion, String apiComponentVersion2) {
		Version version = null;
		try {
			version = new Version(apiComponentVersion);
		} catch (IllegalArgumentException e) {
			// ignore
		}
		Version version2 = null;
		try {
			version2 = new Version(apiComponentVersion2);
		} catch (IllegalArgumentException e) {
			// ignore
		}
		if (version != null && version2 != null) {
			// add check for bundle versions
			if (version.getMajor() != version2.getMajor()) {
				return new Delta(Util.getDeltaComponentVersionsId(apiComponent2), IDelta.API_COMPONENT_ELEMENT_TYPE, IDelta.CHANGED, IDelta.MAJOR_VERSION, RestrictionModifiers.NO_RESTRICTIONS, RestrictionModifiers.NO_RESTRICTIONS, 0, 0, null, id, new String[] {
						id, apiComponentVersion, apiComponentVersion2 });
			} else if (version.getMinor() != version2.getMinor()) {
				return new Delta(Util.getDeltaComponentVersionsId(apiComponent2), IDelta.API_COMPONENT_ELEMENT_TYPE, IDelta.CHANGED, IDelta.MINOR_VERSION, RestrictionModifiers.NO_RESTRICTIONS, RestrictionModifiers.NO_RESTRICTIONS, 0, 0, null, id, new String[] {
						id, apiComponentVersion, apiComponentVersion2 });
			}
		}
		return null;
	}

	/**
	 * Returns a delta that corresponds to the difference between the given
	 * baseline and the reference.
	 *
	 * @param referenceBaseline the given API baseline which is used as the
	 *            reference
	 * @param baseline the given API baseline to compare with
	 * @param visibilityModifiers the given visibility that triggers what
	 *            visibility should be used for the comparison
	 * @param force a flag to force the comparison of nested API components with
	 *            the same versions
	 * @param monitor
	 *
	 * @return a delta, an empty delta if no difference is found or null if the
	 *         delta detection failed
	 * @throws IllegalArgumentException if one of the two baselines is null
	 */
	public static IDelta compare(final IApiBaseline referenceBaseline, final IApiBaseline baseline, final int visibilityModifiers, final boolean force, final IProgressMonitor monitor) {
		SubMonitor localmonitor = SubMonitor.convert(monitor, 2);
		if (referenceBaseline == null || baseline == null) {
			throw new IllegalArgumentException("None of the baselines must be null"); //$NON-NLS-1$
		}
		IApiComponent[] apiComponents = referenceBaseline.getApiComponents();
		IApiComponent[] apiComponents2 = baseline.getApiComponents();
		Set<String> apiComponentsIds = new HashSet<>();
		final Delta globalDelta = new Delta();
		SubMonitor apiLoopMonitor = localmonitor.split(1).setWorkRemaining(apiComponents.length);
		for (IApiComponent apiComponentMainLoop : apiComponents) {
			apiLoopMonitor.split(1);
			IApiComponent apiComponent = apiComponentMainLoop;
			if (!apiComponent.isSystemComponent()) {
				String id = apiComponent.getSymbolicName();
				IApiComponent apiComponentBaseline = baseline.getApiComponent(id);
				IDelta delta = null;
				if (apiComponentBaseline == null) {
					// report removal of an API component
					delta = new Delta(null, IDelta.API_BASELINE_ELEMENT_TYPE, IDelta.REMOVED, IDelta.API_COMPONENT,
							null, id, id);
				} else {
					apiComponentsIds.add(id);
					String versionString = apiComponent.getVersion();
					String versionString2 = apiComponentBaseline.getVersion();
					IDelta bundleVersionChangesDelta = checkBundleVersionChanges(apiComponentBaseline, id,
							versionString, versionString2);
					if (bundleVersionChangesDelta != null) {
						globalDelta.add(bundleVersionChangesDelta);
					}
					if (!versionString.equals(versionString2) || force) {
						long time = System.currentTimeMillis();
						try {
							delta = compare(apiComponent, apiComponentBaseline, referenceBaseline, baseline,
									visibilityModifiers, localmonitor.split(1));
						} finally {
							if (ApiPlugin.DEBUG_API_COMPARATOR) {
								System.out.println("Time spent for " + id + " " + versionString + " : " //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
										+ (System.currentTimeMillis() - time) + "ms"); //$NON-NLS-1$
							}
						}
					}
				}
				if (delta != null && delta != NO_DELTA) {
					globalDelta.add(delta);
				}
			}
		}
		SubMonitor elementLoopMonitor = localmonitor.split(1).setWorkRemaining(apiComponents2.length);
		for (IApiComponent element : apiComponents2) {
			elementLoopMonitor.split(1);
			IApiComponent apiComponent = element;
			if (!apiComponent.isSystemComponent()) {
				String id = apiComponent.getSymbolicName();
				if (!apiComponentsIds.contains(id)) {
					// addition of an API component
					globalDelta.add(new Delta(null, IDelta.API_BASELINE_ELEMENT_TYPE, IDelta.ADDED,
							IDelta.API_COMPONENT, null, id, id));
				}
			}
		}
		return globalDelta.isEmpty() ? NO_DELTA : globalDelta;
	}

	/**
	 * Returns a delta that corresponds to the difference between the given
	 * component and the reference baseline.
	 *
	 * @param component the given component to compare with the given reference
	 *            baseline
	 * @param referenceBaseline the given API baseline which is used as the
	 *            reference
	 * @param visibilityModifiers the given visibility that triggers what
	 *            visibility should be used for the comparison
	 * @param force a flag to force the comparison of nested API components with
	 *            the same versions
	 * @param monitor
	 *
	 * @return a delta, an empty delta if no difference is found or null if the
	 *         delta detection failed
	 * @exception IllegalArgumentException if:
	 *                <ul>
	 *                <li>the given component is null</li>
	 *                <li>the reference baseline is null</li>
	 *                </ul>
	 */
	public static IDelta compare(final IApiComponent component, final IApiBaseline referenceBaseline, final int visibilityModifiers, final boolean force, final IProgressMonitor monitor) {
		SubMonitor localmonitor = SubMonitor.convert(monitor, 2);
		if (component == null) {
			throw new IllegalArgumentException("The composent cannot be null"); //$NON-NLS-1$
		}
		if (referenceBaseline == null) {
			throw new IllegalArgumentException("The reference baseline cannot be null"); //$NON-NLS-1$
		}
		localmonitor.split(1);
		IDelta delta = null;
		if (!component.isSystemComponent()) {
			String id = component.getSymbolicName();
			IApiComponent apiComponent2 = referenceBaseline.getApiComponent(id);
			if (apiComponent2 == null) {
				// report addition of an API component
				delta = new Delta(null, IDelta.API_BASELINE_ELEMENT_TYPE, IDelta.ADDED, IDelta.API_COMPONENT, null, id,
						id);
			} else {
				if (!component.getVersion().equals(apiComponent2.getVersion()) || force) {
					long time = System.currentTimeMillis();
					try {
						delta = compare(apiComponent2, component, visibilityModifiers, localmonitor.split(1));
					} finally {
						if (ApiPlugin.DEBUG_API_COMPARATOR) {
							System.out.println("Time spent for " + id + " " + component.getVersion() + " : " //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
									+ (System.currentTimeMillis() - time) + "ms"); //$NON-NLS-1$
						}
					}
				}
			}
			if (delta != null && delta != NO_DELTA) {
				return delta;
			}
		}
		return NO_DELTA;
	}

	/**
	 * Returns a delta that corresponds to the comparison of the two given API
	 * components. The two components are compared even if their versions are
	 * identical.
	 *
	 * @param referenceComponent the given API component
	 * @param component2 the given API component to compare with
	 * @param referenceBaseline the given API baseline from which the given
	 *            component <code>component</code> is coming from
	 * @param baseline the given API baseline from which the given component
	 *            <code>component2</code> is coming from
	 * @param visibilityModifiers the given visibility that triggers what
	 *            visibility should be used for the comparison
	 * @param monitor
	 *
	 * @return a delta, an empty delta if no difference is found or null if the
	 *         delta detection failed
	 * @exception IllegalArgumentException if:
	 *                <ul>
	 *                <li>both given components are null</li>
	 *                <li>one of the baselines is null</li>
	 *                </ul>
	 */
	public static IDelta compare(final IApiComponent referenceComponent, final IApiComponent component2, final IApiBaseline referenceBaseline, final IApiBaseline baseline, final int visibilityModifiers, final IProgressMonitor monitor) {
		SubMonitor localmonitor = SubMonitor.convert(monitor, 3);
		try {
			if (referenceComponent == null) {
				if (component2 == null) {
					throw new IllegalArgumentException("Both components cannot be null"); //$NON-NLS-1$
				}
				return new Delta(null, IDelta.API_BASELINE_ELEMENT_TYPE, IDelta.ADDED, IDelta.API_COMPONENT, null, component2.getSymbolicName(), Util.getComponentVersionsId(component2));
			} else if (component2 == null) {
				String referenceComponentId = referenceComponent.getSymbolicName();
				return new Delta(null, IDelta.API_BASELINE_ELEMENT_TYPE, IDelta.REMOVED, IDelta.API_COMPONENT, null, referenceComponentId, Util.getComponentVersionsId(referenceComponent));
			}
			if (referenceBaseline == null || baseline == null) {
				throw new IllegalArgumentException("The baselines cannot be null"); //$NON-NLS-1$
			}
			String referenceComponentId = referenceComponent.getSymbolicName();
			final Delta globalDelta = new Delta();

			// check the EE first
			Set<String> referenceEEs = new HashSet<>(referenceComponent.getExecutionEnvironments());
			Set<String> componentsEEs = new HashSet<>(component2.getExecutionEnvironments());
			SubMonitor referencesLoopMonitor = localmonitor.split(1).setWorkRemaining(referenceEEs.size());
			for (String currentEE : referenceEEs) {
				referencesLoopMonitor.split(1);
				if (!componentsEEs.remove(currentEE)) {
					globalDelta.add(new Delta(Util.getDeltaComponentVersionsId(referenceComponent), IDelta.API_COMPONENT_ELEMENT_TYPE, IDelta.REMOVED, IDelta.EXECUTION_ENVIRONMENT, RestrictionModifiers.NO_RESTRICTIONS, RestrictionModifiers.NO_RESTRICTIONS, 0, 0, null, referenceComponentId, new String[] {
							currentEE,
							Util.getComponentVersionsId(referenceComponent) }));
				}
			}
			SubMonitor componentsLoopMonitor = localmonitor.split(1).setWorkRemaining(componentsEEs.size());
			for (String currentEE : componentsEEs) {
				componentsLoopMonitor.split(1);
				globalDelta.add(new Delta(Util.getDeltaComponentVersionsId(referenceComponent), IDelta.API_COMPONENT_ELEMENT_TYPE, IDelta.ADDED, IDelta.EXECUTION_ENVIRONMENT, RestrictionModifiers.NO_RESTRICTIONS, RestrictionModifiers.NO_RESTRICTIONS, 0, 0, null, referenceComponentId, new String[] {
						currentEE,
						Util.getComponentVersionsId(referenceComponent) }));
			}
			return internalCompare(referenceComponent, component2, referenceBaseline, baseline, visibilityModifiers, globalDelta, localmonitor.split(1));
		} catch (CoreException e) {
			// null means an error case
			return null;
		}
	}

	/**
	 * Returns a delta that corresponds to the difference between the given
	 * component and the given reference component. The given component cannot
	 * be null.
	 *
	 * @param referenceComponent the given API component that is used as the
	 *            reference
	 * @param component the given component to compare with
	 * @param visibilityModifiers the given visibility that triggers what
	 *            visibility should be used for the comparison
	 *
	 * @return a delta, an empty delta if no difference is found or null if the
	 *         delta detection failed
	 */
	public static IDelta compare(final IApiComponent referenceComponent, final IApiComponent component, final int visibilityModifiers, final IProgressMonitor monitor) {
		try {
			return compare(referenceComponent, component, referenceComponent == null ? null : referenceComponent.getBaseline(), component.getBaseline(), visibilityModifiers, monitor);
		} catch (CoreException e) {
			ApiPlugin.log(e);
		}
		return null;
	}

	/**
	 * Returns a delta that corresponds to the comparison of the given class
	 * file with the reference.
	 *
	 * @param typeRoot2 the given class file that comes from the
	 *            <code>component2</code>
	 * @param component the given API component from the reference
	 * @param component2 the given API component to compare with
	 * @param reexporter the API component re-exporting component2, or
	 *            <code>null</code> if none
	 * @param referenceBaseline the given API baseline from which the given
	 *            component <code>component</code> is coming from
	 * @param baseline the given API baseline from which the given component
	 *            <code>component2</code> is coming from
	 * @param visibilityModifiers the given visibility that triggers what
	 *            visibility should be used for the comparison
	 * @param monitor
	 *
	 * @return a delta, an empty delta if no difference is found or null if the
	 *         delta detection failed
	 * @exception IllegalArgumentException if:
	 *                <ul>
	 *                <li>the given class file is null</li>
	 *                <li>one of the given components is null</li>
	 *                <li>one of the given baselines is null</li>
	 *                </ul>
	 */
	public static IDelta compare(final IApiTypeRoot typeRoot2, final IApiComponent component, final IApiComponent component2, final IApiComponent reexporter, final IApiBaseline referenceBaseline, final IApiBaseline baseline, final int visibilityModifiers, final IProgressMonitor monitor) {

		if (typeRoot2 == null) {
			throw new IllegalArgumentException("The given class file is null"); //$NON-NLS-1$
		}
		if (component == null || component2 == null) {
			throw new IllegalArgumentException("One of the given components is null"); //$NON-NLS-1$
		}
		if (referenceBaseline == null || baseline == null) {
			throw new IllegalArgumentException("One of the given baselines is null"); //$NON-NLS-1$
		}
		SubMonitor localmonitor = SubMonitor.convert(monitor, 6);
		try {
			IApiType typeDescriptor2 = typeRoot2.getStructure();
			if (typeDescriptor2.isMemberType() || typeDescriptor2.isAnonymous() || typeDescriptor2.isLocal()) {
				// we skip nested types (member, local and anonymous)
				return NO_DELTA;
			}
			String typeName = typeRoot2.getTypeName();
			String id = component.getSymbolicName();
			IApiTypeRoot typeRoot = component.findTypeRoot(typeName, id);
			final IApiDescription apiDescription2 = component2.getApiDescription();
			IApiAnnotations elementDescription2 = apiDescription2.resolveAnnotations(typeDescriptor2.getHandle());
			int visibility = 0;
			if (elementDescription2 != null) {
				visibility = elementDescription2.getVisibility();
			}
			localmonitor.split(1);
			final IApiDescription referenceApiDescription = component.getApiDescription();
			IApiAnnotations refElementDescription = referenceApiDescription.resolveAnnotations(typeDescriptor2.getHandle());
			int refVisibility = 0;
			if (refElementDescription != null) {
				refVisibility = refElementDescription.getVisibility();
			}
			localmonitor.split(1);
			String deltaComponentID = Util.getDeltaComponentVersionsId(component2);
			if (typeRoot == null) {
				if (Util.isAPI(visibility, typeDescriptor2)) {
					return new Delta(deltaComponentID, IDelta.API_COMPONENT_ELEMENT_TYPE, IDelta.ADDED, reexporter == null ? IDelta.TYPE : IDelta.REEXPORTED_TYPE, elementDescription2 != null ? elementDescription2.getRestrictions() : RestrictionModifiers.NO_RESTRICTIONS, RestrictionModifiers.NO_RESTRICTIONS, 0, typeDescriptor2.getModifiers(), typeName, typeName, new String[] {
							typeName, Util.getComponentVersionsId(component2) });
				}
				return NO_DELTA;
			}
			localmonitor.split(1);
			IApiType typeDescriptor = typeRoot.getStructure();
			if ((visibility & visibilityModifiers) == 0) {
				if ((refVisibility & visibilityModifiers) == 0) {
					// no delta
					return NO_DELTA;
				}
				if (Util.isAPI(refVisibility, typeDescriptor)) {
					return new Delta(deltaComponentID, IDelta.API_COMPONENT_ELEMENT_TYPE, IDelta.REMOVED, IDelta.API_TYPE, elementDescription2 != null ? elementDescription2.getRestrictions() : RestrictionModifiers.NO_RESTRICTIONS, RestrictionModifiers.NO_RESTRICTIONS, typeDescriptor.getModifiers(), typeDescriptor2.getModifiers(), typeName, typeName, new String[] {
							typeName, Util.getComponentVersionsId(component2) });
				}
			} else if (!Util.isAPI(refVisibility, typeDescriptor) && Util.isAPI(visibility, typeDescriptor2)) {
				return new Delta(deltaComponentID, IDelta.API_COMPONENT_ELEMENT_TYPE, IDelta.ADDED, IDelta.TYPE, elementDescription2 != null ? elementDescription2.getRestrictions() : RestrictionModifiers.NO_RESTRICTIONS, RestrictionModifiers.NO_RESTRICTIONS, typeDescriptor.getModifiers(), typeDescriptor2.getModifiers(), typeName, typeName, new String[] {
						typeName, Util.getComponentVersionsId(component2) });
			}
			localmonitor.split(1);
			if (visibilityModifiers == VisibilityModifiers.API) {
				// if the visibility is API, we only consider public and
				// protected types
				if (Util.isDefault(typeDescriptor2.getModifiers()) || Flags.isPrivate(typeDescriptor2.getModifiers())) {
					// we need to check if the reference contains the type to
					// report a reduced visibility
					if (Flags.isPublic(typeDescriptor.getModifiers()) || Flags.isProtected(typeDescriptor.getModifiers())) {
						return new Delta(deltaComponentID, IDelta.API_COMPONENT_ELEMENT_TYPE, IDelta.REMOVED, IDelta.API_TYPE, elementDescription2 != null ? elementDescription2.getRestrictions() : RestrictionModifiers.NO_RESTRICTIONS, RestrictionModifiers.NO_RESTRICTIONS, typeDescriptor.getModifiers(), typeDescriptor2.getModifiers(), typeName, typeName, new String[] {
								typeName,
								Util.getComponentVersionsId(component2) });
					} else {
						return NO_DELTA;
					}
				}
			}
			localmonitor.split(1);
			ClassFileComparator comparator = new ClassFileComparator(typeDescriptor, typeRoot2, component, component2, referenceBaseline, baseline, visibilityModifiers);
			IDelta delta = comparator.getDelta();
			if (ApiPlugin.DEBUG_API_COMPARATOR) {
				IStatus status = comparator.getStatus();
				if (status != null) {
					ApiPlugin.log(status);
				}
			}
			return delta;
		} catch (CoreException e) {
			return null;
		}
	}

	/**
	 * Returns a delta that corresponds to the comparison of the given class
	 * file.
	 *
	 * @param typeRoot the given class file
	 * @param typeRoot2 the given class file to compare with
	 * @param component the given API component from which the given class file
	 *            is coming from
	 * @param component2 the given API component to compare with
	 * @param referenceBaseline the given API baseline from which the given
	 *            component <code>component</code> is coming from
	 * @param baseline the given API baseline from which the given component
	 *            <code>component2</code> is coming from
	 * @param visibilityModifiers the given visibility that triggers what
	 *            visibility should be used for the comparison
	 * @param monitor
	 *
	 * @return a delta, an empty delta if no difference is found or
	 *         <code>null</code> if the delta detection failed
	 * @exception IllegalArgumentException if:
	 *                <ul>
	 *                <li>one of the given components is null</li>
	 *                <li>one of the given baselines is null</li>
	 *                </ul>
	 */
	public static IDelta compare(final IApiTypeRoot typeRoot, final IApiTypeRoot typeRoot2, final IApiComponent component, final IApiComponent component2, final IApiBaseline referenceBaseline, final IApiBaseline baseline, final int visibilityModifiers, final IProgressMonitor monitor) {
		if (typeRoot == null || typeRoot2 == null) {
			throw new IllegalArgumentException("One of the given class files is null"); //$NON-NLS-1$
		}
		if (component == null || component2 == null) {
			throw new IllegalArgumentException("One of the given components is null"); //$NON-NLS-1$
		}
		if (referenceBaseline == null || baseline == null) {
			throw new IllegalArgumentException("One of the given baselines is null"); //$NON-NLS-1$
		}
		IDelta delta = null;
		try {
			ClassFileComparator comparator = new ClassFileComparator(typeRoot, typeRoot2, component, component2, referenceBaseline, baseline, visibilityModifiers);
			delta = comparator.getDelta();
			if (ApiPlugin.DEBUG_API_COMPARATOR) {
				IStatus status = comparator.getStatus();
				if (status != null) {
					ApiPlugin.log(status);
				}
			}
		} catch (CoreException e) {
			ApiPlugin.log(e);
		}
		return delta;
	}

	/**
	 * Returns a delta that corresponds to the comparison of the two given API
	 * baselines. Nested API components with the same versions are not compared.
	 * <p>
	 * Equivalent to: compare(baseline, baseline2, visibilityModifiers, false,
	 * false, monitor);
	 * </p>
	 *
	 * @param scope the given scope for the comparison
	 * @param baseline the given API baseline to compare with
	 * @param visibilityModifiers the given visibility that triggers what
	 *            visibility should be used for the comparison
	 * @param force a flag to force the comparison of nested API components with
	 *            the same versions
	 * @param monitor the given progress monitor to report progress
	 *
	 * @return a delta, an empty delta if no difference is found or null if the
	 *         delta detection failed
	 * @throws IllegalArgumentException if one of the two baselines is null
	 *             CoreException if one of the element in the scope cannot be
	 *             visited
	 */
	public static IDelta compare(final IApiScope scope, final IApiBaseline baseline, final int visibilityModifiers, final boolean force, final IProgressMonitor monitor) throws CoreException {
		return compare(scope, baseline, visibilityModifiers, force, false, monitor);
	}

	/**
	 * Returns a delta that corresponds to the comparison of the two given API
	 * baselines. Nested API components with the same versions are not compared.
	 * <p>
	 * Equivalent to: compare(baseline, baseline2, visibilityModifiers, false);
	 * </p>
	 *
	 * @param scope the given scope for the comparison
	 * @param baseline the given API baseline to compare with
	 * @param visibilityModifiers the given visibility that triggers what
	 *            visibility should be used for the comparison
	 * @param force a flag to force the comparison of nested API components with
	 *            the same versions
	 * @param continueOnResolverError if <code>true</code> the comparison will
	 *            continue even if a component in the scope has a resolver error
	 * @param monitor the given progress monitor to report progress
	 *
	 * @return a delta, an empty delta if no difference is found or null if the
	 *         delta detection failed. If set to continue on resolver error a
	 *         delta, possibly empty, will always be returned
	 * @throws IllegalArgumentException if one of the two baselines is null
	 *             CoreException if one of the element in the scope cannot be
	 *             visited
	 */
	public static IDelta compare(final IApiScope scope, final IApiBaseline baseline, final int visibilityModifiers, final boolean force, final boolean continueOnResolverError, final IProgressMonitor monitor) throws CoreException {

		if (scope == null || baseline == null) {
			throw new IllegalArgumentException("None of the scope or the baseline must be null"); //$NON-NLS-1$
		}
		SubMonitor localmonitor = SubMonitor.convert(monitor, 2);
		final Set<IDelta> deltas = new HashSet<>();
		final CompareApiScopeVisitor visitor = new CompareApiScopeVisitor(deltas, baseline, force, visibilityModifiers,
				continueOnResolverError, localmonitor.split(1));
		scope.accept(visitor);

		// If set to continue on error, return whatever deltas were
		// collected
		if (!continueOnResolverError && visitor.containsError()) {
			return null;
		}
		if (deltas.isEmpty()) {
			return NO_DELTA;
		}
		final Delta globalDelta = new Delta();
		for (IDelta iDelta : deltas) {
			iDelta.accept(new DeltaVisitor() {
				@Override
				public void endVisit(IDelta localDelta) {
					if (localDelta.getChildren().length == 0) {
						switch (localDelta.getElementType())
							{
							case IDelta.ANNOTATION_ELEMENT_TYPE:
							case IDelta.ENUM_ELEMENT_TYPE:
							case IDelta.CONSTRUCTOR_ELEMENT_TYPE:
							case IDelta.METHOD_ELEMENT_TYPE:
							case IDelta.INTERFACE_ELEMENT_TYPE:
							case IDelta.CLASS_ELEMENT_TYPE:
							case IDelta.FIELD_ELEMENT_TYPE:
							case IDelta.API_COMPONENT_ELEMENT_TYPE:
							case IDelta.API_BASELINE_ELEMENT_TYPE: {
								globalDelta.add(localDelta);
								break;
							}
							default:
								break;
							}
					}
				}
			});
		}
		localmonitor.split(1);
		return globalDelta.isEmpty() ? NO_DELTA : globalDelta;
	}

	/**
	 * Returns true, if the given type descriptor should be skipped, false
	 * otherwise.
	 */
	static boolean filterType(final int visibilityModifiers, IApiAnnotations elementDescription, IApiType typeDescriptor) {
		if (elementDescription != null && (elementDescription.getVisibility() & visibilityModifiers) == 0) {
			// we skip the class file according to their visibility
			return true;
		}
		if (visibilityModifiers == VisibilityModifiers.API) {
			// if the visibility is API, we only consider public and protected
			// types or types
			// without element description
			if (elementDescription == null || Util.isDefault(typeDescriptor.getModifiers()) || Flags.isPrivate(typeDescriptor.getModifiers())) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Performs the internal compare of the given {@link IApiComponent}s using
	 * their type containers
	 *
	 * @param monitor
	 *
	 * @return a delta of changed API elements
	 */
	private static IDelta internalCompare(final IApiComponent component, final IApiComponent component2, final IApiBaseline referenceBaseline, final IApiBaseline baseline, final int visibilityModifiers, final Delta globalDelta, final IProgressMonitor monitor) throws CoreException {
		final Set<String> typeRootBaseLineNames = new HashSet<>();
		final String id = component.getSymbolicName();
		final SubMonitor localmonitor = SubMonitor.convert(monitor, 4);
		IApiTypeContainer[] typeRootContainers = component.getApiTypeContainers(id);
		IApiTypeContainer[] typeRootContainers2 = component2.getApiTypeContainers(id);
		final IApiDescription apiDescription = component.getApiDescription();
		final IApiDescription apiDescription2 = component2.getApiDescription();
		if (typeRootContainers != null) {
			SubMonitor loopMonitor = localmonitor.split(1).setWorkRemaining(typeRootContainers.length);
			for (IApiTypeContainer container : typeRootContainers) {
				SubMonitor iterationMonitor = loopMonitor.split(1);
				try {
					container.accept(new ApiTypeContainerVisitor() {
						@Override
						public void visit(String packageName, IApiTypeRoot typeRoot) {
							SubMonitor visitMonitor = iterationMonitor.setWorkRemaining(50).split(1).setWorkRemaining(2);
							String typeName = typeRoot.getTypeName();
							try {
								IApiType typeDescriptor = typeRoot.getStructure();
								IApiAnnotations elementDescription = apiDescription.resolveAnnotations(typeDescriptor.getHandle());
								if (typeDescriptor.isMemberType() || typeDescriptor.isAnonymous() || typeDescriptor.isLocal()) {
									// we skip nested types (member, local and
									// anonymous)
									return;
								}
								int visibility;
								if (elementDescription != null) {
									visibility = elementDescription.getVisibility();
								} else {
									// Annotation is missing, not an API?
									visibility = 0;
								}
								IApiTypeRoot typeRoot2 = component2.findTypeRoot(typeName, id);
								IApiComponent provider = null;
								IApiDescription providerApiDesc = null;
								boolean reexported = false;
								if (typeRoot2 == null) {
									// check if the type is provided by a
									// required component (it could have been
									// moved/re-exported)
									IApiComponent[] providers = component2.getBaseline().resolvePackage(component2, packageName);
									SubMonitor providerMonitor = visitMonitor.split(1).setWorkRemaining(providers.length);
									int index = 0;
									while (typeRoot2 == null && index < providers.length) {
										providerMonitor.split(1);
										IApiComponent p = providers[index];
										if (!p.equals(component2)) {
											String id2 = p.getSymbolicName();
											typeRoot2 = p.findTypeRoot(typeName, id2);
											if (typeRoot2 != null) {
												provider = p;
												providerApiDesc = p.getApiDescription();
												IRequiredComponentDescription[] required = component2.getRequiredComponents();
												for (IRequiredComponentDescription description : required) {
													if (description.getId().equals(id2)) {
														reexported = description.isExported();
														break;
													}
												}
											}
										}
										index++;
									}
								} else {
									provider = component2;
									providerApiDesc = apiDescription2;
								}
								visitMonitor.setWorkRemaining(1).split(1);
								String deltaComponentID = Util.getDeltaComponentVersionsId(component2);
								if (typeRoot2 == null) {
									if ((visibility & visibilityModifiers) == 0) {
										// we skip the class file according to
										// their visibility
										return;
									}
									if (visibilityModifiers == VisibilityModifiers.API) {
										// if the visibility is API, we only
										// consider public and protected types
										if (Util.isDefault(typeDescriptor.getModifiers()) || Flags.isPrivate(typeDescriptor.getModifiers())) {
											return;
										}
									}
									globalDelta.add(new Delta(deltaComponentID, IDelta.API_COMPONENT_ELEMENT_TYPE, IDelta.REMOVED, IDelta.TYPE, RestrictionModifiers.NO_RESTRICTIONS, RestrictionModifiers.NO_RESTRICTIONS, typeDescriptor.getModifiers(), 0, typeName, typeName, new String[] {
											typeName,
													component2.isFragment()
															? Util.getComponentVersionsId(component2.getHost())
															: Util.getComponentVersionsId(component2) }));
								} else {
									if ((visibility & visibilityModifiers) == 0) {
										// we skip the class file according to
										// their visibility
										return;
									}
									IApiType typeDescriptor2 = typeRoot2.getStructure();
									IApiAnnotations elementDescription2 = providerApiDesc.resolveAnnotations(typeDescriptor2.getHandle());
									int visibility2 = 0;
									if (elementDescription2 != null) {
										visibility2 = elementDescription2.getVisibility();
									}
									if (visibilityModifiers == VisibilityModifiers.API) {
										// if the visibility is API, we only
										// consider public and protected types
										if (Util.isDefault(typeDescriptor.getModifiers()) || Flags.isPrivate(typeDescriptor.getModifiers())) {
											return;
										}
									}
									if (Util.isAPI(visibility, typeDescriptor)) {
										if (!Util.isAPI(visibility2, typeDescriptor2)) {
											globalDelta.add(new Delta(deltaComponentID, IDelta.API_COMPONENT_ELEMENT_TYPE, IDelta.REMOVED, reexported ? IDelta.REEXPORTED_API_TYPE : IDelta.API_TYPE, elementDescription2 != null ? elementDescription2.getRestrictions() : RestrictionModifiers.NO_RESTRICTIONS, RestrictionModifiers.NO_RESTRICTIONS, typeDescriptor.getModifiers(), typeDescriptor2.getModifiers(), typeName, typeName, new String[] {
													typeName,
													Util.getComponentVersionsId(component2) }));
											return;
										}
									}
									if ((visibility2 & visibilityModifiers) == 0) {
										// we simply report a changed visibility
										globalDelta.add(new Delta(deltaComponentID, IDelta.API_COMPONENT_ELEMENT_TYPE, IDelta.CHANGED, IDelta.TYPE_VISIBILITY, elementDescription2 != null ? elementDescription2.getRestrictions() : RestrictionModifiers.NO_RESTRICTIONS, RestrictionModifiers.NO_RESTRICTIONS, typeDescriptor.getModifiers(), typeDescriptor2.getModifiers(), typeName, typeName, new String[] {
												typeName,
												Util.getComponentVersionsId(component2) }));
									}
									typeRootBaseLineNames.add(typeName);
									ClassFileComparator comparator = new ClassFileComparator(typeDescriptor, typeRoot2, component, provider, referenceBaseline, baseline, visibilityModifiers);
									IDelta delta = comparator.getDelta();
									if (ApiPlugin.DEBUG_API_COMPARATOR) {
										IStatus status = comparator.getStatus();
										if (status != null) {
											ApiPlugin.log(status);
										}
									}
									if (delta != null && delta != NO_DELTA) {
										globalDelta.add(delta);
									}
								}
							} catch (CoreException e) {
								ApiPlugin.log(e);
								AbstractProblemDetector.checkIfDisposed(container.getApiComponent(), iterationMonitor);
							}
						}
					});
				} catch (CoreException e) {
					ApiPlugin.log(e);
					AbstractProblemDetector.checkIfDisposed(container.getApiComponent(), loopMonitor);
				}
			}
		}
		localmonitor.setWorkRemaining(3);
		IRequiredComponentDescription[] requiredComponents = component.getRequiredComponents();
		int length = requiredComponents.length;
		if (length != 0) {
			SubMonitor loopMonitor = localmonitor.split(1).setWorkRemaining(length);
			for (int j = 0; j < length; j++) {
				SubMonitor iterationMonitor = loopMonitor.split(1).setWorkRemaining(1);
				IRequiredComponentDescription description = requiredComponents[j];
				if (description.isExported()) {
					final String currentComponentID = Util.getDeltaComponentVersionsId(component);
					String descriptionID = description.getId();
					IApiComponent currentRequiredApiComponent = referenceBaseline.getApiComponent(descriptionID);
					if (currentRequiredApiComponent == null) {
						continue;
					}
					final IApiDescription reexportedApiDescription = currentRequiredApiComponent.getApiDescription();
					IApiTypeContainer[] apiTypeContainers = currentRequiredApiComponent.getApiTypeContainers();
					if (apiTypeContainers != null) {
						SubMonitor apiContainerLoopMonitor = iterationMonitor.split(1).setWorkRemaining(apiTypeContainers.length);
						for (IApiTypeContainer container : apiTypeContainers) {
							SubMonitor apiContainerIterationMonitor = apiContainerLoopMonitor.split(1);
							try {
								container.accept(new ApiTypeContainerVisitor() {
									@Override
									public void visit(String packageName, IApiTypeRoot typeRoot) {
										apiContainerIterationMonitor.setWorkRemaining(50).split(1);
										String typeName = typeRoot.getTypeName();
										try {
											IApiType typeDescriptor = typeRoot.getStructure();
											IApiAnnotations elementDescription = reexportedApiDescription.resolveAnnotations(typeDescriptor.getHandle());
											if (typeDescriptor.isMemberType() || typeDescriptor.isAnonymous() || typeDescriptor.isLocal()) {
												// we skip nested types (member,
												// local and anonymous)
												return;
											}
											int visibility = 0;
											if (elementDescription != null) {
												visibility = elementDescription.getVisibility();
											}
											IApiTypeRoot typeRoot2 = component2.findTypeRoot(typeName, id);
											IApiDescription providerApiDesc = null;
											if (typeRoot2 == null) {
												// check if the type is provided
												// by a required component (it
												// could have been
												// moved/re-exported)
												IApiComponent[] providers = component2.getBaseline().resolvePackage(component2, packageName);
												int index = 0;
												while (typeRoot2 == null && index < providers.length) {
													IApiComponent p = providers[index];
													if (!p.equals(component2)) {
														String id2 = p.getSymbolicName();
														typeRoot2 = p.findTypeRoot(typeName, id2);
														if (typeRoot2 != null) {
															providerApiDesc = p.getApiDescription();
														}
													}
													index++;
												}
											} else {
												providerApiDesc = apiDescription2;
											}
											if (typeRoot2 == null) {
												if ((visibility & visibilityModifiers) == 0) {
													// we skip the class file
													// according to their
													// visibility
													return;
												}
												if (visibilityModifiers == VisibilityModifiers.API) {
													// if the visibility is API,
													// we only consider public
													// and protected types
													if (Util.isDefault(typeDescriptor.getModifiers()) || Flags.isPrivate(typeDescriptor.getModifiers())) {
														return;
													}
												}
												globalDelta.add(new Delta(currentComponentID, IDelta.API_COMPONENT_ELEMENT_TYPE, IDelta.REMOVED, IDelta.REEXPORTED_TYPE, RestrictionModifiers.NO_RESTRICTIONS, RestrictionModifiers.NO_RESTRICTIONS, typeDescriptor.getModifiers(), 0, typeName, typeName, new String[] {
														typeName,
														Util.getComponentVersionsId(component) }));
											} else {
												typeRootBaseLineNames.add(typeName);
												IApiType typeDescriptor2 = typeRoot2.getStructure();
												IApiAnnotations elementDescription2 = providerApiDesc.resolveAnnotations(typeDescriptor2.getHandle());
												int visibility2 = 0;
												if (elementDescription2 != null) {
													visibility2 = elementDescription2.getVisibility();
												}
												// if the visibility is API, we
												// only consider public and
												// protected types
												if (Util.isDefault(typeDescriptor.getModifiers()) || Flags.isPrivate(typeDescriptor.getModifiers())) {
													return;
												}
												if (Util.isAPI(visibility, typeDescriptor)) {
													if (!Util.isAPI(visibility2, typeDescriptor2)) {
														globalDelta.add(new Delta(currentComponentID, IDelta.API_COMPONENT_ELEMENT_TYPE, IDelta.REMOVED, IDelta.REEXPORTED_API_TYPE, elementDescription2 != null ? elementDescription2.getRestrictions() : RestrictionModifiers.NO_RESTRICTIONS, RestrictionModifiers.NO_RESTRICTIONS, typeDescriptor.getModifiers(), typeDescriptor2.getModifiers(), typeName, typeName, new String[] {
																typeName,
																Util.getComponentVersionsId(component) }));
														return;
													}
												}
											}
										} catch (CoreException e) {
											ApiPlugin.log(e);
											AbstractProblemDetector.checkIfDisposed(container.getApiComponent(), apiContainerIterationMonitor);
										}
									}
								});
							} catch (CoreException e) {
								ApiPlugin.log(e);
								AbstractProblemDetector.checkIfDisposed(container.getApiComponent(), apiContainerIterationMonitor);
							}
						}
					}
				}
			}
		}
		localmonitor.setWorkRemaining(2);
		if (typeRootContainers2 != null) {
			SubMonitor loopMonitor = localmonitor.split(1).setWorkRemaining(typeRootContainers2.length);
			for (IApiTypeContainer container : typeRootContainers2) {
				SubMonitor iterationMonitor = loopMonitor.split(1);
				try {
					container.accept(new ApiTypeContainerVisitor() {
						@Override
						public void visit(String packageName, IApiTypeRoot typeRoot) {
							iterationMonitor.setWorkRemaining(50).split(1);
							String typeName = typeRoot.getTypeName();
							try {
								IApiType type = typeRoot.getStructure();
								IApiAnnotations elementDescription = apiDescription2.resolveAnnotations(type.getHandle());
								if (type.isMemberType() || type.isLocal() || type.isAnonymous()) {
									// we skip nested types (member, local and
									// anonymous)
									return;
								}
								if (filterType(visibilityModifiers, elementDescription, type)) {
									return;
								}
								if (typeRootBaseLineNames.contains(typeName)) {
									// already processed
									return;
								}
								typeRootBaseLineNames.add(typeName);
								String deltaComponentID = Util.getDeltaComponentVersionsId(component2);
								globalDelta.add(new Delta(deltaComponentID, IDelta.API_COMPONENT_ELEMENT_TYPE, IDelta.ADDED, IDelta.TYPE, elementDescription != null ? elementDescription.getRestrictions() : RestrictionModifiers.NO_RESTRICTIONS, RestrictionModifiers.NO_RESTRICTIONS, 0, type.getModifiers(), typeName, typeName, new String[] {
										typeName,
										Util.getComponentVersionsId(component2) }));
							} catch (CoreException e) {
								ApiPlugin.log(e);
								AbstractProblemDetector.checkIfDisposed(container.getApiComponent(), iterationMonitor);
							}
						}
					});
				} catch (CoreException e) {
					ApiPlugin.log(e);
					AbstractProblemDetector.checkIfDisposed(container.getApiComponent(), iterationMonitor);
				}
			}
		}
		localmonitor.setWorkRemaining(1);
		requiredComponents = component2.getRequiredComponents();
		length = requiredComponents.length;
		if (length != 0) {
			SubMonitor loopMonitor = localmonitor.split(1).setWorkRemaining(length);
			for (int j = 0; j < length; j++) {
				SubMonitor iterationMonitor = loopMonitor.split(1).setWorkRemaining(1);
				IRequiredComponentDescription description = requiredComponents[j];
				if (description.isExported()) {
					final String currentComponentID = Util.getDeltaComponentVersionsId(component);
					String descriptionID = description.getId();
					IApiComponent currentRequiredApiComponent = baseline.getApiComponent(descriptionID);
					if (currentRequiredApiComponent == null) {
						continue;
					}
					IApiTypeContainer[] apiTypeContainers = currentRequiredApiComponent.getApiTypeContainers();
					final IApiDescription reexportedApiDescription = currentRequiredApiComponent.getApiDescription();
					if (apiTypeContainers != null) {
						SubMonitor typeContainerLoopMonitor = iterationMonitor.split(1).setWorkRemaining(apiTypeContainers.length);
						for (IApiTypeContainer container : apiTypeContainers) {
							SubMonitor typeContainerIterationMonitor = typeContainerLoopMonitor.split(1);
							try {
								container.accept(new ApiTypeContainerVisitor() {
									@Override
									public void visit(String packageName, IApiTypeRoot typeRoot) {
										typeContainerIterationMonitor.setWorkRemaining(50).split(1);
										String typeName = typeRoot.getTypeName();
										try {
											IApiType typeDescriptor = typeRoot.getStructure();
											IApiAnnotations elementDescription = reexportedApiDescription.resolveAnnotations(typeDescriptor.getHandle());
											if (typeDescriptor.isMemberType() || typeDescriptor.isAnonymous() || typeDescriptor.isLocal()) {
												// we skip nested types (member,
												// local and anonymous)
												return;
											}
											if (filterType(visibilityModifiers, elementDescription, typeDescriptor)) {
												return;
											}
											if (typeRootBaseLineNames.contains(typeName)) {
												// already processed
												return;
											}
											typeRootBaseLineNames.add(typeName);
											globalDelta.add(new Delta(currentComponentID, IDelta.API_COMPONENT_ELEMENT_TYPE, IDelta.ADDED, IDelta.REEXPORTED_TYPE, elementDescription != null ? elementDescription.getRestrictions() : RestrictionModifiers.NO_RESTRICTIONS, RestrictionModifiers.NO_RESTRICTIONS, 0, typeDescriptor.getModifiers(), typeName, typeName, new String[] {
													typeName,
													Util.getComponentVersionsId(component) }));
										} catch (CoreException e) {
											ApiPlugin.log(e);
											AbstractProblemDetector.checkIfDisposed(container.getApiComponent(), typeContainerIterationMonitor);
										}
									}
								});
							} catch (CoreException e) {
								ApiPlugin.log(e);
								AbstractProblemDetector.checkIfDisposed(container.getApiComponent(), typeContainerIterationMonitor);
							}
						}
					}
				}
			}
		}
		return globalDelta.isEmpty() ? NO_DELTA : globalDelta;
	}
}
