/*******************************************************************************
 * Copyright (c) 2000, 2021 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
 *     Gunnar Wagenknecht - adaption to new fetch script builder API
 *******************************************************************************/
package org.eclipse.pde.internal.build.fetch;

import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.osgi.util.NLS;
import org.eclipse.pde.build.Constants;
import org.eclipse.pde.build.IAntScript;
import org.eclipse.pde.build.IFetchFactory;
import org.eclipse.pde.internal.build.IPDEBuildConstants;
import org.eclipse.pde.internal.build.Messages;
import org.eclipse.pde.internal.build.Utils;

/**
 * An <code>FetchTaskFactory</code> for building fetch scripts that will
 * fetch content from a CVS repository (id: <code>CVS</code>).
 * <pre>
 * Map file format:
 * 	type@id,[version]=CVS,args
 * args is a comma-separated list of key/value pairs
 * Accepted args include:
 * 	cvsPassFile - optional password file
 * 	cvsRoot - mandatory repo location
 * 	password - optional password
 * 	path - optional path relative to the cvsRoot
 * 	prebuilt - optional boolean value indicating that the entry points to a pre-built bundle in the repository
 * 	tag - mandatory CVS tag
 * </pre>
 */
public class CVSFetchTaskFactory implements IFetchFactory {
	public static final String ID = "CVS"; //$NON-NLS-1$

	private static final String TARGET_GET_FROM_CVS = "FetchFromCVS"; //$NON-NLS-1$
	private static final String SEPARATOR = ","; //$NON-NLS-1$
	public static final String OVERRIDE_TAG = ID;

	//CVS specific keys used in the map being passed around.
	private static final String KEY_CVSROOT = "cvsRoot"; //$NON-NLS-1$
	private static final String KEY_CVSPASSFILE = "cvsPassFile"; //$NON-NLS-1$
	private static final String KEY_PASSWORD = "password"; //$NON-NLS-1$
	private static final String KEY_PATH = "path"; //$NON-NLS-1$
	private static final String KEY_PREBUILT = "prebuilt"; //$NON-NLS-1$

	//Properties used in the CVS part of the scripts
	private static final String PROP_DESTINATIONFOLDER = "destinationFolder"; //$NON-NLS-1$
	private static final String PROP_CVSROOT = "cvsRoot"; //$NON-NLS-1$
	private static final String PROP_MODULE = "module"; //$NON-NLS-1$
	private static final String PROP_TAG = "tag"; //$NON-NLS-1$
	private static final String PROP_QUIET = "quiet"; //$NON-NLS-1$
	private static final String PROP_REALLYQUIET = "reallyquiet"; //$NON-NLS-1$
	private static final String PROP_FILETOCHECK = "fileToCheck"; //$NON-NLS-1$
	private static final String PROP_ELEMENTNAME = "elementName"; //$NON-NLS-1$

	private void generateAuthentificationAntTask(Map<String, Object> entryInfos, IAntScript script) {
		String password = (String) entryInfos.get(KEY_PASSWORD);
		String cvsPassFileLocation = (String) entryInfos.get(KEY_CVSPASSFILE);
		if (password != null)
			printCVSPassTask((String) entryInfos.get(KEY_CVSROOT), password, cvsPassFileLocation, script);
	}

