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

import java.io.File;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.text.DateFormat;
import java.util.Calendar;
import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Pattern;

import org.apache.tools.ant.BuildException;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.osgi.service.resolver.ResolverError;
import org.eclipse.osgi.util.NLS;
import org.eclipse.pde.api.tools.internal.provisional.ApiPlugin;
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.IApiElement;
import org.eclipse.pde.api.tools.internal.provisional.search.ApiSearchEngine;
import org.eclipse.pde.api.tools.internal.provisional.search.IApiSearchReporter;
import org.eclipse.pde.api.tools.internal.provisional.search.IApiSearchRequestor;
import org.eclipse.pde.api.tools.internal.search.ApiDescriptionModifier;
import org.eclipse.pde.api.tools.internal.search.SkippedComponent;
import org.eclipse.pde.api.tools.internal.search.UseMetadata;
import org.eclipse.pde.api.tools.internal.search.UseSearchRequestor;
import org.eclipse.pde.api.tools.internal.search.XmlSearchReporter;
import org.eclipse.pde.api.tools.internal.util.FilteredElements;
import org.eclipse.pde.api.tools.internal.util.Util;

/**
 * Ant task for performing the API use analysis of a given Eclipse SDK
 *
 * @since 1.0.1
 */
public final class ApiUseTask extends CommonUtilsTask {

	/**
	 * If api references should be considered in the search
	 */
	private boolean considerapi = false;
	/**
	 * If internal references should be considered in the search
	 */
	private boolean considerinternal = false;
	/**
	 * if illegal API use should be reported in the search
	 *
	 * @since 1.1
	 */
	private boolean considerillegaluse = false;
	/**
	 * Set of project names that were not searched
	 */
	private TreeSet<SkippedComponent> notsearched = null;
	/**
	 * The regex pattern to use to compose the scope
	 */
	private String scopepattern = null;
	/**
	 * The regex pattern to use to compose the reference set of component ids
	 */
	private String referencepattern = null;

	/**
	 * handle to the baseline install dir to delete after the scan completes
	 */
	private File baselinedir = null;

	/**
	 * Package name patterns (regular expressions) to consider as API or
	 * <code>null</code> if none.
	 */
	private String[] apiPatterns = null;

	/**
	 * Package name patterns (regular expressions) to consider as internal or
	 * <code>null</code> if none.
	 */
	private String[] internalPatterns = null;

	/**
	 * Archive name patterns to not scan during analysis. Formulation:
	 *
	 * <pre>
	 * <bundle name>:<path to jar>
	 * </pre>
	 */
	private String[] archivePatterns = null;

	/**
	 * List of elements excluded from the scope
	 */
	private FilteredElements excludedElements = null;

	/**
	 * List of elements explicitly limiting the scope
	 */
	private FilteredElements includedElements = null;

	/**
	 * Root directory of api_filters files to apply
	 */
	private String filters = null;

	/**
	 * Set the location of the current product you want to search.
	 *
	 * <p>
	 * It can be a .zip, .jar, .tgz, .tar.gz file, or a directory that
	 * corresponds to the Eclipse installation folder. This is the directory is
	 * which you can find the Eclipse executable.
	 * </p>
	 *
	 * @param location the given location for the baseline to analyze
	 */
	public void setLocation(String location) {
		this.currentBaselineLocation = location;
	}

	/**
	 * Set the regular expression pattern used to build the scope of elements to
	 * search for references from in the product location.
	 *
	 * <p>
	 * The pattern must be a well-formatted regular expression as defined here:
	 * http://java.sun.com/j2se/1.4.2/docs/api/java/util/regex/Pattern.html
	 * </p>
	 */
	public void setScopePattern(String scopepattern) {
		this.scopepattern = scopepattern;
	}

	/**
	 * Set the regular expression pattern used to build the scope of elements to
	 * search for references to in the product location.
	 *
	 * <p>
	 * The pattern must be a well-formatted regular expression as defined here:
	 * http://java.sun.com/j2se/1.4.2/docs/api/java/util/regex/Pattern.html
	 * </p>
	 */
	public void setReferencePattern(String referencepattern) {
		this.referencepattern = referencepattern;
	}

