/*******************************************************************************
 * Copyright (c) 2000, 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.jdt.internal.ui.wizards.buildpaths;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.swt.widgets.Shell;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;

import org.eclipse.core.resources.ResourcesPlugin;

import org.eclipse.jface.dialogs.MessageDialog;

import org.eclipse.jdt.core.ClasspathContainerInitializer;
import org.eclipse.jdt.core.IClasspathAttribute;
import org.eclipse.jdt.core.IClasspathContainer;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IJavaModel;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;

import org.eclipse.jdt.internal.corext.util.JavaModelUtil;
import org.eclipse.jdt.internal.corext.util.Messages;

import org.eclipse.jdt.launching.JavaRuntime;
import org.eclipse.jdt.launching.environments.IExecutionEnvironment;

import org.eclipse.jdt.ui.JavaUI;

import org.eclipse.jdt.internal.ui.JavaPlugin;
import org.eclipse.jdt.internal.ui.wizards.NewWizardMessages;

/**
 *
 */
public class BuildPathSupport {

	public static final String JRE_PREF_PAGE_ID= "org.eclipse.jdt.debug.ui.preferences.VMPreferencePage"; //$NON-NLS-1$
	public static final String EE_PREF_PAGE_ID= "org.eclipse.jdt.debug.ui.jreProfiles"; //$NON-NLS-1$

	/* see also ComplianceConfigurationBlock#PREFS_COMPLIANCE */
	private static final String[] PREFS_COMPLIANCE= new String[] {
			JavaCore.COMPILER_PB_ASSERT_IDENTIFIER, JavaCore.COMPILER_PB_ENUM_IDENTIFIER,
			JavaCore.COMPILER_SOURCE, JavaCore.COMPILER_CODEGEN_TARGET_PLATFORM,
			JavaCore.COMPILER_COMPLIANCE, JavaCore.COMPILER_RELEASE,
			JavaCore.COMPILER_PB_ENABLE_PREVIEW_FEATURES, JavaCore.COMPILER_PB_REPORT_PREVIEW_FEATURES
	};


	private BuildPathSupport() {
		super();
	}

	/**
	 * Returns a deprecation message for a classpath variable name.
	 *
	 * @param variableName classpath variable name
	 * @return the deprecation message, or <code>null</code> iff
	 *         <code>variableName</code> is not a classpath variable or the
	 *         variable is not deprecated
	 */
	public static String getDeprecationMessage(String variableName) {
		String deprecationMessage= JavaCore.getClasspathVariableDeprecationMessage(variableName);
		if (deprecationMessage == null	)
			return null;
		else
			return Messages.format(NewWizardMessages.BuildPathSupport_deprecated,
					new Object[] {variableName, deprecationMessage});
	}

	/**
	 * Finds a source attachment for a new archive in the existing classpaths.
	 * @param elem The new classpath entry
	 * @return A path to be taken for the source attachment or <code>null</code>
	 */
	public static IPath guessSourceAttachment(CPListElement elem) {
		if (elem.getEntryKind() == IClasspathEntry.CPE_CONTAINER) {
			return null;
		}
		IJavaProject currProject= elem.getJavaProject(); // can be null
		try {
			IJavaModel jmodel= JavaCore.create(ResourcesPlugin.getWorkspace().getRoot());
			for (IJavaProject curr : jmodel.getJavaProjects()) {
				if (!curr.equals(currProject)) {
					for (IClasspathEntry entry : curr.getRawClasspath()) {
						if (entry.getEntryKind() == elem.getEntryKind()
							&& entry.getPath().equals(elem.getPath())) {
							IPath attachPath= entry.getSourceAttachmentPath();
							if (attachPath != null && !attachPath.isEmpty()) {
								return attachPath;
							}
						}
					}
				}
			}
		} catch (JavaModelException e) {
			JavaPlugin.log(e.getStatus());
		}
		return null;
	}