	@Override
	public void generateRetrieveElementCall(Map<String, Object> entryInfos, IPath destination, IAntScript script) {
		String type = (String) entryInfos.get(KEY_ELEMENT_TYPE);
		String element = (String) entryInfos.get(KEY_ELEMENT_NAME);
		boolean prebuilt = Boolean.valueOf((String) entryInfos.get(KEY_PREBUILT)).booleanValue();

		Map<String, String> params = new HashMap<>(5);
		// we directly export the CVS content into the destination. if we have a pre-built JAR then
		// we want to put it right in the /plugins directory and not a sub-directory so strip off 2 segments
		// to leave us with the build directory (/plugins will be added by the "element" attribute)
		int remove = prebuilt ? 2 : 1;
		String suggestedPath = destination.lastSegment();
		params.put(PROP_DESTINATIONFOLDER, destination.removeLastSegments(remove).toString());
		params.put(PROP_TAG, (String) entryInfos.get(IFetchFactory.KEY_ELEMENT_TAG));
		params.put(PROP_CVSROOT, (String) entryInfos.get(KEY_CVSROOT));
		params.put(PROP_QUIET, "${cvs.quiet}"); //$NON-NLS-1$
		params.put(PROP_REALLYQUIET, "${cvs.reallyquiet}"); //$NON-NLS-1$
		// the call to CVS requires us to pass a destination directory for the files that we are
		// retrieving, so give it the /plugins dir here
		if (prebuilt) {
			if (type.equals(ELEMENT_TYPE_PLUGIN))
				element = IPDEBuildConstants.DEFAULT_PLUGIN_LOCATION;
			else if (type.equals(ELEMENT_TYPE_FEATURE))
				element = IPDEBuildConstants.DEFAULT_FEATURE_LOCATION;
		} else {
			if (suggestedPath != null)
				element = suggestedPath;
		}
		params.put(PROP_ELEMENTNAME, element);
		String module = entryInfos.get(KEY_PATH) == null ? element : (String) entryInfos.get(KEY_PATH);
		params.put(PROP_MODULE, module);

		IPath locationToCheck = (IPath) destination.clone();
		// if we have a pre-built plug-in then we want to check the existence of the JAR file
		// rather than the plug-in manifest.
		if (prebuilt) {
			locationToCheck = locationToCheck.removeLastSegments(1);
			locationToCheck = locationToCheck.append(IPath.fromOSString(module).lastSegment());
		} else {
			if (type.equals(ELEMENT_TYPE_FEATURE)) {
				locationToCheck = locationToCheck.append(Constants.FEATURE_FILENAME_DESCRIPTOR);
			} else if (type.equals(ELEMENT_TYPE_PLUGIN)) {
				locationToCheck = locationToCheck.append(Constants.PLUGIN_FILENAME_DESCRIPTOR);
			} else if (type.equals(ELEMENT_TYPE_FRAGMENT)) {
				locationToCheck = locationToCheck.append(Constants.FRAGMENT_FILENAME_DESCRIPTOR);
			} else if (type.equals(ELEMENT_TYPE_BUNDLE)) {
				locationToCheck = locationToCheck.append(Constants.BUNDLE_FILENAME_DESCRIPTOR);
			}
		}
		params.put(PROP_FILETOCHECK, locationToCheck.toString());

		printAvailableTask(locationToCheck.toString(), locationToCheck.toString(), script);
		if (!prebuilt && (type.equals(IFetchFactory.ELEMENT_TYPE_PLUGIN) || type.equals(IFetchFactory.ELEMENT_TYPE_FRAGMENT))) {
			printAvailableTask(locationToCheck.toString(), locationToCheck.removeLastSegments(1).append(Constants.BUNDLE_FILENAME_DESCRIPTOR).toString(), script);
		}

		generateAuthentificationAntTask(entryInfos, script);
		script.printAntCallTask(TARGET_GET_FROM_CVS, true, params);
	}