	/**
	 * Set the output location where the reports will be generated.
	 *
	 * <p>
	 * Once the task is completed, reports are available in this directory using a
	 * structure similar to the filter root. A sub-folder is created for each
	 * component that has problems to be reported. Each sub-folder contains a file
	 * called "report.xml".
	 * </p>
	 *
	 * <p>
	 * A special folder called "allNonApiBundles" is also created in this folder
	 * that contains a xml file called "report.xml". This file lists all the bundles
	 * that are not using the API Tools nature.
	 * </p>
	 *
	 * @param reportlocation the given location for the reference baseline to
	 *                       analyze
	 */
	public void setReport(String reportlocation) {
		this.reportLocation = reportlocation;
	}

	/**
	 * Set the debug value.
	 * <p>
	 * The possible values are: <code>true</code>, <code>false</code>
	 * </p>
	 * <p>
	 * Default is <code>false</code>.
	 * </p>
	 *
	 * @param debugValue the given debug value
	 */
	public void setDebug(String debugValue) {
		this.debug = Boolean.toString(true).equals(debugValue);
	}

	/**
	 * Sets if references to API types should be considered in the search.
	 * <p>
	 * The possible values are: <code>true</code>, <code>false</code>
	 * </p>
	 * <p>
	 * Default is <code>false</code>.
	 * </p>
	 *
	 * @param considerapi the given value
	 */
	public void setConsiderAPI(String considerapi) {
		this.considerapi = Boolean.toString(true).equals(considerapi);
	}

	/**
	 * Sets if illegal API use should be considered in the search.
	 * <p>
	 * The possible values are: <code>true</code>, <code>false</code>
	 * </p>
	 * <p>
	 * Default is <code>false</code>.
	 * </p>
	 *
	 * @param considerillegaluse the given value
	 */
	public void setConsiderIllegalUse(String considerillegaluse) {
		this.considerillegaluse = Boolean.toString(true).equals(considerillegaluse);
	}

	/**
	 * Sets any package name patterns to consider as API packages.
	 *
	 * @param patterns comma separated list of regular expressions or
	 *            <code>null</code>
	 */
	public void setApiPatterns(String patterns) {
		apiPatterns = parsePatterns(patterns);
	}

	/**
	 * Sets if references to internal types should be considered in the search.
	 * <p>
	 * The possible values are: <code>true</code>, <code>false</code>
	 * </p>
	 * <p>
	 * Default is <code>false</code>.
	 * </p>
	 *
	 * @param considerinternal the given value
	 */
	public void setConsiderInternal(String considerinternal) {
		this.considerinternal = Boolean.toString(true).equals(considerinternal);
	}

	/**
	 * Sets any package name patterns to consider as internal packages.
	 *
	 * @param patterns comma separated list of regular expressions or
	 *            <code>null</code>
	 */
	public void setInternalPatterns(String patterns) {
		internalPatterns = parsePatterns(patterns);
	}

	/**
	 * Sets any archive name patterns to not scan during the analysis.
	 */
	public void setArchivePatterns(String patterns) {
		archivePatterns = parsePatterns(patterns);
	}

	protected void assertParameters() throws BuildException {
		if (this.reportLocation == null) {
			StringWriter out = new StringWriter();
			PrintWriter writer = new PrintWriter(out);
			writer.println(NLS.bind(Messages.ApiUseTask_missing_report_location, new String[] { this.reportLocation }));
			throw new BuildException(String.valueOf(out.getBuffer()));
		}
		if (this.currentBaselineLocation == null) {
			StringWriter out = new StringWriter();
			PrintWriter writer = new PrintWriter(out);
			writer.println(NLS.bind(Messages.ApiUseTask_missing_baseline_argument, new String[] { this.currentBaselineLocation }));
			throw new BuildException(String.valueOf(out.getBuffer()));
		}
		// stop if we don't want to see anything
		if (!considerapi && !considerinternal && !considerillegaluse) {
			throw new BuildException(Messages.UseTask_no_scan_both_types_not_searched_for);
		}
	}

