/*******************************************************************************
 * Copyright (c) 2009, 2015 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.search;

import java.util.Set;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.pde.api.tools.internal.AntFilterStore;
import org.eclipse.pde.api.tools.internal.builder.AbstractProblemDetector;
import org.eclipse.pde.api.tools.internal.builder.ProblemDetectorBuilder;
import org.eclipse.pde.api.tools.internal.builder.Reference;
import org.eclipse.pde.api.tools.internal.builder.ReferenceAnalyzer;
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.IApiFilterStore;
import org.eclipse.pde.api.tools.internal.provisional.VisibilityModifiers;
import org.eclipse.pde.api.tools.internal.provisional.builder.IApiProblemDetector;
import org.eclipse.pde.api.tools.internal.provisional.builder.IReference;
import org.eclipse.pde.api.tools.internal.provisional.comparator.ApiScope;
import org.eclipse.pde.api.tools.internal.provisional.model.IApiComponent;
import org.eclipse.pde.api.tools.internal.provisional.model.IApiElement;
import org.eclipse.pde.api.tools.internal.provisional.model.IApiMember;
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.problems.IApiProblem;
import org.eclipse.pde.api.tools.internal.provisional.search.ApiSearchEngine;
import org.eclipse.pde.api.tools.internal.provisional.search.IApiSearchRequestor;

/**
 * Default implementation of an {@link IApiSearchRequestor} to use with the
 * {@link ApiSearchEngine}. This requestor returns a search scope composed of
 * the dependent (visible) {@link IApiComponent}s for the given
 * {@link IApiElement}
 *
 * <p>
 * The references are filtered based on api filter stores. The filters may come
 * from a .api_filters found in the component or a separate filter location set
 * in the ant task via {@link #setFilterRoot(String)}. If filter files are found
 * in both locations, the filters at both will be applied.
 * </p>
 *
 * @since 1.0.0
 */
public class UseSearchRequestor implements IApiSearchRequestor {

	/**
	 * The backing elements to search with
	 */
	private Set<String> fComponentIds = null;

	/**
	 * The current {@link IApiFilterStore} from the current
	 * {@link IApiComponent} context we are visiting.
	 */
	private IApiFilterStore currentStore = null;

	/**
	 * The current {@link IApiFilterStore} for the current {@link IApiComponent}
	 * context that we are visiting. The filter store will be created by finding
	 * each component's filter file in the root filter location
	 * {@link #antFilterRoot}.
	 */
	private IApiFilterStore antStore = null;

	/**
	 * The root directory of the .api_filters files that should be used to
	 * filter references.
	 *
	 * The .api_filters files specify specific problems to ignore during api
	 * analysis. During the use scan, the problem filters will be used to filter
	 * the use scan results.
	 *
	 * The root is specified using an absolute path. The root needs to contain
	 * the following structure:
	 *
	 * <pre>
	 * root
	 *  |
	 *  +-- component name (i.e. org.eclipse.jface)
	 *         |
	 *         +--- .api_filters
	 * </pre>
	 */
	private String antFilterRoot = null;

	/**
	 * The mask to use while searching
	 */
	private int fSearchMask = 0;

	/**
	 * The search scope for this requestor
	 */
	private IApiScope fScope = null;

	/**
	 * Patterns for jar API type roots to not scan
	 */
	private String[] jarPatterns = null;

	/**
	 * The default {@link ReferenceAnalyzer} for detecting illegal API use
	 *
	 * @see #includesIllegalUse()
	 */
	ReferenceAnalyzer fAnalyzer = null;

	/**
	 * Constructor
	 *
	 * @param elementnames an array of {@link IApiElement}s for the search engine to
	 *                     use
	 * @param scope        the raw list of {@link IApiElement}s to extract
	 *                     references from
	 * @param searchkinds  the kinds of references to search for. <br>
	 *                     Options include:
	 *                     <ol>
	 *                     <li>{@link #INCLUDE_API}</li>
	 *                     <li>{@link #INCLUDE_INTERNAL}</li>
	 *                     </ol>
	 */
	public UseSearchRequestor(Set<String> elementnames, IApiElement[] scope, int searchkinds) {
		fSearchMask = searchkinds;
		fComponentIds = elementnames;
		fAnalyzer = new ReferenceAnalyzer();
		prepareScope(scope);
	}

	@Override
	public boolean acceptComponent(IApiComponent component) {
		try {
			if (!component.isSystemComponent() && getScope().encloses(component)) {
				if (includesIllegalUse()) {
					fAnalyzer.buildProblemDetectors(component, ProblemDetectorBuilder.K_USE, null);
				}
				currentStore = component.getFilterStore();
				antStore = antFilterRoot != null ? new AntFilterStore(antFilterRoot, component.getSymbolicName()) : null;
				return true;
			}
		} catch (CoreException ce) {
			// do nothing, return false
		}
		currentStore = null;
		return false;
	}

	@Override
	public boolean acceptContainer(IApiTypeContainer container) {
		return considerTypeContainer(container);
	}

	@Override
	public boolean acceptMember(IApiMember member) {
		// don't consider inner types, as they are considered with the root type
		if (member instanceof IApiType type) {
			return !(type.isMemberType() || type.isLocal());
		}
		return true;
	}