	@Override
	public void generateRetrieveFilesCall(final Map<String, Object> entryInfos, IPath destination, final String[] files, IAntScript script) {
		generateAuthentificationAntTask(entryInfos, script);
		String path = (String) entryInfos.get(KEY_PATH);
		for (String file : files) {
			IPath filePath;
			if (path != null) {
				filePath = IPath.fromOSString(path).append(file);
			} else {
				filePath = IPath.fromOSString((String) entryInfos.get(KEY_ELEMENT_NAME)).append(file);
			}
			String tag = (String) entryInfos.get(IFetchFactory.KEY_ELEMENT_TAG);
			String cvsRoot = (String) entryInfos.get(KEY_CVSROOT);
			String dest = "true".equalsIgnoreCase((String) entryInfos.get(KEY_PREBUILT)) ? destination.removeLastSegments(1).toString() : destination.toString(); //$NON-NLS-1$
			printCVSTask("export -r " + tag + ' ' + filePath.toString(), cvsRoot, dest, null, null, "true", Utils.getPropertyFormat(PROP_REALLYQUIET), null, null, "${fetch.failonerror}", script); //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$
			script.println("<move file=\"" + destination + '/' + filePath + "\"" + " tofile=\"" + destination.append(file) + "\" failonerror=\"false\" />"); //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
		}
	}

	@Override
	public void addTargets(IAntScript script) {
		script.printTargetDeclaration(TARGET_GET_FROM_CVS, null, null, "${fileToCheck}", null); //$NON-NLS-1$
		printCVSTask("export -d " + Utils.getPropertyFormat(PROP_ELEMENTNAME), Utils.getPropertyFormat(PROP_CVSROOT), Utils.getPropertyFormat(PROP_DESTINATIONFOLDER), Utils.getPropertyFormat(PROP_MODULE), Utils.getPropertyFormat(PROP_TAG), Utils.getPropertyFormat(PROP_QUIET), Utils.getPropertyFormat(PROP_REALLYQUIET), null, "CVS - " + Utils.getPropertyFormat(PROP_MODULE), script); //$NON-NLS-1$ //$NON-NLS-2$
		script.printTargetEnd();
	}

	/*
	 * Handle the old file format:
	 * Map file arguments:
	 * <code>&lt;TAG&gt;,&lt;CVSROOT&gt;[,&lt;PASSWORD&gt;[,&lt;PATH&gt;[,&lt;CVSPASSFILE&gt;]]]</code>
	 */
	private void legacyParseMapFileEntry(String[] arguments, Properties overrideTags, Map<String, Object> entryInfos) {
		String overrideTag = overrideTags != null ? overrideTags.getProperty(OVERRIDE_TAG) : null;
		entryInfos.put(KEY_CVSPASSFILE, (arguments.length > 4 && !arguments[4].equals("")) ? arguments[4] : null); //$NON-NLS-1$
		entryInfos.put(IFetchFactory.KEY_ELEMENT_TAG, (overrideTag != null && overrideTag.trim().length() != 0 ? overrideTag : arguments[0]));
		entryInfos.put(KEY_CVSROOT, arguments[1]);
		entryInfos.put(KEY_PASSWORD, (arguments.length > 2 && !arguments[2].equals("")) ? arguments[2] : null); //$NON-NLS-1$
		entryInfos.put(KEY_PATH, (arguments.length > 3 && !arguments[3].equals("")) ? arguments[3] : null); //$NON-NLS-1$ 
	}

	@Override
	public void parseMapFileEntry(String repoSpecificentry, Properties overrideTags, Map<String, Object> entryInfos) throws CoreException {
		String[] arguments = Utils.getArrayFromStringWithBlank(repoSpecificentry, SEPARATOR);
		if (arguments.length < 2) {
			String message = NLS.bind(Messages.error_incorrectDirectoryEntry, entryInfos.get(KEY_ELEMENT_NAME));
			throw new CoreException(new Status(IStatus.ERROR, IPDEBuildConstants.PI_PDEBUILD, 1, message, null)); //TODO Need to fix this
		}

		// build up the table of arguments in the map file entry
		Map<String, String> table = new HashMap<>();
		for (String arg : arguments) {
			// if we have at least one arg without an equals sign, then
			// revert back to the legacy parsing
			int index = arg.indexOf('=');
			if (index == -1) {
				legacyParseMapFileEntry(arguments, overrideTags, entryInfos);
				addProjectReference(entryInfos);
				return;
			}
			String key = arg.substring(0, index);
			String value = arg.substring(index + 1);
			table.put(key, value);
		}

		// add entries to the entryInfo map here instead of inside the loop
		// in case we revert to legacy parsing in the middle of the loop (we
		// don't want to contaminate entryInfos
		entryInfos.put(KEY_CVSPASSFILE, table.get(KEY_CVSPASSFILE));
		String overrideTag = overrideTags != null ? overrideTags.getProperty(OVERRIDE_TAG) : null;
		entryInfos.put(IFetchFactory.KEY_ELEMENT_TAG, (overrideTag != null && overrideTag.trim().length() != 0 ? overrideTag : table.get(IFetchFactory.KEY_ELEMENT_TAG)));
		entryInfos.put(KEY_CVSROOT, table.get(KEY_CVSROOT));
		entryInfos.put(KEY_PASSWORD, table.get(KEY_PASSWORD));
		entryInfos.put(KEY_PATH, table.get(KEY_PATH));
		entryInfos.put(KEY_PREBUILT, table.get(KEY_PREBUILT));
		addProjectReference(entryInfos);
	}