	@Override
	public void execute() throws BuildException {
		assertParameters();
		writeDebugHeader();
		cleanReportLocation();
		UseMetadata data = new UseMetadata(getSearchFlags(), this.scopepattern, this.referencepattern, this.currentBaselineLocation, this.reportLocation, this.apiPatterns, this.internalPatterns, this.archivePatterns, DateFormat.getDateTimeInstance().format(Calendar.getInstance().getTime()), getDescription());
		IApiBaseline baseline = getBaseline(CURRENT_BASELINE_NAME, this.currentBaselineLocation);
		IApiSearchReporter reporter = new XmlSearchReporter(this.reportLocation, this.debug);

		try {
			Set<String> ids = new HashSet<>();
			TreeSet<IApiComponent> scope = new TreeSet<>(Util.componentsorter);
			getContext(baseline, ids, scope);
			ApiSearchEngine engine = new ApiSearchEngine();
			UseSearchRequestor requestor = new UseSearchRequestor(ids, scope.toArray(new IApiElement[scope.size()]), getSearchFlags());
			requestor.setJarPatterns(archivePatterns);
			requestor.setFilterRoot(filters);
			// override API descriptions as required
			if (apiPatterns != null || internalPatterns != null) {
				// modify API descriptions
				ApiDescriptionModifier visitor = new ApiDescriptionModifier(internalPatterns, apiPatterns);
				IApiComponent[] components = baseline.getApiComponents();
				for (IApiComponent component : components) {
					if (!component.isSystemComponent() && !component.isSourceComponent()) {
						visitor.setApiDescription(component.getApiDescription());
						component.getApiDescription().accept(visitor, null);
					}
				}
			}

			ApiPlugin.DEBUG_SEARCH_ENGINE = this.debug;
			engine.search(baseline, requestor, reporter, null);
		} catch (CoreException ce) {
			throw new BuildException(Messages.ApiUseTask_search_engine_problem, ce);
		} finally {
			if (baseline != null) {
				baseline.dispose();
				deleteBaseline(this.currentBaselineLocation, this.baselinedir);
			}
			reporter.reportNotSearched(this.notsearched.toArray(new IApiElement[this.notsearched.size()]));
			reporter.reportMetadata(data);
			reporter.reportCounts();
		}
	}

	/**
	 * Returns if we should add the given component to our search scope
	 *
	 * @return true if the given component should be considered, false otherwise
	 */
	boolean acceptComponent(IApiComponent component, Pattern pattern, boolean allowresolve) throws CoreException {
		if (!allowresolve) {
			ResolverError[] errors = component.getErrors();
			if (errors != null) {
				this.notsearched.add(new SkippedComponent(component.getSymbolicName(), component.getVersion(), errors));
				return false;
			}
		}
		if (component.isSystemComponent()) {
			return false;
		}
		if (pattern != null) {
			return pattern.matcher(component.getSymbolicName()).matches();
		}
		return true;
	}

	/**
	 * Collects the scope elements and reference ids in one pass
	 *
	 * @param baseline the baseline to check the components for
	 * @param ids the live set of reference ids
	 * @param scope the live set of elements for the scope
	 */
	private void getContext(IApiBaseline baseline, Set<String> ids, Set<IApiComponent> scope) throws CoreException {
		excludedElements = CommonUtilsTask.initializeFilteredElements(this.excludeListLocation, baseline, this.debug);
		if (this.debug) {
			System.out.println("===================================================================================="); //$NON-NLS-1$
			System.out.println("Excluded elements list:"); //$NON-NLS-1$
			System.out.println(excludedElements);
		}

		includedElements = CommonUtilsTask.initializeFilteredElements(this.includeListLocation, baseline, this.debug);
		if (this.debug) {
			System.out.println("===================================================================================="); //$NON-NLS-1$
			System.out.println("Included elements list:"); //$NON-NLS-1$
			System.out.println(includedElements);
		}

		IApiComponent[] components = baseline.getApiComponents();
		this.notsearched = new TreeSet<>(Util.componentsorter);
		Pattern refPattern = null, scopePattern = null;
		if (this.referencepattern != null) {
			refPattern = Pattern.compile(this.referencepattern);
		}
		if (this.scopepattern != null) {
			scopePattern = Pattern.compile(this.scopepattern);
		}
		for (IApiComponent component : components) {
			String symbolicName = component.getSymbolicName();
			boolean skip = false;
			if (!includedElements.isEmpty() && !(includedElements.containsExactMatch(symbolicName) || includedElements.containsPartialMatch(symbolicName))) {
				skip = true;
			}
			if (!skip && (excludedElements.containsExactMatch(symbolicName) || excludedElements.containsPartialMatch(symbolicName))) {
				skip = true;
			}
			if (!skip) {
				if (acceptComponent(component, refPattern, true)) {
					ids.add(symbolicName);
				}
				if (acceptComponent(component, scopePattern, false)) {
					scope.add(component);
				}
			} else {
				this.notsearched.add(new SkippedComponent(symbolicName, component.getVersion(), component.getErrors()));
			}
		}
	}