	/**
	 * Finds a javadoc location for a new archive in the existing classpaths.
	 * @param elem The new classpath entry
	 * @return A javadoc location found in a similar classpath entry or <code>null</code>.
	 */
	public static String guessJavadocLocation(CPListElement elem) {
		if (elem.getEntryKind() == IClasspathEntry.CPE_CONTAINER) {
			return null;
		}
		IJavaProject currProject= elem.getJavaProject(); // can be null
		try {
			// try if the jar itself contains the source
			IJavaModel jmodel= JavaCore.create(ResourcesPlugin.getWorkspace().getRoot());
			for (IJavaProject curr : jmodel.getJavaProjects()) {
				if (!curr.equals(currProject)) {
					for (IClasspathEntry entry : curr.getRawClasspath()) {
						if (entry.getEntryKind() == elem.getEntryKind() && entry.getPath().equals(elem.getPath())) {
							for (IClasspathAttribute attrib : entry.getExtraAttributes()) {
								if (IClasspathAttribute.JAVADOC_LOCATION_ATTRIBUTE_NAME.equals(attrib.getName())) {
									return attrib.getValue();
								}
							}
						}
					}
				}
			}
		} catch (JavaModelException e) {
			JavaPlugin.log(e.getStatus());
		}
		return null;
	}

	private static class UpdatedClasspathContainer implements IClasspathContainer {

		private IClasspathEntry[] fNewEntries;
		private IClasspathContainer fOriginal;

		public UpdatedClasspathContainer(IClasspathContainer original, IClasspathEntry[] newEntries) {
			fNewEntries= newEntries;
			fOriginal= original;
		}

		@Override
		public IClasspathEntry[] getClasspathEntries() {
			return fNewEntries;
		}

		@Override
		public String getDescription() {
			return fOriginal.getDescription();
		}

		@Override
		public int getKind() {
			return fOriginal.getKind();
		}

		@Override
		public IPath getPath() {
			return fOriginal.getPath();
		}
	}

	/**
	 * Apply a modified classpath entry to the classpath. The classpath entry can also be from a classpath container.
	 * @param shell If not null and the entry could not be found on the projects classpath, a dialog will ask to put the entry on the classpath
	 * @param newEntry The modified entry. The entry's kind or path must be unchanged.
	 * @param changedAttributes The attributes that have changed. See {@link CPListElement} for constants values.
	 * @param jproject Project where the entry belongs to
	 * @param containerPath The path of the entry's parent container or <code>null</code> if the entry is not in a container
	 * @param isReferencedEntry <code>true</code> iff the entry has a {@link IClasspathEntry#getReferencingEntry() referencing entry}
	 * @param monitor The progress monitor to use
	 * @throws CoreException if the update failed
	 */
	public static void modifyClasspathEntry(Shell shell, IClasspathEntry newEntry, String[] changedAttributes, IJavaProject jproject, IPath containerPath, boolean isReferencedEntry, IProgressMonitor monitor) throws CoreException {
		if (containerPath != null) {
			updateContainerClasspath(jproject, containerPath, newEntry, changedAttributes, monitor);
		} else if (isReferencedEntry) {
			updateReferencedClasspathEntry(jproject, newEntry, changedAttributes, monitor);
		} else {
			updateProjectClasspath(shell, jproject, newEntry, changedAttributes, monitor);
		}
	}

	private static void updateContainerClasspath(IJavaProject jproject, IPath containerPath, IClasspathEntry newEntry, String[] changedAttributes, IProgressMonitor monitor) throws CoreException {
		IClasspathContainer container= JavaCore.getClasspathContainer(containerPath, jproject);
		if (container == null) {
			throw new CoreException(new Status(IStatus.ERROR, JavaUI.ID_PLUGIN, IStatus.ERROR, "Container " + containerPath + " cannot be resolved", null));  //$NON-NLS-1$//$NON-NLS-2$
		}
		IClasspathEntry[] entries= container.getClasspathEntries();
		IClasspathEntry[] newEntries= new IClasspathEntry[entries.length];
		for (int i= 0; i < entries.length; i++) {
			IClasspathEntry curr= entries[i];
			if (curr.getEntryKind() == newEntry.getEntryKind() && curr.getPath().equals(newEntry.getPath())) {
				newEntries[i]= getUpdatedEntry(curr, newEntry, changedAttributes, jproject);
			} else {
				newEntries[i]= curr;
			}
		}
		requestContainerUpdate(jproject, container, newEntries);
		monitor.worked(1);
	}

	private static IClasspathEntry getUpdatedEntry(IClasspathEntry currEntry, IClasspathEntry updatedEntry, String[] updatedAttributes, IJavaProject jproject) {
		if (updatedAttributes == null) {
			return updatedEntry; // used updated entry 'as is'
		}
		CPListElement currElem= CPListElement.createFromExisting(currEntry, jproject);
		CPListElement newElem= CPListElement.createFromExisting(updatedEntry, jproject);
		for (String attrib : updatedAttributes) {
			currElem.setAttribute(attrib, newElem.getAttribute(attrib));
		}
		return currElem.getClasspathEntry();
	}