	private void addProjectReference(Map<String, Object> entryInfos) {
		String repoLocation = (String) entryInfos.get(KEY_CVSROOT);
		String module = (String) entryInfos.get(KEY_PATH);
		String projectName = (String) entryInfos.get(KEY_ELEMENT_NAME);
		String tag = (String) entryInfos.get(IFetchFactory.KEY_ELEMENT_TAG);

		if (repoLocation != null && projectName != null) {
			String sourceURLs = asReference(repoLocation, module != null ? module : projectName, projectName, tag);
			if (sourceURLs != null) {
				entryInfos.put(Constants.KEY_SOURCE_REFERENCES, sourceURLs);
			}
		}
	}

	/**
	 * Creates an SCMURL reference to the associated source.
	 * 
	 * @return project reference string or <code>null</code> if none
	 */
	private String asReference(String repoLocation, String module, String projectName, String tagName) {
		// parse protocol, host, repository root from repoLocation
		String protocol = null;
		String host = null;
		String root = null;

		int at = repoLocation.indexOf('@');
		if (at < 0) {
			// should be a local protocol
			if (repoLocation.startsWith(":local:")) { //$NON-NLS-1$
				protocol = "local"; //$NON-NLS-1$
				root = repoLocation.substring(7);
			}
		} else if (at < (repoLocation.length() - 2)) {
			String serverRoot = repoLocation.substring(at + 1);
			String protocolUserPass = repoLocation.substring(0, at);
			int colon = serverRoot.indexOf(':');
			if (colon > 0) {
				host = serverRoot.substring(0, colon);
				if (colon < (serverRoot.length() - 2)) {
					root = serverRoot.substring(colon + 1);
				}
				if (protocolUserPass.startsWith(":")) { //$NON-NLS-1$
					colon = protocolUserPass.indexOf(':', 1);
					if (colon > 0) {
						protocol = protocolUserPass.substring(1, colon);
					}
				} else {
					// missing protocol, assume p-server
					protocol = "pserver"; //$NON-NLS-1$
				}
			}
		}

		if (protocol == null || root == null) {
			return null; // invalid syntax
		}

		// use '|' as separator if the root location uses a colon for a Windows path
		String sep = ":"; //$NON-NLS-1$
		if (root.indexOf(':') >= 0) {
			sep = "|"; //$NON-NLS-1$
		}
		StringBuffer buffer = new StringBuffer();
		buffer.append("scm:cvs"); //$NON-NLS-1$
		buffer.append(sep);
		buffer.append(protocol);
		buffer.append(sep);
		if (host != null) {
			buffer.append(host);
			buffer.append(sep);
		}
		buffer.append(root);
		buffer.append(sep);
		buffer.append(module);

		IPath modulePath = IPath.fromOSString(module);
		if (!modulePath.lastSegment().equals(projectName)) {
			buffer.append(";project=\""); //$NON-NLS-1$
			buffer.append(projectName);
			buffer.append('"');
		}

		if (tagName != null && !tagName.equals("HEAD")) { //$NON-NLS-1$
			buffer.append(";tag="); //$NON-NLS-1$
			buffer.append(tagName);
		}
		return buffer.toString();
	}