	/**
	 * Returns the set of search flags to use for the
	 * {@link IApiSearchRequestor}
	 *
	 * @return the set of flags to use
	 */
	protected int getSearchFlags() {
		int flags = (this.considerapi ? IApiSearchRequestor.INCLUDE_API : 0);
		flags |= (this.considerinternal ? IApiSearchRequestor.INCLUDE_INTERNAL : 0);
		flags |= (this.considerillegaluse ? IApiSearchRequestor.INCLUDE_ILLEGAL_USE : 0);
		return flags;
	}

	/**
	 * Prepares and creates and new baseline with the given name from the given
	 * location. The returned {@link IApiBaseline} is not checked for resolution
	 * errors or consistency. If <code>null</code> is passed in as a location
	 * <code>null</code> is returned.
	 *
	 * @param name the name to give to the baseline
	 * @param location the location the baseline should be prepared from. If
	 *            <code>null</code> is passed in, <code>null</code> is returned
	 * @return a new {@link IApiBaseline} with the given name from the given
	 *         location or <code>null</code> if the given location is
	 *         <code>null</code>
	 */
	protected IApiBaseline getBaseline(String name, String location) {
		if (location == null) {
			return null;
		}
		// extract the baseline to examine
		long time = 0;
		if (this.debug) {
			time = System.currentTimeMillis();
			System.out.println("Preparing '" + name + "' baseline installation..."); //$NON-NLS-1$ //$NON-NLS-2$
		}
		File installdir = extractSDK(name, location);
		if (this.debug) {
			System.out.println("done in: " + (System.currentTimeMillis() - time) + " ms"); //$NON-NLS-1$ //$NON-NLS-2$
			time = System.currentTimeMillis();
		}
		// create the baseline to examine
		if (this.debug) {
			time = System.currentTimeMillis();
			System.out.println("Creating '" + name + "' baseline..."); //$NON-NLS-1$ //$NON-NLS-2$
		}
		IApiBaseline baseline = createBaseline(name, installdir.getAbsolutePath(), this.eeFileLocation);
		if (this.debug) {
			System.out.println("done in: " + (System.currentTimeMillis() - time) + " ms"); //$NON-NLS-1$ //$NON-NLS-2$
		}
		this.baselinedir = installdir;
		return baseline;
	}

	/**
	 * Cleans the report location specified by the parameter
	 * {@link CommonUtilsTask#reportLocation}
	 */
	protected void cleanReportLocation() {
		if (this.reportLocation == null) {
			return;
		}
		long time = 0;
		if (this.debug) {
			time = System.currentTimeMillis();
			System.out.println("Cleaning report location..."); //$NON-NLS-1$
		}
		File file = new File(this.reportLocation);
		if (file.exists()) {
			Util.delete(file);
		}
		if (this.debug) {
			System.out.println("done in: " + (System.currentTimeMillis() - time) + " ms"); //$NON-NLS-1$ //$NON-NLS-2$
		}
	}