	/**
	 * Returns if the given {@link IApiTypeContainer} should be processed
	 *
	 * @return true if the container should be processed false otherwise
	 */
	boolean considerTypeContainer(IApiTypeContainer container) {
		if (jarPatterns != null && container != null) {
			if (container.getContainerType() == IApiTypeContainer.ARCHIVE) {
				String[] pparts = null;
				for (String jarPattern : jarPatterns) {
					pparts = jarPattern.split(":"); //$NON-NLS-1$
					if (pparts.length != 2) {
						continue;
					}
					if (container.getApiComponent().getSymbolicName().equals(pparts[0])) {
						if (container.getName().endsWith(pparts[1])) {
							return false;
						}
					}
				}
			}
		}
		return true;
	}

	@Override
	public boolean acceptReference(IReference reference, IProgressMonitor monitor) {
		try {
			IApiMember member = reference.getResolvedReference();
			if (member != null) {
				IApiComponent component = member.getApiComponent();
				if (!fComponentIds.contains(component.getSymbolicName()) || component.equals(reference.getMember().getApiComponent())) {
					return false;
				}
				if (isIllegalUse(reference, monitor) || (includesAPI() && includesInternal())) {
					return true;
				}
				IApiAnnotations annots = component.getApiDescription().resolveAnnotations(member.getHandle());
				if (annots != null) {
					int vis = annots.getVisibility();
					if (VisibilityModifiers.isAPI(vis) && includesAPI()) {
						return true;
					} else if (VisibilityModifiers.isPrivate(vis) && includesInternal()) {
						return true;
					}
				}
			}
		} catch (CoreException ce) {
			ApiPlugin.log(ce);
			AbstractProblemDetector.checkIfDisposed(reference.getMember().getApiComponent(), monitor);
		}
		return false;
	}

	/**
	 * Returns true if the given reference is an illegal usage reference iff
	 * illegal use is part of the search mask.
	 *
	 * @return true if the reference is illegal use false otherwise
	 * @since 1.1
	 */
	boolean isIllegalUse(IReference reference, IProgressMonitor monitor) {
		IApiProblemDetector[] detectors = fAnalyzer.getProblemDetectors(reference.getReferenceKind());
		for (IApiProblemDetector detector : detectors) {
			if (monitor.isCanceled()) {
				break;
			}
			if (detector.considerReference(reference, monitor)) {
				Reference ref = (Reference) reference;
				ref.setFlags(IReference.F_ILLEGAL);
				try {
					IApiProblem pb = ((AbstractProblemDetector) detector).checkAndCreateProblem(reference, monitor);
					if (pb != null && !isFiltered(pb)) {
						ref.addProblems(pb);
					} else {
						return false;
					}
				} catch (CoreException e) {
					ApiPlugin.log(e);
					AbstractProblemDetector.checkIfDisposed(reference.getMember().getApiComponent(), monitor);
				}
				return true;
			}
		}
		return false;
	}

	/**
	 * Returns if the given problem is filtered
	 *
	 * @return <code>true</code> is filtered, false otherwise
	 */
	boolean isFiltered(IApiProblem problem) {
		return (currentStore != null && currentStore.isFiltered(problem)) || (antStore != null && antStore.isFiltered(problem));
	}

	@Override
	public int getReferenceKinds() {
		return IReference.MASK_REF_ALL & ~IReference.REF_CONSTANTPOOL;
	}

	/**
	 * Prepares the search scope based on the available entries in the
	 * constructor
	 */
	private void prepareScope(IApiElement[] elements) {
		if (elements != null) {
			fScope = new ApiScope();
			for (IApiElement element : elements) {
				fScope.addElement(element.getApiComponent());
			}
		}
	}

	@Override
	public IApiScope getScope() {
		return fScope;
	}

	@Override
	public boolean includesAPI() {
		return (fSearchMask & INCLUDE_API) > 0;
	}

	@Override
	public boolean includesInternal() {
		return (fSearchMask & INCLUDE_INTERNAL) > 0;
	}

	@Override
	public boolean includesIllegalUse() {
		return (fSearchMask & INCLUDE_ILLEGAL_USE) > 0;
	}

	/**
	 * The patterns for jar names to exclude from the search
	 */
	public void setJarPatterns(String[] patterns) {
		jarPatterns = patterns;
	}

	/**
	 * Sets the root directory of the .api_filters files that should be used to
	 * filter references.
	 *
	 * The .api_filters files specify specific problems to ignore during api
	 * analysis. During the use scan, the problem filters will be used to filter
	 * the use scan results. If .api_filter files are found inside the component
	 * those filters will be applied in addition to any found at this filter
	 * root.
	 *
	 * The root is specified using an absolute path. The root needs to contain
	 * the following structure:
	 *
	 * <pre>
	 * root
	 *  |
	 *  +-- component name (i.e. org.eclipse.jface)
	 *         |
	 *         +--- .api_filters
	 * </pre>
	 *
	 * @param filterRoot the absolute string path to the root of the filters
	 */
	public void setFilterRoot(String filterRoot) {
		antFilterRoot = filterRoot;
	}
}