	/**
	 * Request a container update.
	 * @param jproject The project of the container
	 * @param container The container to request a change to
	 * @param newEntries The updated entries
	 * @throws CoreException if the request failed
	 */
	public static void requestContainerUpdate(IJavaProject jproject, IClasspathContainer container, IClasspathEntry[] newEntries) throws CoreException {
		IPath containerPath= container.getPath();
		IClasspathContainer updatedContainer= new UpdatedClasspathContainer(container, newEntries);
		ClasspathContainerInitializer initializer= JavaCore.getClasspathContainerInitializer(containerPath.segment(0));
		if (initializer != null) {
			initializer.requestClasspathContainerUpdate(containerPath, jproject, updatedContainer);
		}
	}

	private static void updateProjectClasspath(Shell shell, IJavaProject jproject, IClasspathEntry newEntry, String[] changedAttributes, IProgressMonitor monitor) throws JavaModelException {
		IClasspathEntry[] oldClasspath= jproject.getRawClasspath();
		int nEntries= oldClasspath.length;
		ArrayList<IClasspathEntry> newEntries= new ArrayList<>(nEntries + 1);
		int entryKind= newEntry.getEntryKind();
		IPath jarPath= newEntry.getPath();
		boolean found= false;
		for (int i= 0; i < nEntries; i++) {
			IClasspathEntry curr= oldClasspath[i];
			if (curr.getEntryKind() == entryKind && curr.getPath().equals(jarPath)) {
				// add modified entry
				newEntries.add(getUpdatedEntry(curr, newEntry, changedAttributes, jproject));
				found= true;
			} else {
				newEntries.add(curr);
			}
		}
		if (!found) {
			if (!putJarOnClasspathDialog(shell)) {
				return;
			}
			// add new
			newEntries.add(newEntry);
		}
		IClasspathEntry[] newClasspath= newEntries.toArray(new IClasspathEntry[newEntries.size()]);
		jproject.setRawClasspath(newClasspath, monitor);
	}

	private static boolean putJarOnClasspathDialog(final Shell shell) {
		if (shell == null) {
			return false;
		}

		final boolean[] result= new boolean[1];
		shell.getDisplay().syncExec(new Runnable() {
			@Override
			public void run() {
				String title= NewWizardMessages.BuildPathSupport_putoncpdialog_title;
				String message= NewWizardMessages.BuildPathSupport_putoncpdialog_message;
				result[0]= MessageDialog.openQuestion(shell, title, message);
			}
		});
		return result[0];
	}

	/**
	 * Apply a modified referenced classpath entry to the classpath.
	 * @param newReferencedEntry the modified entry. The entry's kind or path must be unchanged.
	 * @param changedAttributes the attributes that have changed. See {@link CPListElement} for constants values.
	 * @param jproject project where the entry belongs to
	 * @param monitor the progress monitor to use
	 * @throws CoreException if the update failed
	 */
	private static void updateReferencedClasspathEntry(IJavaProject jproject, IClasspathEntry newReferencedEntry, String[] changedAttributes, IProgressMonitor monitor) throws CoreException {
		IClasspathEntry[] oldReferencedClasspath= jproject.getReferencedClasspathEntries();
		int nEntries= oldReferencedClasspath.length;
		ArrayList<IClasspathEntry> newReferencedEntries= new ArrayList<>(nEntries + 1);
		int entryKind= newReferencedEntry.getEntryKind();
		IPath jarPath= newReferencedEntry.getPath();
		boolean found= false;
		for (int i= 0; i < nEntries; i++) {
			IClasspathEntry curr= oldReferencedClasspath[i];
			if (curr.getEntryKind() == entryKind && curr.getPath().equals(jarPath)) {
				// add modified entry
				newReferencedEntries.add(getUpdatedEntry(curr, newReferencedEntry, changedAttributes, jproject));
				found= true;
			} else {
				newReferencedEntries.add(curr);
			}
		}
		if (!found) {
			newReferencedEntries.add(newReferencedEntry);
		}
		IClasspathEntry[] newReferencedClasspath= newReferencedEntries.toArray(new IClasspathEntry[newReferencedEntries.size()]);

		jproject.setRawClasspath(jproject.getRawClasspath(), newReferencedClasspath, jproject.getOutputLocation(), monitor);
	}