	/**
	 * Writes a general header of debug information iff the debug flag is set to
	 * true
	 */
	protected void writeDebugHeader() {
		if (this.debug) {
			System.out.println("Product location to search : " + this.currentBaselineLocation); //$NON-NLS-1$
			System.out.println("Report location : " + this.reportLocation); //$NON-NLS-1$
			System.out.println("Searching for API references : " + this.considerapi); //$NON-NLS-1$
			System.out.println("Searching for internal references : " + this.considerinternal); //$NON-NLS-1$
			System.out.println("Searching for illegal API use : " + this.considerillegaluse); //$NON-NLS-1$
			if (this.excludeListLocation != null) {
				System.out.println("Exclude list location : " + this.excludeListLocation); //$NON-NLS-1$
			} else {
				System.out.println("No exclude list location"); //$NON-NLS-1$
			}
			if (this.includeListLocation != null) {
				System.out.println("Include list location : " + this.includeListLocation); //$NON-NLS-1$
			} else {
				System.out.println("No include list location"); //$NON-NLS-1$
			}
			if (this.filters != null) {
				System.out.println("API Filter location : " + this.filters); //$NON-NLS-1$
			} else {
				System.out.println("No API filter location"); //$NON-NLS-1$
			}
			if (this.scopepattern == null) {
				System.out.println("No scope pattern defined - searching all bundles"); //$NON-NLS-1$
			} else {
				System.out.println("Scope pattern : " + this.scopepattern); //$NON-NLS-1$
			}
			if (this.referencepattern == null) {
				System.out.println("No baseline pattern defined - reporting references to all bundles"); //$NON-NLS-1$
			} else {
				System.out.println("Baseline pattern : " + this.referencepattern); //$NON-NLS-1$
			}
			System.out.println("-----------------------------------------------------------------------------------------------------"); //$NON-NLS-1$
		}
	}

	/**
	 * Set the exclude list location.
	 *
	 * <p>
	 * The exclude list is used to know what bundles should excluded from the
	 * xml report generated by the task execution. Lines starting with '#' are
	 * ignored from the excluded elements.
	 * </p>
	 * <p>
	 * The format of the exclude list file looks like this:
	 * </p>
	 *
	 * <pre>
	 * # DOC BUNDLES
	 * org.eclipse.jdt.doc.isv
	 * org.eclipse.jdt.doc.user
	 * org.eclipse.pde.doc.user
	 * org.eclipse.platform.doc.isv
	 * org.eclipse.platform.doc.user
	 * # NON-ECLIPSE BUNDLES
	 * com.ibm.icu
	 * com.jcraft.jsch
	 * javax.servlet
	 * javax.servlet.jsp
	 * ...
	 * </pre>
	 * <p>
	 * The location is set using an absolute path.
	 * </p>
	 *
	 * @param excludeListLocation the given location for the excluded list file
	 */
	public void setExcludeList(String excludeListLocation) {
		this.excludeListLocation = excludeListLocation;
	}

	/**
	 * Set the include list location.
	 *
	 * <p>
	 * The include list is used to know what bundles should included from the
	 * xml report generated by the task execution. Lines starting with '#' are
	 * ignored from the included elements.
	 * </p>
	 * <p>
	 * The format of the include list file looks like this:
	 * </p>
	 *
	 * <pre>
	 * # DOC BUNDLES
	 * org.eclipse.jdt.doc.isv
	 * org.eclipse.jdt.doc.user
	 * org.eclipse.pde.doc.user
	 * org.eclipse.platform.doc.isv
	 * org.eclipse.platform.doc.user
	 * # NON-ECLIPSE BUNDLES
	 * com.ibm.icu
	 * com.jcraft.jsch
	 * javax.servlet
	 * javax.servlet.jsp
	 * ...
	 * </pre>
	 * <p>
	 * The location is set using an absolute path.
	 * </p>
	 *
	 * @param includeListLocation the given location for the included list file
	 */
	public void setIncludeList(String includeListLocation) {
		this.includeListLocation = includeListLocation;
	}

	/**
	 * Set the root directory of API filters to use during the use scan.
	 *
	 * The argument is 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 a .api_filters file is found inside the
	 * component both sets of filters will be applied.
	 *
	 * 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 filters the root of the .api_filters files
	 */
	public void setFilters(String filters) {
		this.filters = filters;
	}
}
