/*******************************************************************************
 * 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.util.TreeSet;
import java.util.regex.Pattern;

import org.apache.tools.ant.BuildException;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.osgi.service.resolver.ResolverError;
import org.eclipse.osgi.util.NLS;
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.search.ReferenceLookupVisitor;
import org.eclipse.pde.api.tools.internal.search.SkippedComponent;
import org.eclipse.pde.api.tools.internal.search.UseScanParser;
import org.eclipse.pde.api.tools.internal.util.FilteredElements;
import org.eclipse.pde.api.tools.internal.util.Util;

/**
 * Ant task for performing analysis of an API use scan against an alterante
 * target (migration candidate)
 */
public final class ApiMigrationTask extends CommonUtilsTask {

	/**
	 * Set of project names that were not searched
	 */
	private final 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;

	/**
	 * Location of the API use scan to resolve in the migration candidate. This
	 * can be the root directory containing an 'xml' sub directory or the 'xml'
	 * directory itself.
	 */
	private String scanLocation = null;

	/**
	 * Set the location of the product you want to use as the migration
	 * candidate.
	 *
	 * <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 location for the migration candidate to consider
	 */
	public void setCandidate(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 location of an existing API use scan containing references to
	 * re-resolve in the migration candidate. This can be the root directory
	 * containing 'xml' and 'html' subdirectories or the 'xml' directory itself.
	 *
	 * @param scanLocation the location of an existing API use scan
	 */
	public void setUseScan(String scanLocation) {
		this.scanLocation = scanLocation;
	}

	/**
	 * 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);
	}

	/**
	 * @see ApiUseTask#assertParameters()
	 */
	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()));
		}
		if (this.scanLocation == null) {
			StringWriter out = new StringWriter();
			PrintWriter writer = new PrintWriter(out);
			writer.println(NLS.bind(Messages.ApiMigrationTask_missing_scan_location, new String[] { this.scanLocation }));
			throw new BuildException(String.valueOf(out.getBuffer()));
		}
		String xmlLocation = scanLocation;
		File file = new File(xmlLocation);
		if (!file.exists()) {
			StringWriter out = new StringWriter();
			PrintWriter writer = new PrintWriter(out);
			writer.println(NLS.bind(Messages.ApiMigrationTask_scan_location_not_exist, new String[] { this.scanLocation }));
			throw new BuildException(String.valueOf(out.getBuffer()));
		}
		if (!file.isDirectory()) {
			StringWriter out = new StringWriter();
			PrintWriter writer = new PrintWriter(out);
			writer.println(NLS.bind(Messages.ApiMigrationTask_scan_location_not_dir, new String[] { this.scanLocation }));
			throw new BuildException(String.valueOf(out.getBuffer()));
		}
		if (this.reportLocation.equals(scanLocation)) {
			StringWriter out = new StringWriter();
			PrintWriter writer = new PrintWriter(out);
			writer.println(NLS.bind(Messages.ApiMigrationTask_scan_locatoin_same_as_report_location, new String[] { this.scanLocation }));
			throw new BuildException(String.valueOf(out.getBuffer()));
		}

	}

	@Override
	public void execute() throws BuildException {
		assertParameters();
		writeDebugHeader();
		cleanReportLocation();

		IApiBaseline baseline = getBaseline(CURRENT_BASELINE_NAME, this.currentBaselineLocation);
		try {
			String xmlLocation = scanLocation;
			File file = new File(xmlLocation);
			File nested = new File(file, "xml"); //$NON-NLS-1$
			if (nested.exists() && nested.isDirectory()) {
				file = nested;
			}
			ReferenceLookupVisitor lookup = new ReferenceLookupVisitor(baseline, this.reportLocation);
			lookup.setAnalysisScope(scopepattern);
			lookup.setTargetScope(referencepattern);

			FilteredElements 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);
			}
			lookup.setExcludedElements(excludedElements);

			FilteredElements 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);
			}
			lookup.setIncludedElements(includedElements);

			UseScanParser parser = new UseScanParser();
			parser.parse(file.getAbsolutePath(), new NullProgressMonitor(), lookup);
		} catch (CoreException ce) {
			throw new BuildException(ce.getStatus().getMessage(), ce);
		} catch (Exception e) {
			throw new BuildException(e.getMessage(), e);
		} finally {
			if (baseline != null) {
				baseline.dispose();
				deleteBaseline(this.currentBaselineLocation, this.baselinedir);
			}
		}
	}

	/**
	 * 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;
	}

	/**
	 * 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("Migration candidate to analyze : " + this.currentBaselineLocation); //$NON-NLS-1$
			System.out.println("Report location : " + this.reportLocation); //$NON-NLS-1$
			System.out.println("Scan location : " + this.scanLocation); //$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;
	}
}