	/**
	 * Sets the default compiler compliance options iff <code>modifiedClassPathEntries</code>
	 * contains a classpath container entry that is modified or new and that points to an execution
	 * environment. Does nothing if the EE or the options could not be resolved.
	 *
	 * @param javaProject the Java project
	 * @param modifiedClassPathEntries a list of {@link CPListElement}
	 *
	 * @see #getEEOptions(IExecutionEnvironment)
	 *
	 * @since 3.5
	 */
	public static void setEEComplianceOptions(IJavaProject javaProject, List<CPListElement> modifiedClassPathEntries) {
		for (CPListElement entry : modifiedClassPathEntries) {
			if (entry.getEntryKind() == IClasspathEntry.CPE_CONTAINER) {
				IPath path= entry.getPath();
				if (! path.equals(entry.getOrginalPath())) {
					String eeID= JavaRuntime.getExecutionEnvironmentId(path);
					if (eeID != null) {
						setEEComplianceOptions(javaProject, eeID, null);
						return;
					}
				}
			}
		}
	}

	/**
	 * Sets the default compiler compliance options based on the given execution environment.
	 * Does nothing if the EE or the options could not be resolved.
	 *
	 * @param javaProject the Java project
	 * @param eeID the execution environment ID
	 * @param newProjectCompliance compliance to set for a new project, can be <code>null</code>
	 *
	 * @see #getEEOptions(IExecutionEnvironment)
	 *
	 * @since 3.5
	 */
	public static void setEEComplianceOptions(IJavaProject javaProject, String eeID, String newProjectCompliance) {
		IExecutionEnvironment ee= JavaRuntime.getExecutionEnvironmentsManager().getEnvironment(eeID);
		if (ee != null) {
			Map<String, String> options= javaProject.getOptions(false);
			Map<String, String> eeOptions= getEEOptions(ee);
			if (eeOptions != null) {
				for (String option : PREFS_COMPLIANCE) {
					String val= eeOptions.get(option);
					if (val != null) {
						options.put(option, val);
					}
				}

				if (newProjectCompliance != null) {
					JavaModelUtil.setDefaultClassfileOptions(options, newProjectCompliance); // complete compliance options
				}

				String option= JavaCore.COMPILER_CODEGEN_INLINE_JSR_BYTECODE;
				String inlineJSR= eeOptions.get(option);
				if (inlineJSR != null) {
					options.put(option, inlineJSR);
				}

				javaProject.setOptions(options);
			}
		}
	}

	/**
	 * Returns the compliance options from the given EE. If the result is not <code>null</code>,
	 * it contains at least these core options:
	 * <ul>
	 * <li>{@link JavaCore#COMPILER_COMPLIANCE}</li>
	 * <li>{@link JavaCore#COMPILER_SOURCE}</li>
	 * <li>{@link JavaCore#COMPILER_CODEGEN_TARGET_PLATFORM}</li>
	 * <li>{@link JavaCore#COMPILER_PB_ASSERT_IDENTIFIER}</li>
	 * <li>{@link JavaCore#COMPILER_PB_ENUM_IDENTIFIER}</li>
	 * <li>{@link JavaCore#COMPILER_CODEGEN_INLINE_JSR_BYTECODE} for compliance levels 1.5 and greater</li>
	 * <li>{@link JavaCore#COMPILER_PB_ENABLE_PREVIEW_FEATURES} for compliance levels 11 and greater</li>
	 * <li>{@link JavaCore#COMPILER_PB_REPORT_PREVIEW_FEATURES} for compliance levels 11 and greater</li>
	 * </ul>
	 *
	 * @param ee the EE, can be <code>null</code>
	 * @return the options, or <code>null</code> if none
	 * @since 3.5
	 */
	public static Map<String, String> getEEOptions(IExecutionEnvironment ee) {
		if (ee == null)
			return null;
		Map<String, String> eeOptions= ee.getComplianceOptions();
		if (eeOptions == null)
			return null;

		Object complianceOption= eeOptions.get(JavaCore.COMPILER_COMPLIANCE);
		if (!(complianceOption instanceof String))
			return null;

		// eeOptions can miss some options, make sure they are complete:
		HashMap<String, String> options= new HashMap<>();
		JavaModelUtil.setComplianceOptions(options, (String)complianceOption);
		options.putAll(eeOptions);
		return options;
	}
}