	/**
	 * Print a <code>cvs</code> task to the Ant script.
	 * 
	 * @param command the CVS command to run
	 * @param cvsRoot value for the CVSROOT variable
	 * @param dest the destination directory for the checked out resources
	 * @param module the module name to check out
	 * @param tag the tag of the module to check out
	 * @param quiet whether or not to print informational messages to the output
	 * @param reallyquiet whether or not to print any messages to the output
	 * @param passFile the name of the password file
	 */
	private void printCVSTask(String command, String cvsRoot, String dest, String module, String tag, String quiet, String reallyquiet, String passFile, String taskname, IAntScript script) {
		printCVSTask(command, cvsRoot, dest, module, tag, quiet, reallyquiet, passFile, taskname, null, script);
	}

	/**
	 * Print a <code>cvs</code> task to the Ant script.
	 * 
	 * @param command the CVS command to run
	 * @param cvsRoot value for the CVSROOT variable
	 * @param dest the destination directory for the checked out resources
	 * @param module the module name to check out
	 * @param tag the tag of the module to check out
	 * @param quiet whether or not to print informational messages to the output
	 * @param reallyquiet whether or not to print any messages to the output
	 * @param passFile the name of the password file
	 * @param failOnError whether or not to throw an exception if something goes wrong
	 */
	private void printCVSTask(String command, String cvsRoot, String dest, String module, String tag, String quiet, String reallyquiet, String passFile, String taskname, String failOnError, IAntScript script) {
		script.printTabs();
		script.print("<cvs"); //$NON-NLS-1$
		script.printAttribute("command", command, false); //$NON-NLS-1$
		script.printAttribute("cvsRoot", cvsRoot, false); //$NON-NLS-1$
		script.printAttribute("dest", dest, false); //$NON-NLS-1$
		script.printAttribute("package", module, false); //$NON-NLS-1$
		script.printAttribute("tag", tag, false); //$NON-NLS-1$
		script.printAttribute("quiet", quiet, false); //$NON-NLS-1$
		script.printAttribute("reallyquiet", reallyquiet, false); //$NON-NLS-1$
		script.printAttribute("passfile", passFile, false); //$NON-NLS-1$
		script.printAttribute("taskname", taskname, false); //$NON-NLS-1$
		script.printAttribute("failonerror", failOnError, false); //$NON-NLS-1$
		script.println("/>"); //$NON-NLS-1$
	}

	/**
	 * Print a <code>cvspass</code> task to the Ant script.
	 * 
	 * @param cvsRoot the name of the repository
	 * @param password the password
	 * @param passFile the name of the password file
	 */
	private void printCVSPassTask(String cvsRoot, String password, String passFile, IAntScript script) {
		script.printTabs();
		script.print("<cvspass"); //$NON-NLS-1$
		script.printAttribute("cvsRoot", cvsRoot, true); //$NON-NLS-1$
		script.printAttribute("password", password, true); //$NON-NLS-1$
		script.printAttribute("passfile", passFile, false); //$NON-NLS-1$
		script.println("/>"); //$NON-NLS-1$
	}

	/**
	 * Print the <code>available</code> Ant task to this script. This task sets a property
	 * value if the given file exists at runtime.
	 * 
	 * @param property the property to set
	 * @param file the file to look for
	 */
	private void printAvailableTask(String property, String file, IAntScript script) {
		script.printTabs();
		script.print("<available"); //$NON-NLS-1$
		script.printAttribute("property", property, true); //$NON-NLS-1$
		script.printAttribute("file", file, false); //$NON-NLS-1$
		script.println("/>"); //$NON-NLS-1$
	}
}
